From 13d94b37517d17e5393e86e57a92d1b066dd620f Mon Sep 17 00:00:00 2001 From: ginnoir Date: Tue, 2 Jun 2026 19:19:27 -0500 Subject: [PATCH] chore(deploy): separate maintainer config from public deployment templates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - rename Caddyfile.snippet → Caddyfile (maintainer's full production file) - restore Caddyfile.snippet as a minimal public reference (famapp blocks only) - add compose.example.yaml: standalone famapp + authentik template for new deployers; compose.yaml remains the maintainer's homelab monolith - update deploy/README.md: file table, setup steps referencing example files - apply-compose.ps1: point Caddy push at deploy/Caddyfile (not snippet) --- deploy/Caddyfile.snippet | 224 +----------------------------------- deploy/README.md | 24 +++- deploy/compose.example.yaml | 196 +++++++++++++++++++++++++++++++ 3 files changed, 220 insertions(+), 224 deletions(-) create mode 100644 deploy/compose.example.yaml diff --git a/deploy/Caddyfile.snippet b/deploy/Caddyfile.snippet index 950209f..14efffd 100644 --- a/deploy/Caddyfile.snippet +++ b/deploy/Caddyfile.snippet @@ -1,225 +1,11 @@ -# ============================================================= -# GLOBAL OPTIONS -# ============================================================= -{ - acme_dns cloudflare {env.CF_API_TOKEN} -} +# Add these blocks to your existing Caddyfile. +# Assumes famapp and authentik-server are reachable by Caddy +# (e.g. on a shared Docker network). -# ============================================================= -# SNIPPETS -# ============================================================= -# Reusable matcher — blocks anything not on the LAN. -# Usage: import internal_only inside any site block. -(internal_only) { - @blocked not remote_ip 192.168.1.0/24 - respond @blocked "Access denied" 403 -} - -# ============================================================= -# FOUNDRY VTT — public -# ============================================================= -foundry.ginnoir.com { - reverse_proxy foundry:30000 -} - -foundry2.ginnoir.com { - reverse_proxy foundry2:30000 -} - -# ============================================================= -# TABLETOP TOOLS — public -# ============================================================= -5etools.ginnoir.com { - reverse_proxy 5etools:80 -} - -pf2e.ginnoir.com { - root * /srv/aon - file_server -} - -# ============================================================= -# FILE STORAGE — public -# ============================================================= -files.ginnoir.com { - reverse_proxy owncloud:8080 -} - -# ============================================================= -# STATIC SITES — public -# ============================================================= -ffttsystems.ginnoir.com { - root * /srv/ffttsystems - file_server -} - -# ============================================================= -# MEDIA REQUESTS — public -# ============================================================= -requests.ginnoir.com { - reverse_proxy overseerr:5055 -} - -# ============================================================= -# MEDIA MANAGEMENT — internal only -# ============================================================= -sonarr.ginnoir.com { - import internal_only - reverse_proxy sonarr:8989 -} - -radarr.ginnoir.com { - import internal_only - reverse_proxy radarr:7878 -} - -bazarr.ginnoir.com { - import internal_only - reverse_proxy bazarr:6767 -} - -jackett.ginnoir.com { - import internal_only - reverse_proxy jackett:9117 -} - -prowlarr.ginnoir.com { - import internal_only - reverse_proxy prowlarr:9696 -} - -tautulli.ginnoir.com { - import internal_only - reverse_proxy tautulli:8181 -} - -hydra.ginnoir.com { - import internal_only - reverse_proxy nzbhydra2:5076 -} - -# ============================================================= -# DOWNLOAD CLIENTS — internal only -# ============================================================= -qbittorrent.ginnoir.com { - import internal_only - reverse_proxy qbittorrent:3232 -} - -deluge.ginnoir.com { - import internal_only - reverse_proxy deluge:8112 -} - -nzbget.ginnoir.com { - import internal_only - reverse_proxy nzbget:6789 -} - -whisparr.ginnoir.com { - import internal_only - reverse_proxy whisparr:6969 -} - -stash.ginnoir.com { - import internal_only - reverse_proxy stash:6970 -} - -# ============================================================= -# NOTIFICATIONS & RSS — public -# ============================================================= -ntfy.ginnoir.com, http://ntfy.ginnoir.com { - reverse_proxy ntfy:80 - @httpget { - protocol http - method GET - path_regexp ^/([-_a-z0-9]{0,64}$|docs/|static/) - } - redir @httpget https://{host}{uri} -} - -freshrss.ginnoir.com { - reverse_proxy freshrss:80 - @httpget { - protocol http - method GET - path_regexp ^/([-_a-z0-9]{0,64}$|docs/|static/) - } - redir @httpget https://{host}{uri} -} - -# ============================================================= -# RESUME / PORTFOLIO — public -# ============================================================= -resume.ginnoir.com, https://resume.ginnoir.com { - reverse_proxy app:3000 - @httpget { - protocol http - method GET - path_regexp ^/([-_a-z0-9]{0,64}$|docs/|static/) - } - redir @httpget https://{host}{uri} -} - -j-costa.com, https://j-costa.com { - tls { - issuer acme { - disable_tlsalpn_challenge - } - } - redir * https://resume.ginnoir.com/ginnoir/resume permanent -} - -storage.j-costa.com, https://storage.j-costa.com { - tls { - issuer acme { - disable_tlsalpn_challenge - } - } - reverse_proxy resume-minio:9000 - @httpget { - protocol http - method GET - path_regexp ^/([-_a-z0-9]{0,64}$|docs/|static/) - } - redir @httpget https://{host}{uri} -} - -# ============================================================= -# MINIO CONSOLE — internal only -# ============================================================= -minio.ginnoir.com { - import internal_only - reverse_proxy resume-minio:9001 -} - -# ============================================================= -# MATRIX — public -# ============================================================= -ginnoir.com { - header /.well-known/matrix/* Content-Type application/json - header /.well-known/matrix/* Access-Control-Allow-Origin * - respond /.well-known/matrix/server `{"m.server": "matrix.ginnoir.com:443"}` - respond /.well-known/matrix/client `{"m.homeserver":{"base_url":"https://matrix.ginnoir.com"},"m.identity_server":{"base_url":"https://identity.ginnoir.com"}}` -} - -matrix.ginnoir.com { - reverse_proxy /_matrix/* localhost:8008 - reverse_proxy /_synapse/client/* localhost:8008 -} - -# ============================================================= -# FAMAPP & AUTH — public -# ============================================================= -fam.ginnoir.com { +fam.yourdomain.com { reverse_proxy famapp:3000 } -auth.ginnoir.com { +auth.yourdomain.com { reverse_proxy authentik-server:9000 } - -dev.ginnoir.com { - reverse_proxy 192.168.1.74:3000 -} diff --git a/deploy/README.md b/deploy/README.md index 24308bf..3dddf97 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -2,16 +2,30 @@ Trunk-based: `main` is always green. Production deploys only from version tags (`vX.Y.Z`). The dev-login flow is retained for local development behind a double gate (`NODE_ENV !== "production"` **and** `ENABLE_DEV_LOGIN=true`); a startup assertion in `src/lib/dev-login-config.ts` makes a misconfigured prod fail loud instead of silently exposing it. +## Files in this directory + +| File | Purpose | +| ----------------------- | -------------------------------------------------------------------------- | +| `compose.example.yaml` | **Start here.** Standalone famapp + Authentik stack for new deployments. | +| `compose.yaml` | Maintainer's production compose (full homelab monolith — not a template). | +| `Caddyfile.snippet` | Reverse proxy blocks to add to your Caddyfile. | +| `Caddyfile.dev.snippet` | Dev machine proxy block (maintainer-specific). | +| `Caddyfile` | Maintainer's full production Caddyfile (not a template). | +| `authentik/README.md` | Authentik bootstrap guide. | +| `backups/` | Backup container scripts (used by `famapp-backup` in the example compose). | + ## One-time host setup 1. Install Docker + Compose plugin on the host. 2. `git clone` this repo to e.g. `/srv/famapp`. -3. Copy `.env.production.example` → `/srv/famapp/deploy/.env` and fill in real values. +3. Copy `deploy/compose.example.yaml` → `/srv/famapp/deploy/compose.yaml`. +4. Copy `.env.production.example` → `/srv/famapp/deploy/.env` and fill in real values. - `openssl rand -base64 32` for `AUTH_SECRET`. - `openssl rand -base64 60` for `AUTHENTIK_SECRET_KEY`. - - `pnpm vapid:generate` (locally) for the three VAPID lines. -4. Bootstrap Authentik per `deploy/authentik/README.md`. Save the OIDC client id/secret into `.env`. -5. Wire Caddy with `deploy/Caddyfile.snippet`. + - `openssl rand -hex 64` for the MinIO passwords. + - `pnpm vapid:generate` (locally, from the repo) for the three VAPID lines. +5. Bootstrap Authentik per `deploy/authentik/README.md`. Save the OIDC client id/secret into `.env`. +6. Wire Caddy (or any reverse proxy) using `deploy/Caddyfile.snippet`. ## Cutting a release @@ -28,7 +42,7 @@ git push origin v0.1.0 ```bash cd /srv/famapp/deploy # pin to the tag you just cut -echo 'FAMAPP_IMAGE=ghcr.io/ginnoir/famapp:v0.1.0' >> .env # or edit in place +sed -i 's|FAMAPP_IMAGE=.*|FAMAPP_IMAGE=ghcr.io/ginnoir/famapp:v0.1.0|' .env docker compose pull famapp docker compose up -d famapp docker compose logs -f famapp # watch migrations + boot diff --git a/deploy/compose.example.yaml b/deploy/compose.example.yaml new file mode 100644 index 0000000..6992b5e --- /dev/null +++ b/deploy/compose.example.yaml @@ -0,0 +1,196 @@ +# Standalone famapp deployment template. +# Copy this to compose.yaml (or reference with -f), fill in .env from +# .env.production.example, then: docker compose up -d +# +# Caddy (or any reverse proxy) should sit in front — see Caddyfile.snippet. +# If Caddy runs in a separate compose stack, add famapp to that stack's +# external network instead of exposing ports directly. + +name: famapp + +networks: + famapp_net: + +volumes: + famapp_db_data: + authentik_db_data: + authentik_redis_data: + garden_uploads: + backups: + +services: + famapp: + image: ${FAMAPP_IMAGE:-ghcr.io/ginnoir/famapp:latest} + pull_policy: ${FAMAPP_PULL_POLICY:-always} + restart: unless-stopped + environment: + NODE_ENV: production + AUTH_URL: ${AUTH_URL} + DATABASE_URL: postgres://${FAMAPP_DB_USER}:${FAMAPP_DB_PASSWORD}@famapp-db:5432/${FAMAPP_DB_NAME} + AUTH_SECRET: ${AUTH_SECRET} + AUTH_OIDC_ISSUER: ${AUTH_OIDC_ISSUER} + AUTH_OIDC_CLIENT_ID: ${AUTH_OIDC_CLIENT_ID} + AUTH_OIDC_CLIENT_SECRET: ${AUTH_OIDC_CLIENT_SECRET} + VAPID_PUBLIC_KEY: ${VAPID_PUBLIC_KEY} + VAPID_PRIVATE_KEY: ${VAPID_PRIVATE_KEY} + VAPID_SUBJECT: ${VAPID_SUBJECT} + NTFY_URL: ${NTFY_URL:-} + NTFY_TOPIC: ${NTFY_TOPIC:-} + LOG_LEVEL: ${LOG_LEVEL:-info} + RUN_MIGRATIONS: ${RUN_MIGRATIONS:-true} + MINIO_ENDPOINT: http://famapp-minio:9000 + MINIO_ROOT_USER: ${famapp_MINIO_ROOT_USER} + MINIO_ROOT_PASSWORD: ${famapp_MINIO_ROOT_PASSWORD} + MINIO_BUCKET: ${famapp_MINIO_BUCKET:-garden} + OPENPLANTBOOK_CLIENT_ID: ${famapp_OPENPLANTBOOK_CLIENT_ID:-} + OPENPLANTBOOK_CLIENT_SECRET: ${famapp_OPENPLANTBOOK_CLIENT_SECRET:-} + ports: + - "${FAMAPP_PORT:-3000}:3000" + depends_on: + famapp-db: + condition: service_healthy + famapp-minio: + condition: service_healthy + networks: + - famapp_net + + famapp-db: + image: postgres:16-alpine + restart: unless-stopped + environment: + POSTGRES_USER: ${FAMAPP_DB_USER} + POSTGRES_PASSWORD: ${FAMAPP_DB_PASSWORD} + POSTGRES_DB: ${FAMAPP_DB_NAME} + volumes: + - famapp_db_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${FAMAPP_DB_USER} -d ${FAMAPP_DB_NAME}"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - famapp_net + + famapp-minio: + image: minio/minio:latest + command: server /data --console-address ":9001" + restart: unless-stopped + environment: + MINIO_ROOT_USER: ${famapp_MINIO_ROOT_USER} + MINIO_ROOT_PASSWORD: ${famapp_MINIO_ROOT_PASSWORD} + volumes: + - garden_uploads:/data + ports: + - "9000:9000" + - "9001:9001" + healthcheck: + test: ["CMD-SHELL", "curl -sf http://localhost:9000/minio/health/live || exit 1"] + interval: 30s + timeout: 20s + retries: 3 + start_period: 30s + networks: + - famapp_net + + famapp-backup: + image: alpine:3.20 + restart: unless-stopped + environment: + FAMAPP_DB_HOST: famapp-db + FAMAPP_DB_PORT: "5432" + FAMAPP_DB_USER: ${FAMAPP_DB_USER} + FAMAPP_DB_PASSWORD: ${FAMAPP_DB_PASSWORD} + FAMAPP_DB_NAME: ${FAMAPP_DB_NAME} + AUTHENTIK_DB_HOST: authentik-db + AUTHENTIK_DB_PORT: "5432" + AUTHENTIK_DB_USER: ${AUTHENTIK_DB_USER:-authentik} + AUTHENTIK_DB_PASSWORD: ${AUTHENTIK_DB_PASSWORD} + AUTHENTIK_DB_NAME: ${AUTHENTIK_DB_NAME:-authentik} + volumes: + - backups:/backups + - ./backups:/scripts:ro + depends_on: + famapp-db: + condition: service_healthy + authentik-db: + condition: service_healthy + networks: + - famapp_net + entrypoint: ["sh", "/scripts/entrypoint.sh"] + + authentik-server: + image: ghcr.io/goauthentik/server:${AUTHENTIK_IMAGE_TAG:-2024.12.3} + restart: unless-stopped + command: server + environment: + AUTHENTIK_REDIS__HOST: authentik-redis + AUTHENTIK_POSTGRESQL__HOST: authentik-db + AUTHENTIK_POSTGRESQL__USER: ${AUTHENTIK_DB_USER:-authentik} + AUTHENTIK_POSTGRESQL__PASSWORD: ${AUTHENTIK_DB_PASSWORD} + AUTHENTIK_POSTGRESQL__NAME: ${AUTHENTIK_DB_NAME:-authentik} + AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY} + AUTHENTIK_ERROR_REPORTING__ENABLED: "false" + ports: + - "9200:9000" + depends_on: + authentik-db: + condition: service_healthy + authentik-redis: + condition: service_healthy + networks: + - famapp_net + + authentik-worker: + image: ghcr.io/goauthentik/server:${AUTHENTIK_IMAGE_TAG:-2024.12.3} + restart: unless-stopped + command: worker + environment: + AUTHENTIK_REDIS__HOST: authentik-redis + AUTHENTIK_POSTGRESQL__HOST: authentik-db + AUTHENTIK_POSTGRESQL__USER: ${AUTHENTIK_DB_USER:-authentik} + AUTHENTIK_POSTGRESQL__PASSWORD: ${AUTHENTIK_DB_PASSWORD} + AUTHENTIK_POSTGRESQL__NAME: ${AUTHENTIK_DB_NAME:-authentik} + AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY} + AUTHENTIK_ERROR_REPORTING__ENABLED: "false" + depends_on: + authentik-db: + condition: service_healthy + authentik-redis: + condition: service_healthy + networks: + - famapp_net + + authentik-db: + image: postgres:16-alpine + restart: unless-stopped + environment: + POSTGRES_USER: ${AUTHENTIK_DB_USER:-authentik} + POSTGRES_PASSWORD: ${AUTHENTIK_DB_PASSWORD} + POSTGRES_DB: ${AUTHENTIK_DB_NAME:-authentik} + volumes: + - authentik_db_data:/var/lib/postgresql/data + healthcheck: + test: + [ + "CMD-SHELL", + "pg_isready -U ${AUTHENTIK_DB_USER:-authentik} -d ${AUTHENTIK_DB_NAME:-authentik}", + ] + interval: 10s + timeout: 5s + retries: 5 + networks: + - famapp_net + + authentik-redis: + image: redis:7-alpine + restart: unless-stopped + command: --save 60 1 --loglevel warning + volumes: + - authentik_redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - famapp_net