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 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-02-24 09:21:18 -08:00
parent 2bff030a92
commit 0d265ddf03
3 changed files with 153 additions and 86 deletions

View File

@ -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:

View File

@ -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" <<EOF
# Space: ${ALL_SPACE_NAMES[$i]}
echo "Adding DNS for ${domain}..."
# Add CNAME: ${primary_name}.${primary_zone} -> 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

View File

@ -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-<name>.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-<slug>).
# 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: <create in Infisical first>
# postiz:
# disable_registration: true
# email_from_name: MycoFi Socials
# services:
# - postiz