From 0d265ddf03ce406037bd76adb9b97d98c7bfc0e2 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Tue, 24 Feb 2026 09:21:18 -0800 Subject: [PATCH] feat: update spaces config to match all deployed Postiz instances - Add all 3 active spaces: crypto-commons (cc), p2pfoundation (p2pf), bondingcurve (bcrg) with correct slugs matching container names - Add Sablier auto-sleep labels for resource conservation - Add Pocket ID OAuth config with per-space client credentials - Use multi-host routing (Host || Host) instead of redirect middleware - Switch to restart: unless-stopped matching server deployments - Generator now handles dynamic blocks (OAuth, Sablier, Traefik labels) Co-Authored-By: Claude Opus 4.6 --- docker-compose.template.yml | 61 ++++--------------- generate.sh | 116 +++++++++++++++++++++++++++--------- spaces.yml | 62 +++++++++++++++---- 3 files changed, 153 insertions(+), 86 deletions(-) diff --git a/docker-compose.template.yml b/docker-compose.template.yml index b1b1e4f..e26c065 100644 --- a/docker-compose.template.yml +++ b/docker-compose.template.yml @@ -3,13 +3,13 @@ # ============================================================================= # 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}}) +# Primary: {{PRIMARY_DOMAIN}} | Fallback: {{FALLBACK_DOMAIN}} services: postiz-{{SPACE_SLUG}}: image: {{POSTIZ_IMAGE}} container_name: postiz-{{SPACE_SLUG}} - restart: always + restart: unless-stopped environment: MAIN_URL: 'https://{{PRIMARY_DOMAIN}}' FRONTEND_URL: 'https://{{PRIMARY_DOMAIN}}' @@ -22,6 +22,9 @@ services: IS_GENERAL: '{{IS_GENERAL}}' DISABLE_REGISTRATION: '{{DISABLE_REG}}' + # Pocket ID OAuth +{{OAUTH_BLOCK}} + # Storage STORAGE_PROVIDER: '{{STORAGE_PROVIDER}}' UPLOAD_DIRECTORY: '{{UPLOAD_DIR}}' @@ -48,11 +51,6 @@ services: 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}}' @@ -76,19 +74,7 @@ services: - 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" +{{TRAEFIK_LABELS}} networks: - traefik-public - postiz-{{SPACE_SLUG}}-internal @@ -102,7 +88,8 @@ services: postiz-{{SPACE_SLUG}}-postgres: image: {{POSTGRES_IMAGE}} container_name: postiz-{{SPACE_SLUG}}-postgres - restart: always + restart: unless-stopped +{{SABLIER_LABELS_DB}} environment: POSTGRES_PASSWORD: '${POSTGRES_PASSWORD}' POSTGRES_USER: postiz @@ -116,20 +103,12 @@ services: 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 + restart: unless-stopped +{{SABLIER_LABELS_REDIS}} healthcheck: test: redis-cli ping interval: 10s @@ -139,18 +118,11 @@ services: - 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 + restart: unless-stopped environment: POSTGRES_PASSWORD: temporal POSTGRES_USER: temporal @@ -158,20 +130,11 @@ services: - 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 + restart: unless-stopped depends_on: - postiz-{{SPACE_SLUG}}-temporal-postgres environment: diff --git a/generate.sh b/generate.sh index f4972c0..de8bd30 100755 --- a/generate.sh +++ b/generate.sh @@ -73,33 +73,35 @@ get_space_override() { 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 + local primary_domain fallback_domain email_from slug primary_domain=$(get_space "$space" "primary_domain") fallback_domain=$(get_space "$space" "fallback_domain") email_from=$(get_space "$space" "email_from") + slug=$(get_space "$space" "slug") if [[ -z "$primary_domain" || "$primary_domain" == "null" ]]; then echo -e "${RED} Error: spaces.${space}.primary_domain is required${NC}" return 1 fi + # Slug defaults to space key name + if [[ -z "$slug" || "$slug" == "null" ]]; then + slug="$space" + 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 + local use_sablier use_oauth + local oauth_url oauth_auth_url oauth_token_url oauth_userinfo_url + local oauth_display_name oauth_logo_url postiz_image=$(get_space_override "$space" "image") postiz_port=$(get_space_override "$space" "port") @@ -118,6 +120,14 @@ generate_space() { disable_reg=$(get_space_override "$space" "disable_registration") is_general=$(get_space_override "$space" "is_general") api_limit=$(get_space_override "$space" "api_limit") + use_sablier=$(get_space_override "$space" "sablier") + use_oauth=$(get_space_override "$space" "oauth") + oauth_url=$(get_space_override "$space" "oauth_url") + oauth_auth_url=$(get_space_override "$space" "oauth_auth_url") + oauth_token_url=$(get_space_override "$space" "oauth_token_url") + oauth_userinfo_url=$(get_space_override "$space" "oauth_userinfo_url") + oauth_display_name=$(get_space_override "$space" "oauth_display_name") + oauth_logo_url=$(get_space_override "$space" "oauth_logo_url") # Fallback domain defaults if [[ -z "$fallback_domain" || "$fallback_domain" == "null" ]]; then @@ -127,18 +137,60 @@ generate_space() { email_from="$email_user" fi - local fallback_escaped - fallback_escaped=$(escape_dots "$fallback_domain") + # ----------------------------------------------------------------------- + # Build dynamic blocks + # ----------------------------------------------------------------------- + + # OAuth block + local oauth_block + if [[ "$use_oauth" == "true" ]]; then + oauth_block=" POSTIZ_GENERIC_OAUTH: 'true' + NEXT_PUBLIC_POSTIZ_OAUTH_DISPLAY_NAME: '${oauth_display_name}' + NEXT_PUBLIC_POSTIZ_OAUTH_LOGO_URL: '${oauth_logo_url}' + POSTIZ_OAUTH_URL: '${oauth_url}' + POSTIZ_OAUTH_AUTH_URL: '${oauth_auth_url}' + POSTIZ_OAUTH_TOKEN_URL: '${oauth_token_url}' + POSTIZ_OAUTH_USERINFO_URL: '${oauth_userinfo_url}' + POSTIZ_OAUTH_CLIENT_ID: '\${POSTIZ_OAUTH_CLIENT_ID}' + POSTIZ_OAUTH_CLIENT_SECRET: '\${POSTIZ_OAUTH_CLIENT_SECRET}' + POSTIZ_OAUTH_SCOPE: 'openid profile email'" + else + oauth_block=" # OAuth disabled for this space" + fi + + # Traefik + Sablier labels + local traefik_labels sablier_labels_db sablier_labels_redis + if [[ "$use_sablier" == "true" ]]; then + traefik_labels=" - \"traefik.enable=false\" + - \"sablier.enable=true\" + - \"sablier.group=postiz-${slug}\" + - \"traefik.http.routers.postiz-${slug}.rule=Host(\`${primary_domain}\`) || Host(\`${fallback_domain}\`)\" + - \"traefik.http.routers.postiz-${slug}.entrypoints=web,websecure\" + - \"traefik.http.services.postiz-${slug}.loadbalancer.server.port=${postiz_port}\"" + sablier_labels_db=" labels: + - \"sablier.enable=true\" + - \"sablier.group=postiz-${slug}\"" + sablier_labels_redis=" labels: + - \"sablier.enable=true\" + - \"sablier.group=postiz-${slug}\"" + else + traefik_labels=" - \"traefik.enable=true\" + - \"traefik.http.routers.postiz-${slug}.rule=Host(\`${primary_domain}\`) || Host(\`${fallback_domain}\`)\" + - \"traefik.http.routers.postiz-${slug}.entrypoints=web,websecure\" + - \"traefik.http.services.postiz-${slug}.loadbalancer.server.port=${postiz_port}\" + - \"traefik.docker.network=traefik-public\"" + sablier_labels_db="" + sablier_labels_redis="" + fi local outfile="${OUTDIR}/docker-compose.space-${space}.yml" - # Perform template substitution + # Perform template substitution using sed sed \ -e "s|{{SPACE_NAME}}|${space}|g" \ - -e "s|{{SPACE_SLUG}}|${space}|g" \ + -e "s|{{SPACE_SLUG}}|${slug}|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" \ @@ -159,6 +211,28 @@ generate_space() { -e "s|{{API_LIMIT}}|${api_limit}|g" \ "$TEMPLATE" > "$outfile" + # Replace multi-line blocks (sed can't do these well, use temp files) + # OAuth block + local tmpfile + tmpfile=$(mktemp) + awk -v block="$oauth_block" '{gsub(/{{OAUTH_BLOCK}}/, block); print}' "$outfile" > "$tmpfile" + mv "$tmpfile" "$outfile" + + # Traefik labels block + tmpfile=$(mktemp) + awk -v block="$traefik_labels" '{gsub(/{{TRAEFIK_LABELS}}/, block); print}' "$outfile" > "$tmpfile" + mv "$tmpfile" "$outfile" + + # Sablier labels for DB + tmpfile=$(mktemp) + awk -v block="$sablier_labels_db" '{gsub(/{{SABLIER_LABELS_DB}}/, block); print}' "$outfile" > "$tmpfile" + mv "$tmpfile" "$outfile" + + # Sablier labels for Redis + tmpfile=$(mktemp) + awk -v block="$sablier_labels_redis" '{gsub(/{{SABLIER_LABELS_REDIS}}/, block); print}' "$outfile" > "$tmpfile" + mv "$tmpfile" "$outfile" + echo -e "${GREEN} ✓ ${outfile}${NC}" # Collect domains for tunnel/dns generation @@ -210,26 +284,14 @@ HEADER local domain="${ALL_PRIMARY_DOMAINS[$i]}" local fallback="${ALL_FALLBACK_DOMAINS[$i]}" - # Extract zone (last two parts of domain) - local primary_zone - primary_zone=$(echo "$domain" | rev | cut -d. -f1-2 | rev) - local fallback_zone - fallback_zone=$(echo "$fallback" | rev | cut -d. -f1-2 | rev) - - # Extract record name (everything before the zone) - local primary_name - primary_name=$(echo "$domain" | sed "s/\\.${primary_zone}$//") - local fallback_name - fallback_name=$(echo "$fallback" | sed "s/\\.${fallback_zone}$//") - cat >> "$dns_file" < tunnel +# Add CNAME: ${domain} -> ${tunnel_cname} # (Use Cloudflare dashboard or API - zone ID needed per domain) echo "Adding DNS for ${fallback}..." -# Add CNAME: ${fallback_name}.${fallback_zone} -> tunnel +# Add CNAME: ${fallback} -> ${tunnel_cname} # (Use Cloudflare dashboard or API - zone ID needed per domain) EOF diff --git a/spaces.yml b/spaces.yml index c45f4c8..ca23e43 100644 --- a/spaces.yml +++ b/spaces.yml @@ -8,7 +8,7 @@ # Usage: # 1. Add a new space block under `spaces:` # 2. Run: ./generate.sh -# 3. Add secrets to Infisical (or .env) for the new space +# 3. Add secrets to Infisical 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 # @@ -33,6 +33,16 @@ defaults: disable_registration: false is_general: true api_limit: 30 + # Sablier auto-sleep (saves resources for low-traffic spaces) + sablier: true + # Pocket ID OAuth (enabled by default, per-space client_id/secret in Infisical) + oauth: true + oauth_url: https://auth.jeffemmett.com + oauth_auth_url: https://auth.jeffemmett.com/authorize + oauth_token_url: https://auth.jeffemmett.com/api/oidc/token + oauth_userinfo_url: https://auth.jeffemmett.com/api/oidc/userinfo + oauth_display_name: Pocket ID + oauth_logo_url: https://raw.githubusercontent.com/pocket-id/pocket-id/refs/heads/main/frontend/static/img/static-logo.svg # Cloudflare tunnel target for DNS CNAME records cloudflare: @@ -42,15 +52,46 @@ cloudflare: # ============================================================================= # Community Spaces # ============================================================================= +# Each space gets a short `slug` used for container names (postiz-). +# If omitted, slug defaults to the space key name. +# +# Infisical project for each space stores: JWT_SECRET, POSTGRES_PASSWORD, +# EMAIL_PASS, POSTIZ_OAUTH_CLIENT_ID, POSTIZ_OAUTH_CLIENT_SECRET, and any +# social media API keys. +# ============================================================================= + spaces: crypto-commons: - # The primary domain users visit + slug: cc primary_domain: socials.crypto-commons.org - # Fallback domain on rsocials.online (redirects to primary) - fallback_domain: socials.rsocials.online - # Email sender for this space + fallback_domain: socials.valleyofthecommons.com email_from: noreply@rmail.online - # Services to deploy for this space + infisical_project_id: a76d3d2c-205e-4356-a8ad-2a2c19724d8c + postiz: + disable_registration: true + email_from_name: Crypto Commons + services: + - postiz + + p2pfoundation: + slug: p2pf + primary_domain: p2pf.rsocials.online + fallback_domain: socials.p2pfoundation.net + email_from: noreply@rmail.online + infisical_project_id: ea4b3b47-12d5-40d1-9a80-30bc48cdba7a + postiz: + email_from_name: P2P Foundation Socials + services: + - postiz + + bondingcurve: + slug: bcrg + primary_domain: bondingcurve.rsocials.online + fallback_domain: socials.bondingcurve.tech + email_from: noreply@rmail.online + infisical_project_id: 255cb27d-e8d3-459d-af2c-faff55b46d9f + postiz: + email_from_name: Bonding Curve Research services: - postiz @@ -58,12 +99,13 @@ spaces: # Example: Adding a new space # ----------------------------------------------------------------------- # mycofi: + # slug: mycofi # Short name for containers (postiz-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 + # infisical_project_id: + # postiz: + # disable_registration: true + # email_from_name: MycoFi Socials # services: # - postiz