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:
parent
f767975986
commit
ced6b1f49f
|
|
@ -32,6 +32,10 @@ yarn-error.log*
|
|||
.env
|
||||
.env*.local
|
||||
postiz/.env
|
||||
infisical/.env
|
||||
|
||||
# generated compose files (rebuild with ./generate.sh)
|
||||
/generated/
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
@ -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
|
||||
|
|
@ -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 "$@"
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
Loading…
Reference in New Issue