From 57cd6ed2706fabe79e9ca35e36dc5cc3e9d1d3e8 Mon Sep 17 00:00:00 2001 From: ginnoir Date: Thu, 4 Jun 2026 16:11:31 -0500 Subject: [PATCH] refactor: split monolith into per-domain stacks (Phase A: build) Add stacks// compose + env for the 11 target stacks (proxy, media, foundry, owncloud, resume, famapp, authentik, notify, monitoring, remote, dev). Each app stack joins a shared external `edge` network for Caddy and keeps its DB/cache co-located (no shared backing services). All named volumes convert to tiered bind mounts: DBs/configs -> /config (SSD), blobs/repos/registry -> /storage1/labdata (ZFS). Gitea repos+LFS split to ZFS. Move Vault into the management plane (portainer-compose.yml) and add the shared `edge` network there. apply-compose.ps1 -Portainer now also pushes vault.hcl. Additive only: root docker-compose.yml/.env/Caddyfile untouched, so the live monolith is unchanged. Live cutover (Phase B) is next. --- apply-compose.ps1 | 7 +- portainer-compose.yml | 56 ++++- stacks/authentik/.env | 6 + stacks/authentik/docker-compose.yml | 92 ++++++++ stacks/dev/.env | 19 ++ stacks/dev/docker-compose.yml | 326 +++++++++++++++++++++++++++ stacks/famapp/.env | 33 +++ stacks/famapp/docker-compose.yml | 90 ++++++++ stacks/foundry/.env | 5 + stacks/foundry/docker-compose.yml | 69 ++++++ stacks/media/.env | 5 + stacks/media/docker-compose.yml | 206 +++++++++++++++++ stacks/monitoring/.env | 3 + stacks/monitoring/docker-compose.yml | 35 +++ stacks/notify/.env | 5 + stacks/notify/docker-compose.yml | 82 +++++++ stacks/owncloud/.env | 7 + stacks/owncloud/docker-compose.yml | 81 +++++++ stacks/proxy/.env | 4 + stacks/proxy/docker-compose.yml | 37 +++ stacks/remote/docker-compose.yml | 28 +++ stacks/resume/.env | 9 + stacks/resume/docker-compose.yml | 99 ++++++++ 23 files changed, 1296 insertions(+), 8 deletions(-) create mode 100644 stacks/authentik/.env create mode 100644 stacks/authentik/docker-compose.yml create mode 100644 stacks/dev/.env create mode 100644 stacks/dev/docker-compose.yml create mode 100644 stacks/famapp/.env create mode 100644 stacks/famapp/docker-compose.yml create mode 100644 stacks/foundry/.env create mode 100644 stacks/foundry/docker-compose.yml create mode 100644 stacks/media/.env create mode 100644 stacks/media/docker-compose.yml create mode 100644 stacks/monitoring/.env create mode 100644 stacks/monitoring/docker-compose.yml create mode 100644 stacks/notify/.env create mode 100644 stacks/notify/docker-compose.yml create mode 100644 stacks/owncloud/.env create mode 100644 stacks/owncloud/docker-compose.yml create mode 100644 stacks/proxy/.env create mode 100644 stacks/proxy/docker-compose.yml create mode 100644 stacks/remote/docker-compose.yml create mode 100644 stacks/resume/.env create mode 100644 stacks/resume/docker-compose.yml diff --git a/apply-compose.ps1 b/apply-compose.ps1 index 515a8ab..d1e433d 100644 --- a/apply-compose.ps1 +++ b/apply-compose.ps1 @@ -64,8 +64,11 @@ if ($Compose) { if ($Portainer) { Write-Host "Pushing portainer-compose.yml ..." scp $portainerLocal "${server}:~/valhalla-lab/portainer-compose.yml" - Write-Host "Applying portainer compose ..." - ssh $server "docker compose -f ~/valhalla-lab/portainer-compose.yml up -d" + Write-Host "Pushing vault.hcl (Vault now lives in the management plane) ..." + ssh $server "mkdir -p /config/vault/config /config/vault/data /config/vault/logs" + scp $vaultConfigLocal "${server}:/config/vault/config/vault.hcl" + Write-Host "Applying management plane (portainer, vault, github-runner, watchtower) ..." + ssh $server "docker compose -f ~/valhalla-lab/portainer-compose.yml --env-file ~/valhalla-lab/.env up -d" } if ($DevStack) { diff --git a/portainer-compose.yml b/portainer-compose.yml index 1d31f5b..9730e8e 100644 --- a/portainer-compose.yml +++ b/portainer-compose.yml @@ -1,12 +1,23 @@ -# Management-plane services: portainer, github-runner, watchtower. -# These manage the application stack rather than being part of it. +# Management plane: portainer, vault, github-runner, watchtower. +# These manage / underpin the application stacks rather than being part of them, +# so they are deployed by raw `docker compose` (NOT a Portainer-managed stack) — +# Portainer cannot manage the stack that contains Portainer itself. # -# This file also owns the portainer_proxy network that the main -# docker-compose.yml references as external: true. +# This file owns the shared infrastructure networks: +# - edge : the single reverse-proxy network. Caddy + every service it +# proxies join this. Replaces caddy's old per-stack network +# membership. App stacks reference it as external. +# - portainer_proxy : legacy net for caddy -> portainer. Kept until the monolith +# is fully dismantled, then dropped (portainer is on edge too). +# +# Vault lives here (moved out of the dev stack): it is estate-wide secrets infra, +# must boot before the app stacks, and should not reseal when a dev tool changes. +# Vault data is the /config/vault bind mount — moving the service definition here +# does not touch any data. Re-unseal after (apply-compose.ps1 -VaultUnseal). # # Fresh-install order: -# docker compose -f portainer-compose.yml up -d # network + mgmt plane first -# dc up -d # application stack second +# docker compose -f portainer-compose.yml up -d # networks + mgmt plane first +# (then create the Portainer git stacks for proxy + each app domain) services: portainer: @@ -15,6 +26,7 @@ services: restart: unless-stopped networks: - portainer_proxy + - edge volumes: - /var/run/docker.sock:/var/run/docker.sock - /config/portainer:/data @@ -23,6 +35,35 @@ services: labels: - "com.centurylabs.watchtower.enable=false" + # ============================================================ + # VAULT (estate-wide secret management) + # One-time setup after first start: + # docker exec -it vault vault operator init + # vault operator unseal (3x with unseal keys) + # Unseal keys + root token live in .env (they are the one secret Vault + # itself cannot hold). Re-unseal after a restart with: + # powershell -File apply-compose.ps1 -VaultUnseal + # ============================================================ + vault: + container_name: vault + image: hashicorp/vault:latest + restart: unless-stopped + networks: + - edge + cap_add: + - IPC_LOCK + environment: + - VAULT_ADDR=http://0.0.0.0:8200 + volumes: + - /config/vault/config:/vault/config:ro + - /config/vault/data:/vault/data + - /config/vault/logs:/vault/logs + command: vault server -config=/vault/config/vault.hcl + ports: + - "8200:8200" + labels: + - "com.centurylabs.watchtower.enable=false" + github-runner: container_name: github_runner image: myoung34/github-runner:latest @@ -55,6 +96,9 @@ services: - "com.centurylabs.watchtower.enable=false" networks: + edge: + name: edge + driver: bridge portainer_proxy: name: portainer_proxy driver: bridge diff --git a/stacks/authentik/.env b/stacks/authentik/.env new file mode 100644 index 0000000..eb0723c --- /dev/null +++ b/stacks/authentik/.env @@ -0,0 +1,6 @@ +# authentik stack secrets +AUTHENTIK_IMAGE_TAG=2024.12.3 +AUTHENTIK_DB_USER=authentik +AUTHENTIK_DB_PASSWORD=ebb7d34ecbc4fc8f95cd16ef491d2162cdf8 +AUTHENTIK_DB_NAME=authentik +AUTHENTIK_SECRET_KEY=C33S/oXOVOLrlLnWGMRYadgH9zfEC7Bm1vHtbczkFxHcTOj9j6io397c/OGb diff --git a/stacks/authentik/docker-compose.yml b/stacks/authentik/docker-compose.yml new file mode 100644 index 0000000..92eae5a --- /dev/null +++ b/stacks/authentik/docker-compose.yml @@ -0,0 +1,92 @@ +# authentik stack — Authentik server + worker + dedicated Postgres and Redis. +# +# Volume -> bind-mount conversions (both low density -> SSD): +# db data -> /config/authentik/postgres +# redis data -> /config/authentik/redis +# Only the server joins edge (auth.ginnoir.com); worker/db/redis stay private. + +services: + authentik-server: + container_name: authentik_server + image: ghcr.io/goauthentik/server:${AUTHENTIK_IMAGE_TAG:-2024.12.3} + restart: unless-stopped + command: server + networks: [authentik, edge] + 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 + + authentik-worker: + container_name: authentik_worker + image: ghcr.io/goauthentik/server:${AUTHENTIK_IMAGE_TAG:-2024.12.3} + restart: unless-stopped + command: worker + networks: [authentik] + 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 + + authentik-db: + container_name: postgres_authentik + image: postgres:16-alpine + restart: unless-stopped + networks: [authentik] + environment: + POSTGRES_USER: ${AUTHENTIK_DB_USER:-authentik} + POSTGRES_PASSWORD: ${AUTHENTIK_DB_PASSWORD} + POSTGRES_DB: ${AUTHENTIK_DB_NAME:-authentik} + volumes: + - /config/authentik/postgres:/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 + + authentik-redis: + container_name: redis_authentik + image: redis:7-alpine + restart: unless-stopped + networks: [authentik] + command: --save 60 1 --loglevel warning + volumes: + - /config/authentik/redis:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + +networks: + authentik: + name: authentik + driver: bridge + edge: + name: edge + external: true diff --git a/stacks/dev/.env b/stacks/dev/.env new file mode 100644 index 0000000..e18b988 --- /dev/null +++ b/stacks/dev/.env @@ -0,0 +1,19 @@ +# dev stack secrets +TZ=America/Chicago + +# Gitea +GITEA_DB_PASSWORD=gitea_d3vp4ss_9f2k + +# code-server (VS Code in browser) +CODE_SERVER_PASSWORD=c0de_s3rver_pass + +# BookStack wiki +BOOKSTACK_DB_PASSWORD=bookstack_d3v_pass +BOOKSTACK_DB_ROOT_PASSWORD=bookstack_r00t_pass + +# Plane project management +# Rotate with: openssl rand -hex 32 +PLANE_SECRET_KEY=60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b52 +PLANE_DB_PASSWORD=plane_d3v_p4ss_8x3m +PLANE_MINIO_USER=planeadmin +PLANE_MINIO_PASSWORD=plane_minio_p4ss diff --git a/stacks/dev/docker-compose.yml b/stacks/dev/docker-compose.yml new file mode 100644 index 0000000..baa44c7 --- /dev/null +++ b/stacks/dev/docker-compose.yml @@ -0,0 +1,326 @@ +# dev stack — developer tooling: Gitea, code-server, registry, DBX, BookStack, Plane. +# (Renamed from "dev-stack"; private network renamed devstack -> dev. Vault moved +# to the management plane. All named volumes converted to tiered bind mounts.) +# +# Volume -> bind-mount conversions: +# gitea db -> /config/gitea/postgres (SSD) +# dbx data -> /config/dbx (SSD) +# plane db -> /config/plane/postgres (SSD) +# plane redis -> /config/plane/redis (SSD) +# plane minio -> /storage1/labdata/plane/minio (ZFS; attachments) +# Tier-corrections (already bind-mounted, moved SSD -> ZFS): +# registry -> /storage1/labdata/registry (image layers) +# gitea repositories -> /storage1/labdata/gitea/repositories (git repos) +# gitea lfs -> /storage1/labdata/gitea/lfs (large files) +# Gitea config/avatars/indexers stay on SSD (/config/gitea). The repo/LFS split +# is set via GITEA__repository__ROOT and GITEA__lfs__PATH pointing at ZFS mounts. + +services: + # ============================================================ GITEA + gitea: + container_name: gitea + image: gitea/gitea:latest + restart: unless-stopped + networks: [dev, edge] + environment: + - USER_UID=1000 + - USER_GID=1000 + - GITEA__database__DB_TYPE=postgres + - GITEA__database__HOST=postgres_gitea:5432 + - GITEA__database__NAME=gitea + - GITEA__database__USER=gitea + - GITEA__database__PASSWD=${GITEA_DB_PASSWORD} + - GITEA__server__DOMAIN=gitea.ginnoir.com + - GITEA__server__ROOT_URL=https://gitea.ginnoir.com + - GITEA__server__SSH_DOMAIN=gitea.ginnoir.com + - GITEA__server__SSH_PORT=2222 + # High-density git data on the ZFS pool; config stays on SSD (/data) + - GITEA__repository__ROOT=/repos + - GITEA__lfs__PATH=/repos-lfs + volumes: + - /config/gitea:/data + - /storage1/labdata/gitea/repositories:/repos + - /storage1/labdata/gitea/lfs:/repos-lfs + - /etc/localtime:/etc/localtime:ro + ports: + - "3030:3000" + - "2222:22" + depends_on: + postgres_gitea: + condition: service_healthy + + postgres_gitea: + container_name: postgres_gitea + image: postgres:16-alpine + restart: unless-stopped + networks: [dev] + environment: + POSTGRES_USER: gitea + POSTGRES_PASSWORD: ${GITEA_DB_PASSWORD} + POSTGRES_DB: gitea + volumes: + - /config/gitea/postgres:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U gitea -d gitea"] + interval: 10s + timeout: 5s + retries: 5 + + # ============================================================ CODE-SERVER + code-server: + container_name: code_server + image: lscr.io/linuxserver/code-server:latest + restart: unless-stopped + networks: [dev, edge] + environment: + - PUID=1000 + - PGID=1000 + - TZ=${TZ} + - PASSWORD=${CODE_SERVER_PASSWORD} + - SUDO_PASSWORD=${CODE_SERVER_PASSWORD} + - DEFAULT_WORKSPACE=/workspace + volumes: + - /config/code-server:/config + - /home/ginnoir:/workspace + ports: + - "8443:8443" + + # ============================================================ REGISTRY + registry: + container_name: registry + image: registry:2 + restart: unless-stopped + networks: [dev, edge] + volumes: + - /storage1/labdata/registry:/var/lib/registry + ports: + - "5000:5000" + + # ============================================================ DBX + dbx: + container_name: dbx + image: t8y2/dbx:latest + restart: unless-stopped + networks: [dev, edge] + volumes: + - /config/dbx:/app/data + ports: + - "4224:4224" + + # ============================================================ BOOKSTACK + bookstack: + container_name: bookstack + image: lscr.io/linuxserver/bookstack:latest + restart: unless-stopped + networks: [dev, edge] + environment: + - PUID=1000 + - PGID=1000 + - TZ=${TZ} + - APP_URL=https://docs.ginnoir.com + - DB_HOST=mariadb_bookstack + - DB_PORT=3306 + - DB_USER=bookstack + - DB_PASS=${BOOKSTACK_DB_PASSWORD} + - DB_DATABASE=bookstack + volumes: + - /config/bookstack:/config + ports: + - "6875:80" + depends_on: + mariadb_bookstack: + condition: service_healthy + + mariadb_bookstack: + container_name: mariadb_bookstack + image: lscr.io/linuxserver/mariadb:latest + restart: unless-stopped + networks: [dev] + environment: + - PUID=1000 + - PGID=1000 + - TZ=${TZ} + - MYSQL_ROOT_PASSWORD=${BOOKSTACK_DB_ROOT_PASSWORD} + - MYSQL_DATABASE=bookstack + - MYSQL_USER=bookstack + - MYSQL_PASSWORD=${BOOKSTACK_DB_PASSWORD} + volumes: + - /config/bookstack-db:/config + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + interval: 10s + timeout: 5s + retries: 5 + + # ============================================================ PLANE + plane-web: + container_name: plane_web + image: makeplane/plane-frontend:latest + restart: unless-stopped + networks: [dev, edge] + command: node web/server.js + environment: + - NEXT_PUBLIC_API_BASE_URL=https://plane.ginnoir.com + + plane-api: + container_name: plane_api + image: makeplane/plane-backend:latest + restart: unless-stopped + networks: [dev, edge] + command: ./bin/docker-entrypoint-api.sh + environment: + - SECRET_KEY=${PLANE_SECRET_KEY} + - DEBUG=0 + - DATABASE_URL=postgresql://plane:${PLANE_DB_PASSWORD}@postgres_plane:5432/plane + - REDIS_URL=redis://redis_plane:6379/ + - USE_MINIO=1 + - AWS_REGION=us-east-1 + - AWS_ACCESS_KEY_ID=${PLANE_MINIO_USER} + - AWS_SECRET_ACCESS_KEY=${PLANE_MINIO_PASSWORD} + - AWS_S3_ENDPOINT_URL=http://plane_minio:9000 + - AWS_S3_BUCKET_NAME=uploads + - WEB_URL=https://plane.ginnoir.com + - CORS_ALLOWED_ORIGINS=https://plane.ginnoir.com + - GUNICORN_WORKERS=2 + depends_on: + postgres_plane: + condition: service_healthy + redis_plane: + condition: service_healthy + plane-migrator: + condition: service_completed_successfully + + plane-worker: + container_name: plane_worker + image: makeplane/plane-backend:latest + restart: unless-stopped + networks: [dev] + command: ./bin/docker-entrypoint-worker.sh + environment: + - SECRET_KEY=${PLANE_SECRET_KEY} + - DEBUG=0 + - DATABASE_URL=postgresql://plane:${PLANE_DB_PASSWORD}@postgres_plane:5432/plane + - REDIS_URL=redis://redis_plane:6379/ + - USE_MINIO=1 + - AWS_REGION=us-east-1 + - AWS_ACCESS_KEY_ID=${PLANE_MINIO_USER} + - AWS_SECRET_ACCESS_KEY=${PLANE_MINIO_PASSWORD} + - AWS_S3_ENDPOINT_URL=http://plane_minio:9000 + - AWS_S3_BUCKET_NAME=uploads + depends_on: + postgres_plane: + condition: service_healthy + redis_plane: + condition: service_healthy + plane-migrator: + condition: service_completed_successfully + + plane-beat: + container_name: plane_beat + image: makeplane/plane-backend:latest + restart: unless-stopped + networks: [dev] + command: ./bin/docker-entrypoint-beat.sh + environment: + - SECRET_KEY=${PLANE_SECRET_KEY} + - DEBUG=0 + - DATABASE_URL=postgresql://plane:${PLANE_DB_PASSWORD}@postgres_plane:5432/plane + - REDIS_URL=redis://redis_plane:6379/ + depends_on: + postgres_plane: + condition: service_healthy + redis_plane: + condition: service_healthy + plane-migrator: + condition: service_completed_successfully + + plane-migrator: + container_name: plane_migrator + image: makeplane/plane-backend:latest + restart: "no" + networks: [dev] + command: ./bin/docker-entrypoint-migrator.sh + environment: + - SECRET_KEY=${PLANE_SECRET_KEY} + - DATABASE_URL=postgresql://plane:${PLANE_DB_PASSWORD}@postgres_plane:5432/plane + - REDIS_URL=redis://redis_plane:6379/ + depends_on: + postgres_plane: + condition: service_healthy + redis_plane: + condition: service_healthy + + plane-create-bucket: + container_name: plane_create_bucket + image: minio/mc:latest + restart: "no" + networks: [dev] + depends_on: + plane-minio: + condition: service_healthy + entrypoint: > + /bin/sh -c " + until /usr/bin/mc alias set plane_minio http://plane_minio:9000 $$MINIO_USER $$MINIO_PASSWORD; do sleep 1; done; + /usr/bin/mc mb plane_minio/uploads --ignore-existing; + exit 0; + " + environment: + - MINIO_USER=${PLANE_MINIO_USER} + - MINIO_PASSWORD=${PLANE_MINIO_PASSWORD} + + postgres_plane: + container_name: postgres_plane + image: postgres:16-alpine + restart: unless-stopped + networks: [dev] + environment: + POSTGRES_USER: plane + POSTGRES_PASSWORD: ${PLANE_DB_PASSWORD} + POSTGRES_DB: plane + volumes: + - /config/plane/postgres:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U plane -d plane"] + interval: 10s + timeout: 5s + retries: 5 + + redis_plane: + container_name: redis_plane + image: redis:7-alpine + restart: unless-stopped + networks: [dev] + command: --save 60 1 --loglevel warning + volumes: + - /config/plane/redis:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + + plane-minio: + container_name: plane_minio + image: minio/minio:latest + restart: unless-stopped + networks: [dev] + command: server /data --console-address ":9001" + environment: + MINIO_ROOT_USER: ${PLANE_MINIO_USER} + MINIO_ROOT_PASSWORD: ${PLANE_MINIO_PASSWORD} + volumes: + - /storage1/labdata/plane/minio:/data + healthcheck: + test: ["CMD-SHELL", "curl -sf http://localhost:9000/minio/health/live || exit 1"] + interval: 30s + timeout: 20s + retries: 3 + start_period: 30s + +networks: + dev: + name: dev + driver: bridge + edge: + name: edge + external: true diff --git a/stacks/famapp/.env b/stacks/famapp/.env new file mode 100644 index 0000000..1a9ec64 --- /dev/null +++ b/stacks/famapp/.env @@ -0,0 +1,33 @@ +# famapp stack secrets +FAMAPP_IMAGE=ghcr.io/ginnoir/famapp:latest +FAMAPP_PULL_POLICY=always +RUN_MIGRATIONS=true +LOG_LEVEL=info + +AUTH_URL=https://fam.ginnoir.com +FAMAPP_DB_USER=famapp +FAMAPP_DB_PASSWORD=a9a999d16dd5cdc521bb62eff331265d3c24 +FAMAPP_DB_NAME=famapp + +# famapp Auth.js session secret (mapped to the container's AUTH_SECRET) +FAMAPP_AUTH_SECRET=CJsICgBs78YxxSdJfS8KU9Zn9k+QytRoVui9kVtuHtI= + +# OIDC via Authentik (public URL through Caddy — no container dependency) +AUTH_OIDC_ISSUER=https://auth.ginnoir.com/application/o/famapp/ +AUTH_OIDC_CLIENT_ID=6uOZ8yjTR9Z2m1RZU1cLHYXHV2Lv6zZtfuJcrsdq +AUTH_OIDC_CLIENT_SECRET=CPcNM8jXaJ2jHFKxRwGuuKyMKJsT502l3FztO1AOcFqDzWqlWu5x7Ox52Q6zyBa2v33YzoA9igrXW8FFjd1Gp7wrZV4PGTwvXmOd7pIMOY93gNotAgJaewT8WHW1F3YD + +VAPID_PUBLIC_KEY=BNED_UgfPhyoG6_JOabhpQpN3nr6QM5tO0PDiAadMGCcZROn8LM7tnsBrKaB1E5kfuDFb1rC_M86cT3uCvBqSBk +VAPID_PRIVATE_KEY=QyJiyCI2TJa_QWVXznfj4vjpWTBsyFfSCtn1brpE9aI +VAPID_SUBJECT=mailto:3nigma.matt@gmail.com + +NTFY_URL=https://ntfy.ginnoir.com +NTFY_TOPIC=famapp + +# MinIO object storage (plant/container image uploads) +famapp_MINIO_ROOT_USER=famapp +famapp_MINIO_ROOT_PASSWORD=d0fet0th3x +famapp_MINIO_BUCKET=garden + +famapp_OPENPLANTBOOK_CLIENT_ID=5JdQLab69RhVaF4er7b7D29BCrkf3fo3OEiDsOQa +famapp_OPENPLANTBOOK_CLIENT_SECRET=kxPTrdQPAVta7sjVuIRf5EGIJ1fKvlMlqRSJpQtFxRftJhg7oeGnXzwH7ls6tluQLFUYVDYRMwISiAuKZGZLvkuJzUwFGCwhZw7UtW8OvcTGy4717WeNUinvuCDSsNXP diff --git a/stacks/famapp/docker-compose.yml b/stacks/famapp/docker-compose.yml new file mode 100644 index 0000000..9606ae8 --- /dev/null +++ b/stacks/famapp/docker-compose.yml @@ -0,0 +1,90 @@ +# famapp stack — family app + its Postgres and MinIO (plant/container images). +# +# Volume -> bind-mount conversions: +# db data -> /config/famapp/postgres (SSD; DB) +# minio data -> /storage1/labdata/famapp/minio (ZFS; uploaded images) +# Only famapp joins edge (fam.ginnoir.com); db + minio stay private. +# famapp reaches Authentik (OIDC) and ntfy over their public URLs through Caddy, +# so there is no cross-stack container dependency. + +services: + famapp: + container_name: famapp + image: ${FAMAPP_IMAGE:-ghcr.io/ginnoir/famapp:latest} + pull_policy: ${FAMAPP_PULL_POLICY:-always} + restart: unless-stopped + networks: [famapp, edge] + 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: ${FAMAPP_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} + OPENPLANTBOOK_CLIENT_ID: ${famapp_OPENPLANTBOOK_CLIENT_ID} + OPENPLANTBOOK_CLIENT_SECRET: ${famapp_OPENPLANTBOOK_CLIENT_SECRET} + ports: + - "3010:3000" + depends_on: + famapp-db: + condition: service_healthy + famapp-minio: + condition: service_healthy + + famapp-db: + container_name: postgres_famapp + image: postgres:16-alpine + restart: unless-stopped + networks: [famapp] + environment: + POSTGRES_USER: ${FAMAPP_DB_USER} + POSTGRES_PASSWORD: ${FAMAPP_DB_PASSWORD} + POSTGRES_DB: ${FAMAPP_DB_NAME} + volumes: + - /config/famapp/postgres:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${FAMAPP_DB_USER} -d ${FAMAPP_DB_NAME}"] + interval: 10s + timeout: 5s + retries: 5 + + famapp-minio: + container_name: minio_famapp + image: minio/minio:latest + command: server /data --console-address ":9003" + restart: unless-stopped + networks: [famapp] + environment: + MINIO_ROOT_USER: ${famapp_MINIO_ROOT_USER} + MINIO_ROOT_PASSWORD: ${famapp_MINIO_ROOT_PASSWORD} + volumes: + - /storage1/labdata/famapp/minio:/data + ports: + - "9002:9000" + - "9003:9003" + 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: + name: famapp + driver: bridge + edge: + name: edge + external: true diff --git a/stacks/foundry/.env b/stacks/foundry/.env new file mode 100644 index 0000000..1192d50 --- /dev/null +++ b/stacks/foundry/.env @@ -0,0 +1,5 @@ +# foundry stack secrets +FOUNDRY_USERNAME=ginnoir +FOUNDRY_PASSWORD=agt-UAU!uky_ykh3juy +FOUNDRY_ADMIN_KEY=fuckyouben +FOUNDRY_PATCH_URLS=https://gist.githubusercontent.com/ginnoir/898b152a09369f9089770daa71887262/raw/a354e4284f2b1feb616910f65bfe65a4ffdb5473/plutonium-12.x.sh diff --git a/stacks/foundry/docker-compose.yml b/stacks/foundry/docker-compose.yml new file mode 100644 index 0000000..678f6c5 --- /dev/null +++ b/stacks/foundry/docker-compose.yml @@ -0,0 +1,69 @@ +# foundry stack — Foundry VTT instances + 5etools mirror. +# Tabletop services; bind-mounted to /storage1/foundry*. They don't talk to each +# other, so they only need `edge` for Caddy to proxy them. + +services: + foundry: + container_name: foundry + image: felddy/foundryvtt:latest + hostname: valhalla-primary + init: true + restart: unless-stopped + networks: [edge] + volumes: + - /storage1/foundry:/data + - /storage1/foundry/updatemain.bash:/home/foundry/updatemain.bash + - /storage1/Books/Tabletop:/data/Data/storage + environment: + - FOUNDRY_USERNAME=${FOUNDRY_USERNAME} + - FOUNDRY_PASSWORD=${FOUNDRY_PASSWORD} + - FOUNDRY_ADMIN_KEY=${FOUNDRY_ADMIN_KEY} + - CONTAINER_VERBOSE=true + - FOUNDRY_HOSTNAME=foundry.ginnoir.com + - FOUNDRY_PROXY_PORT=443 + - FOUNDRY_PROXY_SSL=true + - CONTAINER_PRESERVE_OWNER=/data/Data/storage + - FOUNDRY_UID=1000 + - FOUNDRY_GID=1000 + - CONTAINER_PATCH_URLS=${FOUNDRY_PATCH_URLS} + ports: + - "30000:30000" + + foundry2: + container_name: foundry2 + image: felddy/foundryvtt:latest + hostname: valhalla-secondary + init: true + restart: unless-stopped + networks: [edge] + volumes: + - /storage1/foundry2:/data + - /storage1/foundry/updatemain.bash:/home/foundry/updatemain.bash + - /storage1/Books/Tabletop:/data/Data/storage + environment: + - FOUNDRY_USERNAME=${FOUNDRY_USERNAME} + - FOUNDRY_PASSWORD=${FOUNDRY_PASSWORD} + - FOUNDRY_ADMIN_KEY=${FOUNDRY_ADMIN_KEY} + - CONTAINER_VERBOSE=true + - FOUNDRY_HOSTNAME=foundry2.ginnoir.com + - FOUNDRY_PROXY_PORT=443 + - FOUNDRY_PROXY_SSL=true + - CONTAINER_PRESERVE_OWNER=/data/Data/storage + - FOUNDRY_UID=1000 + - FOUNDRY_GID=1000 + - CONTAINER_PATCH_URLS=${FOUNDRY_PATCH_URLS} + ports: + - "30001:30000" + + 5etools: + container_name: 5etools + image: ghcr.io/5etools-mirror-3/5etools-src:latest + restart: unless-stopped + networks: [edge] + ports: + - "9009:80" + +networks: + edge: + name: edge + external: true diff --git a/stacks/media/.env b/stacks/media/.env new file mode 100644 index 0000000..2712830 --- /dev/null +++ b/stacks/media/.env @@ -0,0 +1,5 @@ +# media stack config +TZ=America/Chicago +PUID=1000 +PGID=1000 +ROOT=/ diff --git a/stacks/media/docker-compose.yml b/stacks/media/docker-compose.yml new file mode 100644 index 0000000..cd4ec7d --- /dev/null +++ b/stacks/media/docker-compose.yml @@ -0,0 +1,206 @@ +# media stack — *arr suite, download clients, and media utilities. +# All config/data is bind-mounted to /config and /storage1 already (no named +# volumes to migrate). Services join the private `media` net for inter-app +# traffic (prowlarr<->sonarr/radarr) and `edge` so Caddy can proxy them. + +services: + deluge: + container_name: deluge + image: lscr.io/linuxserver/deluge:latest + restart: unless-stopped + networks: [media, edge] + environment: + - PUID=${PUID} + - PGID=${PGID} + - TZ=${TZ} + volumes: + - ${ROOT}/storage1/torrents:/downloads + - ${ROOT}/config/deluge:/config + ports: + - "8112:8112" + + nzbget: + container_name: nzbget + image: lscr.io/linuxserver/nzbget:latest + restart: unless-stopped + networks: [media, edge] + environment: + - PUID=${PUID} + - PGID=${PGID} + - TZ=${TZ} + volumes: + - ${ROOT}/storage1/complete:/downloads + - ${ROOT}/config/nzbget:/config + ports: + - "6789:6789" + + sonarr: + container_name: sonarr + image: lscr.io/linuxserver/sonarr:latest + restart: unless-stopped + networks: [media, edge] + environment: + - PUID=${PUID} + - PGID=${PGID} + - TZ=${TZ} + volumes: + - /etc/localtime:/etc/localtime:ro + - ${ROOT}/config/sonarr:/config + - ${ROOT}/storage1/Media/TV:/tv + - ${ROOT}/storage1/Media/Anime:/anime + - ${ROOT}/storage1/complete:/downloads + ports: + - "8989:8989" + + radarr: + container_name: radarr + image: lscr.io/linuxserver/radarr:latest + restart: unless-stopped + networks: [media, edge] + environment: + - PUID=${PUID} + - PGID=${PGID} + - TZ=${TZ} + volumes: + - /etc/localtime:/etc/localtime:ro + - ${ROOT}/config/radarr:/config + - ${ROOT}/storage1/Media/Movies:/movies + - ${ROOT}/storage1/complete:/downloads + ports: + - "7878:7878" + + bazarr: + container_name: bazarr + image: lscr.io/linuxserver/bazarr:latest + restart: unless-stopped + networks: [media, edge] + environment: + - PUID=${PUID} + - PGID=${PGID} + - TZ=${TZ} + - UMASK_SET=022 + volumes: + - ${ROOT}/config/bazarr:/config + - ${ROOT}/storage1/Media/Movies:/movies + - ${ROOT}/storage1/Media/TV:/tv + - ${ROOT}/storage1/Media/Anime:/anime + ports: + - "6767:6767" + + prowlarr: + container_name: prowlarr + image: ghcr.io/hotio/prowlarr:latest + restart: unless-stopped + networks: [media, edge] + environment: + - PUID=1000 + - PGID=1000 + - UMASK=002 + - TZ=${TZ} + volumes: + - /config/prowlarr:/config + ports: + - "9696:9696" + + overseerr: + container_name: overseerr + image: sctx/overseerr:latest + restart: unless-stopped + networks: [media, edge] + environment: + - PUID=1000 + - PGID=1000 + - UMASK=002 + - TZ=${TZ} + volumes: + - /config/overseerr:/app/config + ports: + - "5055:5055" + + tautulli: + image: ghcr.io/linuxserver/tautulli:latest + container_name: tautulli + restart: unless-stopped + networks: [media, edge] + environment: + - PUID=${PUID} + - PGID=${PGID} + - TZ=${TZ} + volumes: + - ${ROOT}/config/tautulli:/config + - ${ROOT}/storage1/PMS:/logs + ports: + - "8181:8181" + + qbittorrent: + image: lscr.io/linuxserver/qbittorrent:latest + container_name: qbittorrent + restart: unless-stopped + networks: [media, edge] + environment: + - PUID=1000 + - PGID=1000 + - TZ=${TZ} + - WEBUI_PORT=3232 + - TORRENTING_PORT=6881 + volumes: + - /config/qbittorrent:/config + - /storage1/complete:/downloads + ports: + - "3232:3232" + - "6881:6881" + - "6881:6881/udp" + + whisparr: + container_name: whisparr + image: ghcr.io/hotio/whisparr:latest + restart: unless-stopped + networks: [media, edge] + environment: + - PUID=1000 + - PGID=1000 + - UMASK=002 + - TZ=${TZ} + - WEBUI_PORTS=6969/tcp + volumes: + - /config/whisparr:/config + - /storage1:/data + ports: + - "6969:6969" + + stash: + image: stashapp/stash:latest + container_name: stash + restart: unless-stopped + networks: [media, edge] + logging: + driver: json-file + options: + max-file: "10" + max-size: "2m" + environment: + - PUID=1000 + - PGID=1000 + - STASH_STASH=/data/ + - STASH_GENERATED=/generated/ + - STASH_METADATA=/metadata/ + - STASH_CACHE=/cache/ + - STASH_PORT=6970 + volumes: + - /etc/localtime:/etc/localtime:ro + - /config/stash:/root/.stash + - /storage1/LinuxISOs:/data + - /storage1/stash/metadata:/metadata + - /storage1/stash/cache:/cache + - /storage1/stash/blobs:/blobs + - /storage1/stash/generated:/generated + ports: + - "6971:6970" + +networks: + media: + name: media + driver: bridge + edge: + name: edge + external: true diff --git a/stacks/monitoring/.env b/stacks/monitoring/.env new file mode 100644 index 0000000..80f9d53 --- /dev/null +++ b/stacks/monitoring/.env @@ -0,0 +1,3 @@ +# monitoring stack secrets +# Homarr config encryption key. Generate with: openssl rand -hex 32 +HOMARR_SECRET_ENCRYPTION_KEY=98989838bf06c9e42d77944ce848963a22ab316b55a09b0d199652ef0934394f diff --git a/stacks/monitoring/docker-compose.yml b/stacks/monitoring/docker-compose.yml new file mode 100644 index 0000000..1c51fd6 --- /dev/null +++ b/stacks/monitoring/docker-compose.yml @@ -0,0 +1,35 @@ +# monitoring stack — Uptime Kuma + Homarr dashboard. +# (Named "monitoring" to avoid confusion with the raw-compose management plane.) +# Both are bind-mounted and proxied internal-only; both join edge. + +services: + uptime-kuma: + container_name: uptime_kuma + image: louislam/uptime-kuma:2 + restart: unless-stopped + networks: [edge] + ports: + - "3001:3001" + volumes: + - /config/uptime-kuma:/app/data + - /var/run/docker.sock:/var/run/docker.sock:ro + + homarr: + container_name: homarr + image: ghcr.io/homarr-labs/homarr:latest + restart: unless-stopped + networks: [edge] + environment: + - SECRET_ENCRYPTION_KEY=${HOMARR_SECRET_ENCRYPTION_KEY} + ports: + - "7575:7575" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - /config/homarr/configs:/app/data/configs + - /config/homarr/icons:/app/public/icons + - /config/homarr/data:/data + +networks: + edge: + name: edge + external: true diff --git a/stacks/notify/.env b/stacks/notify/.env new file mode 100644 index 0000000..b5ae5e0 --- /dev/null +++ b/stacks/notify/.env @@ -0,0 +1,5 @@ +# notify stack secrets +TZ=America/Chicago +FRESHRSS_API_PASSWORD=d0fet0th3x +FRESHRSS_PASSWORD=d0fet0th3x +FRESHRSS_EMAIL=3nigma.matt@gmail.com diff --git a/stacks/notify/docker-compose.yml b/stacks/notify/docker-compose.yml new file mode 100644 index 0000000..61a22c2 --- /dev/null +++ b/stacks/notify/docker-compose.yml @@ -0,0 +1,82 @@ +# notify stack — ntfy push, FreshRSS reader, and the Vigilant RSS watcher. +# +# Volume -> bind-mount conversion: +# vigilant cache -> /config/vigilant/cache (SSD; regenerable) +# ntfy + freshrss are proxied (ntfy.ginnoir.com, freshrss.ginnoir.com). Vigilant +# only needs egress to post to ntfy's public URL; all three join edge. + +services: + ntfy: + image: binwiederhier/ntfy:latest + container_name: ntfy + command: + - serve + restart: unless-stopped + networks: [edge] + environment: + - TZ=${TZ} + - NTFY_DEFAULT_HOST=https://ntfy.ginnoir.com + - NTFY_BASE_URL=https://ntfy.ginnoir.com + - NTFY_CACHE_FILE=/var/lib/ntfy/cache.db + - NTFY_AUTH_FILE=/var/lib/ntfy/auth.db + - NTFY_AUTH_DEFAULT_ACCESS=read-write + - NTFY_BEHIND_PROXY=true + - NTFY_ATTACHMENT_CACHE_DIR=/var/lib/ntfy/attachments + - NTFY_ENABLE_LOGIN=true + user: 1000:1000 + volumes: + - /config/ntfy/var/cache/ntfy:/var/cache/ntfy + - /config/ntfy/etc/ntfy:/etc/ntfy + - /config/ntfy/var/lib/ntfy:/var/lib/ntfy + ports: + - "8000:80" + healthcheck: + test: + [ + "CMD-SHELL", + "wget -q --tries=1 http://localhost:80/v1/health -O - | grep -Eo '\"healthy\"\\s*:\\s*true' || exit 1", + ] + interval: 60s + timeout: 10s + retries: 3 + start_period: 40s + + freshrss: + image: freshrss/freshrss:latest + container_name: freshrss + hostname: freshrss + restart: unless-stopped + networks: [edge] + logging: + options: + max-size: 10m + volumes: + - /config/freshrss/data:/var/www/FreshRSS/data + - /config/freshrss/extensions:/var/www/FreshRSS/extensions + ports: + - "9999:80" + environment: + - CRON_MIN=3,33 + - TRUSTED_PROXY=172.16.0.1/12 192.168.0.1/16 + - FRESHRSS_INSTALL=--default-user ginnoir --api-enabled --base-url https://freshrss.ginnoir.com + - FRESHRSS_USER=--api-password ${FRESHRSS_API_PASSWORD} --email ${FRESHRSS_EMAIL} --password ${FRESHRSS_PASSWORD} --user ginnoir + + vigilant: + container_name: vigilant + image: ghcr.io/verifiedjoseph/vigilant:latest + restart: unless-stopped + networks: [edge] + environment: + - VIGILANT_NOTIFICATION_SERVICE=ntfy + - VIGILANT_NOTIFICATION_NTFY_URL=https://ntfy.ginnoir.com/ + - VIGILANT_NOTIFICATION_NTFY_TOPIC=RSS + volumes: + - /config/vigilant/feeds.yaml:/app/feeds.yaml + - /config/vigilant/cache:/app/cache + security_opt: + - no-new-privileges:true + +networks: + edge: + name: edge + external: true diff --git a/stacks/owncloud/.env b/stacks/owncloud/.env new file mode 100644 index 0000000..ebcee36 --- /dev/null +++ b/stacks/owncloud/.env @@ -0,0 +1,7 @@ +# owncloud stack secrets +OWNCLOUD_DOMAIN=files.ginnoir.com +OWNCLOUD_DB_USER=ginnoir +OWNCLOUD_DB_PASSWORD=d0fet0th3x +OWNCLOUD_ADMIN_USER=ginnoir +OWNCLOUD_ADMIN_PASSWORD=d0fet0th3x +MYSQL_ROOT_PASSWORD=d0fet0th3x diff --git a/stacks/owncloud/docker-compose.yml b/stacks/owncloud/docker-compose.yml new file mode 100644 index 0000000..6868857 --- /dev/null +++ b/stacks/owncloud/docker-compose.yml @@ -0,0 +1,81 @@ +# owncloud stack — ownCloud server + its dedicated MariaDB and Redis. +# DB/cache stay co-located with the app so depends_on health-gating keeps working. +# All data is bind-mounted already (/config/owncloud, /config/mysql, /config/redis). +# Only the owncloud server joins `edge`; mariadb/redis stay private. + +services: + owncloud: + image: owncloud/server:latest + container_name: owncloud_server + restart: unless-stopped + networks: [owncloud, edge] + ports: + - "8080:8080" + depends_on: + mariadb: + condition: service_healthy + redis: + condition: service_healthy + environment: + - OWNCLOUD_DOMAIN=${OWNCLOUD_DOMAIN} + - OWNCLOUD_DB_TYPE=mysql + - OWNCLOUD_DB_NAME=owncloud + - OWNCLOUD_DB_USERNAME=${OWNCLOUD_DB_USER} + - OWNCLOUD_DB_PASSWORD=${OWNCLOUD_DB_PASSWORD} + - OWNCLOUD_DB_HOST=mariadb + - OWNCLOUD_ADMIN_USERNAME=${OWNCLOUD_ADMIN_USER} + - OWNCLOUD_ADMIN_PASSWORD=${OWNCLOUD_ADMIN_PASSWORD} + - OWNCLOUD_MYSQL_UTF8MB4=true + - OWNCLOUD_REDIS_ENABLED=true + - OWNCLOUD_REDIS_HOST=redis + healthcheck: + test: ["CMD", "/usr/bin/healthcheck"] + interval: 30s + timeout: 10s + retries: 5 + volumes: + - /config/owncloud:/mnt/data + - /storage1/Books:/mnt/books + - /mnt/dropbox:/mnt/data/files/dropbox + - /storage1/ROMs:/mnt/roms + + mariadb: + image: mariadb:latest + container_name: owncloud_mariadb + restart: unless-stopped + networks: [owncloud] + environment: + - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} + - MYSQL_USER=${OWNCLOUD_DB_USER} + - MYSQL_PASSWORD=${OWNCLOUD_DB_PASSWORD} + - MYSQL_DATABASE=owncloud + command: ["--max-allowed-packet=128M", "--innodb-log-file-size=64M"] + healthcheck: + test: ["CMD", "mariadb-admin", "ping", "-u", "root", "--password=${MYSQL_ROOT_PASSWORD}"] + interval: 10s + timeout: 5s + retries: 5 + volumes: + - /config/mysql:/var/lib/mysql + + redis: + image: redis:latest + container_name: owncloud_redis + restart: unless-stopped + networks: [owncloud] + command: ["--databases", "1"] + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + volumes: + - /config/redis:/data + +networks: + owncloud: + name: owncloud + driver: bridge + edge: + name: edge + external: true diff --git a/stacks/proxy/.env b/stacks/proxy/.env new file mode 100644 index 0000000..5011ca4 --- /dev/null +++ b/stacks/proxy/.env @@ -0,0 +1,4 @@ +# proxy stack secrets +# Cloudflare API token for Caddy's DNS-01 TLS challenge. +# Scoped token: Zone > DNS > Edit on the ginnoir.com zone only. +CF_API_TOKEN=cfut_ijIT9HDjXWKgd0dtnyLqxv8L3deB7hSDivSqquwp905a6c51 diff --git a/stacks/proxy/docker-compose.yml b/stacks/proxy/docker-compose.yml new file mode 100644 index 0000000..2dc96b1 --- /dev/null +++ b/stacks/proxy/docker-compose.yml @@ -0,0 +1,37 @@ +# proxy stack — Caddy reverse proxy / TLS terminator. +# +# Caddy is the single ingress for the whole estate. It joins only the shared +# `edge` network (owned by the management plane); every service it proxies also +# joins `edge`, so `reverse_proxy :` resolves by container name. +# This replaces Caddy's old membership in 11 separate per-stack networks. +# +# The Caddyfile itself is unchanged — upstreams are service names that resolve +# over edge. It is bind-mounted from /config/caddy/Caddyfile and hot-reloaded +# by the GitHub Actions runner on push (deploy.yml). +# +# serfriz/caddy-cloudflare-ddns bundles caddy-dns/cloudflare (DNS-01 ACME), +# caddy-cloudflare-ip (real client IPs behind CF), caddy-dynamicdns. + +services: + caddy: + container_name: caddy + image: serfriz/caddy-cloudflare-ddns:latest + restart: unless-stopped + networks: + - edge + ports: + - "80:80" + - "443:443" + environment: + - CF_API_TOKEN=${CF_API_TOKEN} + volumes: + - /config/caddy/Caddyfile:/etc/caddy/Caddyfile + - /config/caddy/site:/srv + - /config/caddy/data:/data + - /config/caddy/config:/config + - /storage1/Books:/srv/Books + +networks: + edge: + name: edge + external: true diff --git a/stacks/remote/docker-compose.yml b/stacks/remote/docker-compose.yml new file mode 100644 index 0000000..b35a159 --- /dev/null +++ b/stacks/remote/docker-compose.yml @@ -0,0 +1,28 @@ +# remote stack — RustDesk relay (hbbr) + signal/ID server (hbbs). +# Not proxied by Caddy (raw TCP/UDP ports); bind-mounted to /config/rustdesk. +# They use the project default network for the inter-container dependency. + +services: + hbbr: + container_name: hbbr + image: rustdesk/rustdesk-server:latest + command: hbbr + restart: unless-stopped + ports: + - "21117:21117" + volumes: + - /config/rustdesk/hbbr/data:/root + + hbbs: + container_name: hbbs + image: rustdesk/rustdesk-server:latest + command: hbbs + restart: unless-stopped + ports: + - "21115:21115" + - "21116:21116" + - "21116:21116/udp" + volumes: + - /config/rustdesk/hbbs/data:/root + depends_on: + - hbbr diff --git a/stacks/resume/.env b/stacks/resume/.env new file mode 100644 index 0000000..d481159 --- /dev/null +++ b/stacks/resume/.env @@ -0,0 +1,9 @@ +# resume stack secrets +POSTGRES_PASSWORD=postgres +MINIO_ROOT_USER=minioadmin +MINIO_ROOT_PASSWORD=minioadmin +CHROME_TOKEN=chrome_token +# Auth/JWT signing secret for Reactive Resume (the app reads ${RESUME_AUTH_SECRET}). +# Pinned to the running value so recreating the container keeps sessions valid. +# Rotate with: openssl rand -hex 64 +RESUME_AUTH_SECRET=1b7e96f61f080d04d4bf673d9ea1d9349eb7693cddee5772b6c3f153c88032db5b97efd8570087bc6f340d2cd874aabc1e5c681bc22889921240d6a23738bc7f diff --git a/stacks/resume/docker-compose.yml b/stacks/resume/docker-compose.yml new file mode 100644 index 0000000..4768a5b --- /dev/null +++ b/stacks/resume/docker-compose.yml @@ -0,0 +1,99 @@ +# resume stack — Reactive Resume app + Postgres, MinIO, and the Chrome printer. +# +# Volume -> bind-mount conversions (tiered per density policy): +# postgres data -> /config/resume/postgres (SSD; DB, low density) +# minio data -> /storage1/labdata/resume/minio (ZFS; object store, blobs) +# The Chrome service is stateless. app + resume-minio join edge (resume.ginnoir.com, +# storage.j-costa.com, minio.ginnoir.com); postgres stays private. + +services: + postgres: + container_name: postgres_resume + image: postgres:16-alpine + restart: unless-stopped + networks: [resume] + volumes: + - /config/resume/postgres:/var/lib/postgresql/data + environment: + - POSTGRES_DB=postgres + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"] + interval: 10s + timeout: 5s + retries: 5 + + resume-minio: + container_name: minio_resume + image: minio/minio:latest + restart: unless-stopped + networks: [resume, edge] + command: server /data --console-address :9001 + ports: + - "9000:9000" + - "9004:9001" + volumes: + - /storage1/labdata/resume/minio:/data + environment: + - MINIO_ROOT_USER=${MINIO_ROOT_USER} + - MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD} + healthcheck: + test: ["CMD-SHELL", "curl -sf http://localhost:9000/minio/health/live || exit 1"] + interval: 30s + timeout: 20s + retries: 3 + start_period: 30s + + chrome: + container_name: chrome + image: ghcr.io/browserless/chromium:latest + restart: unless-stopped + networks: [resume] + environment: + - TIMEOUT=10000 + - CONCURRENT=10 + - TOKEN=${CHROME_TOKEN} + - EXIT_ON_HEALTH_FAILURE=true + - PRE_REQUEST_HEALTH_CHECK=true + + app: + container_name: resume + image: amruthpillai/reactive-resume:latest + restart: unless-stopped + networks: [resume, edge] + ports: + - "3000:3000" + depends_on: + postgres: + condition: service_healthy + resume-minio: + condition: service_healthy + chrome: + condition: service_started + environment: + - PORT=3000 + - NODE_ENV=production + - APP_URL=https://resume.ginnoir.com + - STORAGE_URL=https://storage.j-costa.com/default + - PRINTER_ENDPOINT=http://chrome:3000 + - PRINTER_TOKEN=${CHROME_TOKEN} + - DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/postgres + - AUTH_SECRET=${RESUME_AUTH_SECRET} + - MAIL_FROM=noreply@localhost + - STORAGE_ENDPOINT=resume-minio + - STORAGE_PORT=9000 + - STORAGE_REGION=us-east-1 + - STORAGE_BUCKET=default + - STORAGE_ACCESS_KEY=${MINIO_ROOT_USER} + - STORAGE_SECRET_KEY=${MINIO_ROOT_PASSWORD} + - STORAGE_USE_SSL=false + - STORAGE_SKIP_BUCKET_CHECK=false + +networks: + resume: + name: resume + driver: bridge + edge: + name: edge + external: true