diff --git a/.gitignore b/.gitignore index f09501f..406964b 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,10 @@ yarn-error.log* .env .env*.local postiz/.env +infisical/.env + +# generated compose files (rebuild with ./generate.sh) +/generated/ # vercel .vercel diff --git a/DEPLOY.md b/DEPLOY.md new file mode 100644 index 0000000..44994e4 --- /dev/null +++ b/DEPLOY.md @@ -0,0 +1,193 @@ +# rSpace Deployment Guide + +## Architecture + +``` +spaces.yml # Single source of truth for all spaces + | + v +generate.sh # Reads config, produces per-space compose files + | + v +generated/ # Per-space docker-compose files (gitignored) + docker-compose.space-*.yml + tunnel-hostnames.yml # Cloudflare tunnel entries + dns-commands.sh # DNS CNAME setup commands +``` + +Each "space" is a community Postiz instance with its own domain, database, Redis, and Temporal stack — all defined by a single block in `spaces.yml`. + +## Prerequisites + +- `yq` v4+ ([install](https://github.com/mikefarah/yq#install)) +- Docker + Docker Compose +- Access to Netcup RS 8000 (`ssh netcup`) +- Cloudflare dashboard access (for DNS) + +## Adding a New Space + +### 1. Define the space + +Edit `spaces.yml` and add a block: + +```yaml +spaces: + mycofi: + primary_domain: socials.mycofi.earth + fallback_domain: mycofi.rsocials.online + email_from: noreply@mycofi.earth + services: + - postiz +``` + +Override any defaults if needed: + +```yaml + mycofi: + primary_domain: socials.mycofi.earth + fallback_domain: mycofi.rsocials.online + email_from: noreply@mycofi.earth + postiz: + disable_registration: true + email_from_name: MycoFi Socials + services: + - postiz +``` + +### 2. Generate compose files + +```bash +./generate.sh # All spaces +./generate.sh mycofi # Single space +``` + +Output: `generated/docker-compose.space-mycofi.yml` + +### 3. Create secrets + +**Option A: .env file (simple)** + +Create `generated/.env` (or per-space file): + +```bash +JWT_SECRET=$(openssl rand -hex 32) +POSTGRES_PASSWORD=$(openssl rand -hex 16) +EMAIL_PASS=your-mailcow-password +``` + +**Option B: Infisical (recommended for production)** + +```bash +# Install CLI: https://infisical.com/docs/cli/overview +infisical secrets set JWT_SECRET="$(openssl rand -hex 32)" \ + --projectId --env prod +``` + +### 4. Deploy + +```bash +# Simple deploy +cd generated/ +docker compose -f docker-compose.space-mycofi.yml up -d + +# With Infisical +infisical run --projectId --env prod -- \ + infisical run --projectId --env prod -- \ + docker compose -f docker-compose.space-mycofi.yml up -d +``` + +### 5. Configure DNS + Tunnel + +Add entries from `generated/tunnel-hostnames.yml` to `/root/cloudflared/config.yml` on Netcup: + +```yaml +- hostname: socials.mycofi.earth + service: http://localhost:80 +- hostname: mycofi.rsocials.online + service: http://localhost:80 +``` + +Restart the tunnel: + +```bash +ssh netcup "docker restart cloudflared" +``` + +Add Cloudflare DNS CNAMEs (in the dashboard for each domain zone): + +| Type | Name | Target | Proxy | +|------|------|--------|-------| +| CNAME | `socials` | `a838e9dc-...cfargotunnel.com` | Proxied | + +### 6. Verify + +- `https://socials.mycofi.earth` -> Postiz login +- `https://mycofi.rsocials.online` -> 301 redirect to primary domain + +## File Reference + +| File | Purpose | +|------|---------| +| `spaces.yml` | Master config — all spaces, domains, defaults | +| `docker-compose.template.yml` | Postiz stack template with `{{PLACEHOLDER}}` vars | +| `generate.sh` | Reads config, fills template, outputs compose files | +| `generated/` | Build artifacts (gitignored) | +| `postiz/docker-compose.yml` | Legacy manual compose (kept for reference) | +| `infisical/docker-compose.yml` | Infisical secret manager deployment | +| `infisical/.env.example` | Required env vars for Infisical | + +## Infisical Setup + +### Deploy Infisical on Netcup + +```bash +scp -r infisical/ netcup:/opt/infisical/ +ssh netcup +cd /opt/infisical + +# Generate secrets +cat > .env < +EOF + +docker compose up -d +``` + +### Add DNS + Tunnel + +1. Add `secrets.jeffemmett.com` CNAME in Cloudflare +2. Add hostname to tunnel config: + ```yaml + - hostname: secrets.jeffemmett.com + service: http://localhost:80 + ``` +3. `docker restart cloudflared` +4. Visit `https://secrets.jeffemmett.com` to complete setup + +### Infisical Project Structure + +``` +Organization: rSpace + Project: shared -> SMTP creds, AI keys, Cloudflare tokens + Project: space- -> Per-space: JWT_SECRET, POSTGRES_PASSWORD, social API keys + Project: rspace-online -> Landing page: GEMINI_API_KEY, RUNPOD keys +``` + +## Defaults + +All defaults are in `spaces.yml` under `defaults.postiz:`. Per-space overrides go under `spaces..postiz:`. + +| Setting | Default | +|---------|---------| +| Image | `ghcr.io/gitroomhq/postiz-app:latest` | +| Port | 5000 | +| PostgreSQL | `postgres:17-alpine` | +| Redis | `redis:7.2` | +| Temporal | `temporalio/auto-setup:1.28.1` | +| Email host | `mailcowdockerized-postfix-mailcow-1` | +| Email port | 587 | +| Storage | local | +| Registration | enabled | diff --git a/docker-compose.template.yml b/docker-compose.template.yml new file mode 100644 index 0000000..b1b1e4f --- /dev/null +++ b/docker-compose.template.yml @@ -0,0 +1,201 @@ +# ============================================================================= +# Postiz Stack — Space: {{SPACE_NAME}} +# ============================================================================= +# Generated by generate.sh from spaces.yml — DO NOT EDIT DIRECTLY. +# To modify, edit spaces.yml and re-run: ./generate.sh +# Domain: {{PRIMARY_DOMAIN}} (fallback: {{FALLBACK_DOMAIN}}) + +services: + postiz-{{SPACE_SLUG}}: + image: {{POSTIZ_IMAGE}} + container_name: postiz-{{SPACE_SLUG}} + restart: always + environment: + MAIN_URL: 'https://{{PRIMARY_DOMAIN}}' + FRONTEND_URL: 'https://{{PRIMARY_DOMAIN}}' + NEXT_PUBLIC_BACKEND_URL: 'https://{{PRIMARY_DOMAIN}}/api' + JWT_SECRET: '${JWT_SECRET}' + DATABASE_URL: 'postgresql://postiz:${POSTGRES_PASSWORD}@postiz-{{SPACE_SLUG}}-postgres:5432/postiz' + REDIS_URL: 'redis://postiz-{{SPACE_SLUG}}-redis:6379' + BACKEND_INTERNAL_URL: 'http://localhost:3000' + TEMPORAL_ADDRESS: "postiz-{{SPACE_SLUG}}-temporal:7233" + IS_GENERAL: '{{IS_GENERAL}}' + DISABLE_REGISTRATION: '{{DISABLE_REG}}' + + # Storage + STORAGE_PROVIDER: '{{STORAGE_PROVIDER}}' + UPLOAD_DIRECTORY: '{{UPLOAD_DIR}}' + NEXT_PUBLIC_UPLOAD_DIRECTORY: '{{UPLOAD_DIR}}' + + # Social Media API Settings (from .env or Infisical) + X_API_KEY: '${X_API_KEY:-}' + X_API_SECRET: '${X_API_SECRET:-}' + LINKEDIN_CLIENT_ID: '${LINKEDIN_CLIENT_ID:-}' + LINKEDIN_CLIENT_SECRET: '${LINKEDIN_CLIENT_SECRET:-}' + REDDIT_CLIENT_ID: '${REDDIT_CLIENT_ID:-}' + REDDIT_CLIENT_SECRET: '${REDDIT_CLIENT_SECRET:-}' + THREADS_APP_ID: '${THREADS_APP_ID:-}' + THREADS_APP_SECRET: '${THREADS_APP_SECRET:-}' + FACEBOOK_APP_ID: '${FACEBOOK_APP_ID:-}' + FACEBOOK_APP_SECRET: '${FACEBOOK_APP_SECRET:-}' + YOUTUBE_CLIENT_ID: '${YOUTUBE_CLIENT_ID:-}' + YOUTUBE_CLIENT_SECRET: '${YOUTUBE_CLIENT_SECRET:-}' + TIKTOK_CLIENT_ID: '${TIKTOK_CLIENT_ID:-}' + TIKTOK_CLIENT_SECRET: '${TIKTOK_CLIENT_SECRET:-}' + DISCORD_CLIENT_ID: '${DISCORD_CLIENT_ID:-}' + DISCORD_CLIENT_SECRET: '${DISCORD_CLIENT_SECRET:-}' + DISCORD_BOT_TOKEN_ID: '${DISCORD_BOT_TOKEN_ID:-}' + MASTODON_URL: '${MASTODON_URL:-https://mastodon.social}' + MASTODON_CLIENT_ID: '${MASTODON_CLIENT_ID:-}' + MASTODON_CLIENT_SECRET: '${MASTODON_CLIENT_SECRET:-}' + SLACK_ID: '${SLACK_ID:-}' + SLACK_SECRET: '${SLACK_SECRET:-}' + SLACK_SIGNING_SECRET: '${SLACK_SIGNING_SECRET:-}' + PINTEREST_CLIENT_ID: '${PINTEREST_CLIENT_ID:-}' + PINTEREST_CLIENT_SECRET: '${PINTEREST_CLIENT_SECRET:-}' + + # Email + EMAIL_PROVIDER: '{{EMAIL_PROVIDER}}' + EMAIL_FROM_NAME: '{{EMAIL_FROM_NAME}}' + EMAIL_FROM_ADDRESS: '{{EMAIL_FROM}}' + EMAIL_HOST: '{{EMAIL_HOST}}' + EMAIL_PORT: '{{EMAIL_PORT}}' + EMAIL_SECURE: '{{EMAIL_SECURE}}' + EMAIL_USER: '{{EMAIL_USER}}' + EMAIL_PASS: '${EMAIL_PASS}' + NODE_TLS_REJECT_UNAUTHORIZED: '0' + + # AI + OPENAI_API_KEY: '${OPENAI_API_KEY:-}' + + # Misc + NX_ADD_PLUGINS: false + API_LIMIT: {{API_LIMIT}} + + volumes: + - postiz-{{SPACE_SLUG}}-config:/config/ + - postiz-{{SPACE_SLUG}}-uploads:/uploads/ + labels: + - "traefik.enable=true" + # Primary domain -> Postiz + - "traefik.http.routers.postiz-{{SPACE_SLUG}}.rule=Host(`{{PRIMARY_DOMAIN}}`)" + - "traefik.http.routers.postiz-{{SPACE_SLUG}}.entrypoints=web" + - "traefik.http.services.postiz-{{SPACE_SLUG}}.loadbalancer.server.port={{POSTIZ_PORT}}" + # Redirect fallback domain -> primary domain + - "traefik.http.routers.postiz-{{SPACE_SLUG}}-redirect.rule=Host(`{{FALLBACK_DOMAIN}}`)" + - "traefik.http.routers.postiz-{{SPACE_SLUG}}-redirect.entrypoints=web" + - "traefik.http.routers.postiz-{{SPACE_SLUG}}-redirect.middlewares=postiz-{{SPACE_SLUG}}-redirect" + - "traefik.http.middlewares.postiz-{{SPACE_SLUG}}-redirect.redirectregex.regex=^https?://{{FALLBACK_ESCAPED}}(.*)" + - "traefik.http.middlewares.postiz-{{SPACE_SLUG}}-redirect.redirectregex.replacement=https://{{PRIMARY_DOMAIN}}$${1}" + - "traefik.http.middlewares.postiz-{{SPACE_SLUG}}-redirect.redirectregex.permanent=true" + - "traefik.docker.network=traefik-public" + networks: + - traefik-public + - postiz-{{SPACE_SLUG}}-internal + - mailcow-network + depends_on: + postiz-{{SPACE_SLUG}}-postgres: + condition: service_healthy + postiz-{{SPACE_SLUG}}-redis: + condition: service_healthy + + postiz-{{SPACE_SLUG}}-postgres: + image: {{POSTGRES_IMAGE}} + container_name: postiz-{{SPACE_SLUG}}-postgres + restart: always + environment: + POSTGRES_PASSWORD: '${POSTGRES_PASSWORD}' + POSTGRES_USER: postiz + POSTGRES_DB: postiz + volumes: + - postiz-{{SPACE_SLUG}}-postgres-data:/var/lib/postgresql/data + networks: + - postiz-{{SPACE_SLUG}}-internal + healthcheck: + test: pg_isready -U postiz -d postiz + interval: 10s + timeout: 3s + retries: 3 + cap_drop: + - ALL + cap_add: + - DAC_OVERRIDE + - FOWNER + - SETGID + - SETUID + security_opt: + - no-new-privileges:true + + postiz-{{SPACE_SLUG}}-redis: + image: {{REDIS_IMAGE}} + container_name: postiz-{{SPACE_SLUG}}-redis + restart: always + healthcheck: + test: redis-cli ping + interval: 10s + timeout: 3s + retries: 3 + volumes: + - postiz-{{SPACE_SLUG}}-redis-data:/data + networks: + - postiz-{{SPACE_SLUG}}-internal + cap_drop: + - ALL + cap_add: + - SETGID + - SETUID + security_opt: + - no-new-privileges:true + + postiz-{{SPACE_SLUG}}-temporal-postgres: + image: {{TEMPORAL_PG_IMAGE}} + container_name: postiz-{{SPACE_SLUG}}-temporal-postgres + restart: always + environment: + POSTGRES_PASSWORD: temporal + POSTGRES_USER: temporal + networks: + - postiz-{{SPACE_SLUG}}-internal + volumes: + - postiz-{{SPACE_SLUG}}-temporal-postgres-data:/var/lib/postgresql/data + cap_drop: + - ALL + cap_add: + - DAC_OVERRIDE + - FOWNER + - SETGID + - SETUID + security_opt: + - no-new-privileges:true + + postiz-{{SPACE_SLUG}}-temporal: + image: {{TEMPORAL_IMAGE}} + container_name: postiz-{{SPACE_SLUG}}-temporal + restart: always + depends_on: + - postiz-{{SPACE_SLUG}}-temporal-postgres + environment: + - DB=postgres12 + - DB_PORT=5432 + - POSTGRES_USER=temporal + - POSTGRES_PWD=temporal + - POSTGRES_SEEDS=postiz-{{SPACE_SLUG}}-temporal-postgres + - TEMPORAL_NAMESPACE=default + networks: + - postiz-{{SPACE_SLUG}}-internal + +volumes: + postiz-{{SPACE_SLUG}}-postgres-data: + postiz-{{SPACE_SLUG}}-redis-data: + postiz-{{SPACE_SLUG}}-config: + postiz-{{SPACE_SLUG}}-uploads: + postiz-{{SPACE_SLUG}}-temporal-postgres-data: + +networks: + traefik-public: + external: true + postiz-{{SPACE_SLUG}}-internal: + internal: true + mailcow-network: + external: true + name: mailcowdockerized_mailcow-network diff --git a/generate.sh b/generate.sh new file mode 100755 index 0000000..f4972c0 --- /dev/null +++ b/generate.sh @@ -0,0 +1,297 @@ +#!/usr/bin/env bash +# ============================================================================= +# generate.sh — Generate per-space docker-compose files from spaces.yml +# ============================================================================= +# +# Reads spaces.yml and produces: +# generated/docker-compose.space-.yml (per-space Postiz stack) +# generated/tunnel-hostnames.yml (Cloudflare tunnel entries) +# generated/dns-commands.sh (Cloudflare DNS CNAME commands) +# +# Requirements: yq (https://github.com/mikefarah/yq) v4+ +# +# Usage: +# ./generate.sh # Generate all spaces +# ./generate.sh crypto-commons # Generate a single space +# + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CONFIG="${SCRIPT_DIR}/spaces.yml" +TEMPLATE="${SCRIPT_DIR}/docker-compose.template.yml" +OUTDIR="${SCRIPT_DIR}/generated" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color + +# --------------------------------------------------------------------------- +# Preflight checks +# --------------------------------------------------------------------------- +if ! command -v yq &>/dev/null; then + echo -e "${RED}Error: yq is required but not installed.${NC}" + echo " Install: https://github.com/mikefarah/yq#install" + echo " Quick: sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 && sudo chmod +x /usr/local/bin/yq" + exit 1 +fi + +if [[ ! -f "$CONFIG" ]]; then + echo -e "${RED}Error: spaces.yml not found at ${CONFIG}${NC}" + exit 1 +fi + +if [[ ! -f "$TEMPLATE" ]]; then + echo -e "${RED}Error: docker-compose.template.yml not found at ${TEMPLATE}${NC}" + exit 1 +fi + +# --------------------------------------------------------------------------- +# Read defaults from spaces.yml +# --------------------------------------------------------------------------- +get_default() { + local key="$1" + yq ".defaults.postiz.${key}" "$CONFIG" 2>/dev/null || echo "" +} + +get_space() { + local space="$1" key="$2" + yq ".spaces.${space}.${key}" "$CONFIG" 2>/dev/null || echo "" +} + +get_space_override() { + local space="$1" key="$2" + local val + val=$(yq ".spaces.${space}.postiz.${key} // \"\"" "$CONFIG" 2>/dev/null) + if [[ -z "$val" || "$val" == "null" ]]; then + get_default "$key" + else + echo "$val" + fi +} + +# Escape dots for regex (used in Traefik redirect rules) +escape_dots() { + # Produce \\. for each dot (double-escaped for YAML double-quoted strings) + # sed replacement needs 4 backslashes to produce 2 in output + echo "$1" | sed 's/\./\\\\\\\\./g' +} + +# --------------------------------------------------------------------------- +# Generate compose file for a single space +# --------------------------------------------------------------------------- +generate_space() { + local space="$1" + + local primary_domain fallback_domain email_from + primary_domain=$(get_space "$space" "primary_domain") + fallback_domain=$(get_space "$space" "fallback_domain") + email_from=$(get_space "$space" "email_from") + + if [[ -z "$primary_domain" || "$primary_domain" == "null" ]]; then + echo -e "${RED} Error: spaces.${space}.primary_domain is required${NC}" + return 1 + fi + + # Read defaults (with per-space overrides) + local postiz_image postiz_port postgres_image redis_image temporal_image temporal_pg_image + local email_provider email_from_name email_host email_port email_secure email_user + local storage_provider upload_dir disable_reg is_general api_limit + + postiz_image=$(get_space_override "$space" "image") + postiz_port=$(get_space_override "$space" "port") + postgres_image=$(get_space_override "$space" "postgres_image") + redis_image=$(get_space_override "$space" "redis_image") + temporal_image=$(get_space_override "$space" "temporal_image") + temporal_pg_image=$(get_space_override "$space" "temporal_postgres_image") + email_provider=$(get_space_override "$space" "email_provider") + email_from_name=$(get_space_override "$space" "email_from_name") + email_host=$(get_space_override "$space" "email_host") + email_port=$(get_space_override "$space" "email_port") + email_secure=$(get_space_override "$space" "email_secure") + email_user=$(get_space_override "$space" "email_user") + storage_provider=$(get_space_override "$space" "storage_provider") + upload_dir=$(get_space_override "$space" "upload_directory") + disable_reg=$(get_space_override "$space" "disable_registration") + is_general=$(get_space_override "$space" "is_general") + api_limit=$(get_space_override "$space" "api_limit") + + # Fallback domain defaults + if [[ -z "$fallback_domain" || "$fallback_domain" == "null" ]]; then + fallback_domain="${space}.rsocials.online" + fi + if [[ -z "$email_from" || "$email_from" == "null" ]]; then + email_from="$email_user" + fi + + local fallback_escaped + fallback_escaped=$(escape_dots "$fallback_domain") + + local outfile="${OUTDIR}/docker-compose.space-${space}.yml" + + # Perform template substitution + sed \ + -e "s|{{SPACE_NAME}}|${space}|g" \ + -e "s|{{SPACE_SLUG}}|${space}|g" \ + -e "s|{{PRIMARY_DOMAIN}}|${primary_domain}|g" \ + -e "s|{{FALLBACK_DOMAIN}}|${fallback_domain}|g" \ + -e "s|{{FALLBACK_ESCAPED}}|${fallback_escaped}|g" \ + -e "s|{{POSTIZ_IMAGE}}|${postiz_image}|g" \ + -e "s|{{POSTIZ_PORT}}|${postiz_port}|g" \ + -e "s|{{POSTGRES_IMAGE}}|${postgres_image}|g" \ + -e "s|{{REDIS_IMAGE}}|${redis_image}|g" \ + -e "s|{{TEMPORAL_IMAGE}}|${temporal_image}|g" \ + -e "s|{{TEMPORAL_PG_IMAGE}}|${temporal_pg_image}|g" \ + -e "s|{{EMAIL_PROVIDER}}|${email_provider}|g" \ + -e "s|{{EMAIL_FROM_NAME}}|${email_from_name}|g" \ + -e "s|{{EMAIL_FROM}}|${email_from}|g" \ + -e "s|{{EMAIL_HOST}}|${email_host}|g" \ + -e "s|{{EMAIL_PORT}}|${email_port}|g" \ + -e "s|{{EMAIL_SECURE}}|${email_secure}|g" \ + -e "s|{{EMAIL_USER}}|${email_user}|g" \ + -e "s|{{STORAGE_PROVIDER}}|${storage_provider}|g" \ + -e "s|{{UPLOAD_DIR}}|${upload_dir}|g" \ + -e "s|{{DISABLE_REG}}|${disable_reg}|g" \ + -e "s|{{IS_GENERAL}}|${is_general}|g" \ + -e "s|{{API_LIMIT}}|${api_limit}|g" \ + "$TEMPLATE" > "$outfile" + + echo -e "${GREEN} ✓ ${outfile}${NC}" + + # Collect domains for tunnel/dns generation + ALL_PRIMARY_DOMAINS+=("$primary_domain") + ALL_FALLBACK_DOMAINS+=("$fallback_domain") + ALL_SPACE_NAMES+=("$space") +} + +# --------------------------------------------------------------------------- +# Generate Cloudflare tunnel hostname entries +# --------------------------------------------------------------------------- +generate_tunnel_config() { + local tunnel_cname + tunnel_cname=$(yq '.cloudflare.tunnel_cname' "$CONFIG" 2>/dev/null) + + local tunnel_file="${OUTDIR}/tunnel-hostnames.yml" + cat > "$tunnel_file" <<'HEADER' +# Cloudflare Tunnel Hostname Entries +# Add these to /root/cloudflared/config.yml on Netcup +# Then restart: docker restart cloudflared +HEADER + + for i in "${!ALL_PRIMARY_DOMAINS[@]}"; do + cat >> "$tunnel_file" < "$dns_file" <
> "$dns_file" < tunnel +# (Use Cloudflare dashboard or API - zone ID needed per domain) + +echo "Adding DNS for ${fallback}..." +# Add CNAME: ${fallback_name}.${fallback_zone} -> tunnel +# (Use Cloudflare dashboard or API - zone ID needed per domain) + +EOF + done + + chmod +x "$dns_file" + echo -e "${GREEN} ✓ ${dns_file}${NC}" +} + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- +main() { + echo -e "${CYAN}╔═══════════════════════════════════════════╗${NC}" + echo -e "${CYAN}║ rSpace Config Generator ║${NC}" + echo -e "${CYAN}╚═══════════════════════════════════════════╝${NC}" + echo + + mkdir -p "$OUTDIR" + + # Collect all domains across spaces + declare -a ALL_PRIMARY_DOMAINS=() + declare -a ALL_FALLBACK_DOMAINS=() + declare -a ALL_SPACE_NAMES=() + + local filter_space="${1:-}" + + # Get list of spaces + local spaces + spaces=$(yq '.spaces | keys | .[]' "$CONFIG" 2>/dev/null) + + if [[ -z "$spaces" ]]; then + echo -e "${RED}No spaces defined in spaces.yml${NC}" + exit 1 + fi + + echo -e "${YELLOW}Generating compose files...${NC}" + + for space in $spaces; do + if [[ -n "$filter_space" && "$space" != "$filter_space" ]]; then + continue + fi + echo -e " ${CYAN}Space: ${space}${NC}" + generate_space "$space" + done + + echo + echo -e "${YELLOW}Generating tunnel & DNS config...${NC}" + generate_tunnel_config + + echo + echo -e "${GREEN}═══════════════════════════════════════════${NC}" + echo -e "${GREEN}Done! Generated files in: ${OUTDIR}/${NC}" + echo -e "${GREEN}═══════════════════════════════════════════${NC}" + echo + echo -e "Next steps:" + echo -e " 1. Review generated files in ${OUTDIR}/" + echo -e " 2. Copy compose files to server or use directly" + echo -e " 3. Add secrets (.env or Infisical) for each space" + echo -e " 4. Deploy: ${CYAN}docker compose -f generated/docker-compose.space-.yml up -d${NC}" + echo -e " 5. Update Cloudflare tunnel config (see ${OUTDIR}/tunnel-hostnames.yml)" + echo -e " 6. Add DNS CNAMEs (see ${OUTDIR}/dns-commands.sh)" +} + +main "$@" diff --git a/infisical/.env.example b/infisical/.env.example new file mode 100644 index 0000000..f335565 --- /dev/null +++ b/infisical/.env.example @@ -0,0 +1,14 @@ +# Infisical Secret Management - Environment Variables +# Copy to .env and fill in values before deploying + +# Database password (generate a strong random password) +INFISICAL_DB_PASS=change-me-strong-password + +# Encryption key for secrets at rest (generate with: openssl rand -hex 16) +INFISICAL_ENCRYPTION_KEY=change-me-generate-with-openssl-rand-hex-16 + +# Auth secret for JWT tokens (generate with: openssl rand -base64 32) +INFISICAL_AUTH_SECRET=change-me-generate-with-openssl-rand-base64-32 + +# SMTP password for email notifications (noreply@rmail.online) +SMTP_PASSWORD=your-mailcow-noreply-password diff --git a/infisical/docker-compose.yml b/infisical/docker-compose.yml new file mode 100644 index 0000000..8024e9e --- /dev/null +++ b/infisical/docker-compose.yml @@ -0,0 +1,105 @@ +# ============================================================================= +# Infisical - Secret Management Platform +# ============================================================================= +# Deploy on Netcup RS 8000: /opt/infisical/ +# +# Setup: +# 1. Copy this file to /opt/infisical/ on Netcup +# 2. Create .env with required secrets (see .env.example) +# 3. docker compose up -d +# 4. Add DNS CNAME: secrets.jeffemmett.com -> tunnel +# 5. Add tunnel hostname in /root/cloudflared/config.yml +# 6. Visit https://secrets.jeffemmett.com to complete setup +# + +services: + infisical: + image: infisical/infisical:latest + container_name: infisical + restart: always + environment: + - ENCRYPTION_KEY=${INFISICAL_ENCRYPTION_KEY} + - AUTH_SECRET=${INFISICAL_AUTH_SECRET} + - DB_CONNECTION_URI=postgresql://infisical:${INFISICAL_DB_PASS}@infisical-db:5432/infisical + - REDIS_URL=redis://infisical-redis:6379 + - SITE_URL=https://secrets.jeffemmett.com + - SMTP_HOST=mailcowdockerized-postfix-mailcow-1 + - SMTP_PORT=587 + - SMTP_FROM_ADDRESS=noreply@rmail.online + - SMTP_FROM_NAME=rSpace Secrets + - SMTP_USERNAME=noreply@rmail.online + - SMTP_PASSWORD=${SMTP_PASSWORD} + - NODE_TLS_REJECT_UNAUTHORIZED=0 + labels: + - "traefik.enable=true" + - "traefik.http.routers.infisical.rule=Host(`secrets.jeffemmett.com`)" + - "traefik.http.routers.infisical.entrypoints=web" + - "traefik.http.services.infisical.loadbalancer.server.port=8080" + - "traefik.docker.network=traefik-public" + networks: + - traefik-public + - infisical-internal + - mailcow-network + depends_on: + infisical-db: + condition: service_healthy + infisical-redis: + condition: service_healthy + + infisical-db: + image: postgres:16-alpine + container_name: infisical-db + restart: always + environment: + POSTGRES_PASSWORD: ${INFISICAL_DB_PASS} + POSTGRES_USER: infisical + POSTGRES_DB: infisical + volumes: + - infisical-pgdata:/var/lib/postgresql/data + networks: + - infisical-internal + healthcheck: + test: pg_isready -U infisical -d infisical + interval: 10s + timeout: 3s + retries: 3 + cap_drop: + - ALL + cap_add: + - DAC_OVERRIDE + - FOWNER + - SETGID + - SETUID + security_opt: + - no-new-privileges:true + + infisical-redis: + image: redis:7.2-alpine + container_name: infisical-redis + restart: always + healthcheck: + test: redis-cli ping + interval: 10s + timeout: 3s + retries: 3 + networks: + - infisical-internal + cap_drop: + - ALL + cap_add: + - SETGID + - SETUID + security_opt: + - no-new-privileges:true + +volumes: + infisical-pgdata: + +networks: + traefik-public: + external: true + infisical-internal: + internal: true + mailcow-network: + external: true + name: mailcowdockerized_mailcow-network diff --git a/spaces.yml b/spaces.yml new file mode 100644 index 0000000..c45f4c8 --- /dev/null +++ b/spaces.yml @@ -0,0 +1,69 @@ +# ============================================================================= +# rSpace Community Spaces - Master Configuration +# ============================================================================= +# +# This file defines all community spaces (Postiz instances) and their routing. +# Adding a new space = adding a block here + running ./generate.sh +# +# Usage: +# 1. Add a new space block under `spaces:` +# 2. Run: ./generate.sh +# 3. Add secrets to Infisical (or .env) for the new space +# 4. Deploy: docker compose -f generated/docker-compose.space-.yml up -d +# 5. DNS: The script outputs the Cloudflare CNAME commands to run +# + +# Default settings inherited by all spaces (can be overridden per-space) +defaults: + postiz: + image: ghcr.io/gitroomhq/postiz-app:latest + port: 5000 + postgres_image: postgres:17-alpine + redis_image: redis:7.2 + temporal_image: temporalio/auto-setup:1.28.1 + temporal_postgres_image: postgres:16 + email_provider: nodemailer + email_from_name: rSocials + email_host: mailcowdockerized-postfix-mailcow-1 + email_port: 587 + email_secure: false + email_user: noreply@rmail.online + storage_provider: local + upload_directory: /uploads + disable_registration: false + is_general: true + api_limit: 30 + +# Cloudflare tunnel target for DNS CNAME records +cloudflare: + tunnel_id: a838e9dc-0af5-4212-8af2-6864eb15e1b5 + tunnel_cname: a838e9dc-0af5-4212-8af2-6864eb15e1b5.cfargotunnel.com + +# ============================================================================= +# Community Spaces +# ============================================================================= +spaces: + crypto-commons: + # The primary domain users visit + primary_domain: socials.crypto-commons.org + # Fallback domain on rsocials.online (redirects to primary) + fallback_domain: socials.rsocials.online + # Email sender for this space + email_from: noreply@rmail.online + # Services to deploy for this space + services: + - postiz + + # ----------------------------------------------------------------------- + # Example: Adding a new space + # ----------------------------------------------------------------------- + # mycofi: + # primary_domain: socials.mycofi.earth + # fallback_domain: mycofi.rsocials.online + # email_from: noreply@mycofi.earth + # # Override defaults if needed: + # # postiz: + # # disable_registration: true + # # email_from_name: MycoFi Socials + # services: + # - postiz