#!/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 } # --------------------------------------------------------------------------- # Generate compose file for a single space # --------------------------------------------------------------------------- generate_space() { local space="$1" local primary_domain fallback_domain email_from slug infisical_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") infisical_slug=$(get_space "$space" "infisical_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 # Infisical slug defaults to postiz- if [[ -z "$infisical_slug" || "$infisical_slug" == "null" ]]; then infisical_slug="postiz-${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") 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") 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 fallback_domain="${space}.rsocials.online" fi if [[ -z "$email_from" || "$email_from" == "null" ]]; then email_from="$email_user" fi # ----------------------------------------------------------------------- # Build dynamic blocks # ----------------------------------------------------------------------- # OAuth block (config only — client_id/secret injected by Infisical) 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_SCOPE: 'openid profile email' # POSTIZ_OAUTH_CLIENT_ID + CLIENT_SECRET from Infisical" 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 using sed sed \ -e "s|{{SPACE_NAME}}|${space}|g" \ -e "s|{{SPACE_SLUG}}|${slug}|g" \ -e "s|{{PRIMARY_DOMAIN}}|${primary_domain}|g" \ -e "s|{{FALLBACK_DOMAIN}}|${fallback_domain}|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" \ -e "s|{{INFISICAL_SLUG}}|${infisical_slug}|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}" # Generate minimal .env template (only Infisical creds + postgres password) local envfile="${OUTDIR}/env.space-${space}" cat > "$envfile" </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_cname} # (Use Cloudflare dashboard or API - zone ID needed per domain) echo "Adding DNS for ${fallback}..." # Add CNAME: ${fallback} -> ${tunnel_cname} # (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 "$@"