feat: add centralized spaces config + Infisical secret management

Single spaces.yml defines all community Postiz instances. generate.sh
reads it and produces per-space docker-compose files with correct
Traefik labels, redirect middleware, and networking. Infisical
deployment config added for secrets.jeffemmett.com.

Adding a new space is now a single block in spaces.yml + ./generate.sh.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-02-23 17:35:15 -08:00
parent f767975986
commit ced6b1f49f
7 changed files with 883 additions and 0 deletions

4
.gitignore vendored
View File

@ -32,6 +32,10 @@ yarn-error.log*
.env
.env*.local
postiz/.env
infisical/.env
# generated compose files (rebuild with ./generate.sh)
/generated/
# vercel
.vercel

193
DEPLOY.md Normal file
View File

@ -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 <space-project-id> --env prod
```
### 4. Deploy
```bash
# Simple deploy
cd generated/
docker compose -f docker-compose.space-mycofi.yml up -d
# With Infisical
infisical run --projectId <shared-id> --env prod -- \
infisical run --projectId <space-id> --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
INFISICAL_DB_PASS=$(openssl rand -hex 16)
INFISICAL_ENCRYPTION_KEY=$(openssl rand -hex 16)
INFISICAL_AUTH_SECRET=$(openssl rand -base64 32)
SMTP_PASSWORD=<noreply@rmail.online password>
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-<name> -> 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.<name>.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 |

201
docker-compose.template.yml Normal file
View File

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

297
generate.sh Executable file
View File

@ -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-<name>.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" <<EOF
# Space: ${ALL_SPACE_NAMES[$i]}
- hostname: ${ALL_PRIMARY_DOMAINS[$i]}
service: http://localhost:80
- hostname: ${ALL_FALLBACK_DOMAINS[$i]}
service: http://localhost:80
EOF
done
echo -e "${GREEN}${tunnel_file}${NC}"
# DNS commands
local dns_file="${OUTDIR}/dns-commands.sh"
cat > "$dns_file" <<HEADER
#!/usr/bin/env bash
# Cloudflare DNS CNAME commands for all spaces
# Run these from Netcup (or anywhere with CF API access)
# Requires: \$CLOUDFLARE_ZONE_TOKEN set in environment
#
# Tunnel CNAME target: ${tunnel_cname}
HEADER
for i in "${!ALL_PRIMARY_DOMAINS[@]}"; do
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
# (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-<name>.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 "$@"

14
infisical/.env.example Normal file
View File

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

View File

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

69
spaces.yml Normal file
View File

@ -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-<name>.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