From f34382ebdd2c3789a7f4aa42e7e00f561a5dd0ef Mon Sep 17 00:00:00 2001 From: bunker-admin Date: Thu, 21 May 2026 19:32:21 -0600 Subject: [PATCH] chore(approach-c): Phase 0 initial template overlay + session handoff This session shipped: - Approach B end-to-end (commit 4a3d9d7): full rollout to all 7 tenants; marcelle E2E validated twice (121s + 100s). - v2.10.2 surgical update applied to 6 remaining tenants. This commit lands the kickoff for Approach C (template re-render path): scripts/templates changes: - docker-compose.yml.hbs.OLD-style-pre-approach-c: preserved old CCP template (Handlebars-heavy, dynamic container names, secrets rendered at template-time). - docker-compose.yml.hbs: REWRITTEN as a near-mirror of canonical docker-compose.prod.yml. Minimal Handlebars overlay: - Header comment lists {{name}}, {{slug}}, {{composeProject}}. - 5 image refs: ${IMAGE_TAG:-latest} -> {{imageTag}}, so CCP can per-instance override once Phase 1 lands the Instance.imageTag column. All other variation flows through env-var substitution from tenant's .env. Container names are now hardcoded (matching prod), feature flags are deferred to COMPOSE_PROFILES gating (matching prod). Why a rewrite: the old CCP template and prod compose used fundamentally different conventions (dynamic vs hardcoded names, render-time vs substitute-time secrets, Handlebars vs profiles gating). Sync-by-addition couldn't reconcile them. The rewrite makes Approach C re-render safe for the install.sh-installed fleet (marcelle, linda, pia and future). docs/SESSION_HANDOFF_2026-05-21.md: full session handoff covering fleet state, Approach B rollout, Approach C plan, and where to start next session. force-added because /docs is gitignored (same precedent as docs/SESSION_HANDOFF_2026-05-20.md from prior session). Phase 0 remaining work (next session): - Audit env.hbs against new compose env-var expectations - Sync static config files (nginx/, configs/prometheus/, etc.) - Build api/scripts/render-for-instance.ts harness - Iterate template until rendered output is per-instance-only diff against marcelle/linda/pia actual compose. Then Phases 1-6 per plan in subsequent sessions (~11-14 hours total). Bunker Admin --- .../templates/docker-compose.yml.hbs | 2185 ++++++++++------- ...r-compose.yml.hbs.OLD-style-pre-approach-c | 1091 ++++++++ docs/SESSION_HANDOFF_2026-05-21.md | 169 ++ 3 files changed, 2559 insertions(+), 886 deletions(-) create mode 100644 changemaker-control-panel/templates/docker-compose.yml.hbs.OLD-style-pre-approach-c create mode 100644 docs/SESSION_HANDOFF_2026-05-21.md diff --git a/changemaker-control-panel/templates/docker-compose.yml.hbs b/changemaker-control-panel/templates/docker-compose.yml.hbs index 0803b72..6f9fa5b 100644 --- a/changemaker-control-panel/templates/docker-compose.yml.hbs +++ b/changemaker-control-panel/templates/docker-compose.yml.hbs @@ -1,44 +1,421 @@ -# Changemaker Lite — Instance: {{name}} +############################################################################### +############################################################################### +# Changemaker Lite v2 — Tenant compose (CCP template) +# Instance: {{name}} ({{slug}}) # Compose project: {{composeProject}} -# Generated by CCP +# +# This template is a near-mirror of changemaker.lite/docker-compose.prod.yml. +# Approach C (CCP-driven release upgrade) renders this against the tenant's +# context and writes the result to the tenant's filesystem. Variation is +# almost entirely env-var driven (.env file); only image-tag overrides for +# the four core CCP-built images use Handlebars ({{imageTag}}). +# +# To keep this template in sync with canonical docker-compose.prod.yml: +# - When a new service is added to changemaker.lite/docker-compose.prod.yml, +# copy the same block here. Use Handlebars only where per-instance +# variation is needed (currently only {{imageTag}} for the 5 CCP images). +############################################################################### +############################################################################### + +x-logging: &default-logging + driver: "json-file" + options: + max-size: "10m" + max-file: "3" services: - # ─── Core Infrastructure ─────────────────────────────────── + # ========================================================================= + # V2 CORE SERVICES + # ========================================================================= - v2-postgres: - image: {{registryUrl}}/postgres:16-alpine - container_name: {{containerPrefix}}-postgres + # Unified Express.js API + api: + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/changemaker-api:{{imageTag}} + container_name: changemaker-v2-api restart: unless-stopped - environment: - POSTGRES_USER: changemaker - POSTGRES_PASSWORD: "{{secrets.postgresPassword}}" - POSTGRES_DB: changemaker_v2 - volumes: - - {{containerPrefix}}-postgres-data:/var/lib/postgresql/data - - ./api/prisma/init-nocodb-db.sh:/docker-entrypoint-initdb.d/10-init-nocodb.sh:ro - - ./api/prisma/init-gancio-db.sh:/docker-entrypoint-initdb.d/20-init-gancio.sh:ro - - ./api/prisma/init-gitea-db.sh:/docker-entrypoint-initdb.d/30-init-gitea.sh:ro ports: - - "127.0.0.1:{{ports.postgres}}:5432" - networks: - - {{networkName}} + - "127.0.0.1:${API_PORT:-4000}:4000" + - "127.0.0.1:${LISTMONK_PROXY_PORT:-9002}:9002" healthcheck: - test: ["CMD-SHELL", "pg_isready -U changemaker -d changemaker_v2"] + test: ["CMD", "wget", "-q", "--spider", "http://localhost:4000/api/health"] + interval: 15s + timeout: 5s + retries: 3 + start_period: 120s + environment: + - NODE_ENV=${NODE_ENV:-production} + - PORT=4000 + - LOG_DIR=/app/logs + - DATABASE_URL=postgresql://${V2_POSTGRES_USER:-changemaker}:${V2_POSTGRES_PASSWORD:?V2_POSTGRES_PASSWORD must be set in .env}@changemaker-v2-postgres:5432/${V2_POSTGRES_DB:-changemaker_v2} + - REDIS_URL=redis://:${REDIS_PASSWORD}@redis-changemaker:6379 + - JWT_ACCESS_SECRET=${JWT_ACCESS_SECRET} + - JWT_REFRESH_SECRET=${JWT_REFRESH_SECRET} + - JWT_INVITE_SECRET=${JWT_INVITE_SECRET} + # Updated 2026-04-12 (P2-2, P2-3): these secrets are now REQUIRED (Zod + # .min(32)) — empty fallback removed. Refresh expiry default 7d → 24h. + - GITEA_SSO_SECRET=${GITEA_SSO_SECRET} + - SERVICE_PASSWORD_SALT=${SERVICE_PASSWORD_SALT} + - JWT_ACCESS_EXPIRY=${JWT_ACCESS_EXPIRY:-15m} + - JWT_REFRESH_EXPIRY=${JWT_REFRESH_EXPIRY:-24h} + - ENCRYPTION_KEY=${ENCRYPTION_KEY} + - INITIAL_ADMIN_EMAIL=${INITIAL_ADMIN_EMAIL:-admin@cmlite.org} + - INITIAL_ADMIN_PASSWORD=${INITIAL_ADMIN_PASSWORD:?INITIAL_ADMIN_PASSWORD must be set in .env} + - SMTP_HOST=${SMTP_HOST:-mailhog-changemaker} + - SMTP_PORT=${SMTP_PORT:-1025} + - SMTP_USER=${SMTP_USER:-} + - SMTP_PASS=${SMTP_PASS:-} + - SMTP_FROM=${SMTP_FROM:-noreply@cmlite.org} + - EMAIL_TEST_MODE=${EMAIL_TEST_MODE:-false} + - LISTMONK_URL=http://listmonk-app:9000 + - LISTMONK_ADMIN_USER=${LISTMONK_ADMIN_USER:-admin} + - LISTMONK_ADMIN_PASSWORD=${LISTMONK_ADMIN_PASSWORD:-} + - LISTMONK_SYNC_ENABLED=${LISTMONK_SYNC_ENABLED:-false} + - LISTMONK_PROXY_PORT=${LISTMONK_PROXY_PORT:-9002} + - REPRESENT_API_URL=${REPRESENT_API_URL:-https://represent.opennorth.ca} + - CORS_ORIGINS=${CORS_ORIGINS:-http://localhost:3000,http://localhost} + - ADMIN_URL=${ADMIN_URL:-http://localhost:3000} + - API_URL=${API_URL:-http://localhost:4000} + - DOMAIN=${DOMAIN:-cmlite.org} + - NAR_DATA_DIR=/data + - PANGOLIN_API_URL=${PANGOLIN_API_URL:-} + - PANGOLIN_API_KEY=${PANGOLIN_API_KEY:-} + - PANGOLIN_ORG_ID=${PANGOLIN_ORG_ID:-} + - PANGOLIN_SITE_ID=${PANGOLIN_SITE_ID:-} + - PANGOLIN_ENDPOINT=${PANGOLIN_ENDPOINT:-} + - PANGOLIN_NEWT_ID=${PANGOLIN_NEWT_ID:-} + - PANGOLIN_NEWT_SECRET=${PANGOLIN_NEWT_SECRET:-} + # NODE_TLS_REJECT_UNAUTHORIZED removed — never disable TLS validation globally + - EXCALIDRAW_URL=${EXCALIDRAW_URL:-http://excalidraw-changemaker:80} + - EXCALIDRAW_PORT=${EXCALIDRAW_PORT:-8090} + - EXCALIDRAW_EMBED_PORT=${EXCALIDRAW_EMBED_PORT:-8886} + - HOMEPAGE_URL=${HOMEPAGE_URL:-http://homepage-changemaker:3000} + - HOMEPAGE_EMBED_PORT=${HOMEPAGE_EMBED_PORT:-8887} + - VAULTWARDEN_URL=${VAULTWARDEN_URL:-http://vaultwarden-changemaker:80} + - VAULTWARDEN_EMBED_PORT=${VAULTWARDEN_EMBED_PORT:-8890} + - ROCKETCHAT_URL=${ROCKETCHAT_URL:-http://rocketchat-changemaker:3000} + - ROCKETCHAT_ADMIN_USER=${ROCKETCHAT_ADMIN_USER:-rcadmin} + - ROCKETCHAT_ADMIN_PASSWORD=${ROCKETCHAT_ADMIN_PASSWORD:?ROCKETCHAT_ADMIN_PASSWORD must be set in .env} + - ROCKETCHAT_EMBED_PORT=${ROCKETCHAT_EMBED_PORT:-8891} + - ENABLE_CHAT=${ENABLE_CHAT:-false} + - GANCIO_URL=${GANCIO_URL:-http://gancio-changemaker:13120} + - GANCIO_EMBED_PORT=${GANCIO_EMBED_PORT:-8892} + - GANCIO_ADMIN_USER=${GANCIO_ADMIN_USER:-admin} + - GANCIO_ADMIN_PASSWORD=${GANCIO_ADMIN_PASSWORD:-} + - GANCIO_SYNC_ENABLED=${GANCIO_SYNC_ENABLED:-false} + # Jitsi Meet (video conferencing) + - ENABLE_MEET=${ENABLE_MEET:-false} + - JITSI_APP_ID=${JITSI_APP_ID:-changemaker} + - JITSI_APP_SECRET=${JITSI_APP_SECRET:-} + - JITSI_URL=${JITSI_URL:-http://jitsi-web-changemaker:80} + - JITSI_EMBED_PORT=${JITSI_EMBED_PORT:-8893} + # Embed ports for iframe embedding without DNS/subdomain (configurable for multi-instance) + - NOCODB_EMBED_PORT=${NOCODB_EMBED_PORT:-8881} + - N8N_EMBED_PORT=${N8N_EMBED_PORT:-8882} + - GITEA_EMBED_PORT=${GITEA_EMBED_PORT:-8883} + - MAILHOG_EMBED_PORT=${MAILHOG_EMBED_PORT:-8884} + - GRAFANA_EMBED_PORT=${GRAFANA_EMBED_PORT:-8894} + - ALERTMANAGER_EMBED_PORT=${ALERTMANAGER_EMBED_PORT:-8895} + # SMS Campaigns (Termux Android Bridge) + - ENABLE_SMS=${ENABLE_SMS:-false} + # Social, People, Analytics (initial defaults; DB authoritative once admin saves) + - ENABLE_SOCIAL=${ENABLE_SOCIAL:-false} + - ENABLE_PEOPLE=${ENABLE_PEOPLE:-false} + - ENABLE_ANALYTICS=${ENABLE_ANALYTICS:-false} + # CCP Agent (remote management) + - ENABLE_CCP_AGENT=${ENABLE_CCP_AGENT:-false} + - CCP_URL=${CCP_URL:-} + - CCP_AGENT_URL=${CCP_AGENT_URL:-} + - COMPOSE_PROFILES=${COMPOSE_PROFILES:-} + - TERMUX_API_URL=${TERMUX_API_URL:-http://10.0.0.193:5001} + - TERMUX_API_KEY=${TERMUX_API_KEY:-} + - SMS_DELAY_BETWEEN_MS=${SMS_DELAY_BETWEEN_MS:-3000} + - SMS_MAX_RETRIES=${SMS_MAX_RETRIES:-3} + - SMS_RESPONSE_SYNC_INTERVAL_MS=${SMS_RESPONSE_SYNC_INTERVAL_MS:-30000} + - SMS_DEVICE_MONITOR_INTERVAL_MS=${SMS_DEVICE_MONITOR_INTERVAL_MS:-30000} + # Docker container management (socket proxy, network discovery, service names) + - DOCKER_PROXY_URL=${DOCKER_PROXY_URL:-http://docker-socket-proxy:2375} + - DOCKER_NETWORK_NAME=${DOCKER_NETWORK_NAME:-changemaker-lite} + - NEWT_CONTAINER_NAME=${NEWT_CONTAINER_NAME:-newt-changemaker} + - NEWT_COMPOSE_SERVICE=${NEWT_COMPOSE_SERVICE:-newt} + # Container Registry + - GITEA_REGISTRY=${GITEA_REGISTRY:-gitea.bnkops.com/admin} + - GITEA_REGISTRY_USER=${GITEA_REGISTRY_USER:-} + - GITEA_REGISTRY_PASS=${GITEA_REGISTRY_PASS:-} + # Gitea (docs comments, version history, auto-setup) + - GITEA_URL=${GITEA_URL:-http://gitea-changemaker:3000} + - GITEA_API_TOKEN=${GITEA_API_TOKEN:-} + - GITEA_ADMIN_PASSWORD=${GITEA_ADMIN_PASSWORD:-${INITIAL_ADMIN_PASSWORD}} + - GITEA_DOCS_REPO=${GITEA_DOCS_REPO:-admin/changemaker.lite} + - GITEA_DOCS_PREFIX=${GITEA_DOCS_PREFIX:-mkdocs/docs} + - GITEA_DOCS_BRANCH=${GITEA_DOCS_BRANCH:-v2} + # GeoIP (MaxMind GeoLite2) + - MAXMIND_ACCOUNT_ID=${MAXMIND_ACCOUNT_ID:-} + - MAXMIND_LICENSE_KEY=${MAXMIND_LICENSE_KEY:-} + volumes: + - ./assets/uploads:/app/uploads + - ./mkdocs:/mkdocs:rw + - ./data:/data:ro + - ./data/geoip:/data/geoip:rw + - ./data/upgrade:/app/upgrade:rw + - ./configs:/app/configs:ro + - ./logs/api:/app/logs + - ./.env:/.env:rw # Pangolin setup auto-writes credentials + deploy: + resources: + limits: + cpus: '2' + memory: 1G + reservations: + cpus: '0.25' + memory: 256M + depends_on: + v2-postgres: + condition: service_healthy + redis: + condition: service_healthy + logging: *default-logging + networks: + - changemaker-lite + + # Fastify Media API (Microservice for Media Management) + media-api: + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/changemaker-media-api:{{imageTag}} + container_name: changemaker-media-api + restart: unless-stopped + ports: + - "127.0.0.1:${MEDIA_API_PORT:-4100}:4100" + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:4100/health"] + interval: 15s + timeout: 5s + retries: 3 + start_period: 30s + environment: + - NODE_ENV=${NODE_ENV:-production} + - MEDIA_API_PORT=${MEDIA_API_PORT:-4100} + - DATABASE_URL=postgresql://${V2_POSTGRES_USER:-changemaker}:${V2_POSTGRES_PASSWORD:?V2_POSTGRES_PASSWORD must be set in .env}@changemaker-v2-postgres:5432/${V2_POSTGRES_DB:-changemaker_v2} + - REDIS_URL=redis://:${REDIS_PASSWORD}@redis-changemaker:6379 + - JWT_ACCESS_SECRET=${JWT_ACCESS_SECRET} + - JWT_REFRESH_SECRET=${JWT_REFRESH_SECRET} + - JWT_INVITE_SECRET=${JWT_INVITE_SECRET} + # Added 2026-04-12 (P2-2): media-api shares the api's env schema; both + # require these secrets to boot. + - GITEA_SSO_SECRET=${GITEA_SSO_SECRET} + - SERVICE_PASSWORD_SALT=${SERVICE_PASSWORD_SALT} + - JWT_ACCESS_EXPIRY=${JWT_ACCESS_EXPIRY:-15m} + - JWT_REFRESH_EXPIRY=${JWT_REFRESH_EXPIRY:-24h} + - CORS_ORIGINS=${CORS_ORIGINS:-http://localhost:3000,http://localhost:3100} + - ENCRYPTION_KEY=${ENCRYPTION_KEY} + - ENABLE_MEDIA_FEATURES=${ENABLE_MEDIA_FEATURES:-true} + - ENABLE_HLS_TRANSCODE=${ENABLE_HLS_TRANSCODE:-false} + - MEDIA_ROOT=/media/local + - MEDIA_UPLOADS=/media/uploads + - MAX_UPLOAD_SIZE_GB=${MAX_UPLOAD_SIZE_GB:-10} + - INITIAL_ADMIN_PASSWORD=${INITIAL_ADMIN_PASSWORD} + volumes: + - ${MEDIA_ROOT:-./media}:/media:ro + - ${MEDIA_ROOT:-./media}/local/inbox:/media/local/inbox:rw + - ${MEDIA_ROOT:-./media}/local/thumbnails:/media/local/thumbnails:rw + - ${MEDIA_ROOT:-./media}/local/photos:/media/local/photos:rw + - ${MEDIA_ROOT:-./media}/local/documents:/media/local/documents:rw + - ${MEDIA_ROOT:-./media}/local/hls:/media/local/hls:rw + - ${MEDIA_ROOT:-./media}/public:/media/public:rw + deploy: + resources: + # Bumped to 4/2G for in-process HLS FFmpeg transcoding headroom. + limits: + cpus: '4' + memory: 2G + reservations: + cpus: '0.5' + memory: 512M + depends_on: + v2-postgres: + condition: service_healthy + redis: + condition: service_healthy + logging: *default-logging + networks: + - changemaker-lite + + # React Admin GUI (Vite dev server) + admin: + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/changemaker-admin:{{imageTag}} + container_name: changemaker-v2-admin + restart: unless-stopped + ports: + - "127.0.0.1:${ADMIN_PORT:-3000}:3000" + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:3000/"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 60s + environment: + - DOMAIN=${DOMAIN:-cmlite.org} + - NODE_ENV=${NODE_ENV:-production} + - VITE_API_URL=http://changemaker-v2-api:4000 + - VITE_MEDIA_API_URL=${VITE_MEDIA_API_URL:-http://changemaker-media-api:4100} + - VITE_MKDOCS_URL=http://mkdocs-changemaker:8000 + - VITE_DOMAIN=${DOMAIN:-cmlite.org} + - VITE_MKDOCS_SITE_PORT=${MKDOCS_SITE_SERVER_PORT:-4004} + depends_on: + api: + condition: service_healthy + logging: *default-logging + networks: + - changemaker-lite + + # PostgreSQL 16 (v2 database) + v2-postgres: + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/postgres:16-alpine + container_name: changemaker-v2-postgres + restart: unless-stopped + ports: + - "127.0.0.1:${V2_POSTGRES_PORT:-5433}:5432" + environment: + POSTGRES_USER: ${V2_POSTGRES_USER:-changemaker} + POSTGRES_PASSWORD: ${V2_POSTGRES_PASSWORD:?V2_POSTGRES_PASSWORD must be set in .env} + POSTGRES_DB: ${V2_POSTGRES_DB:-changemaker_v2} + volumes: + - v2-postgres-data:/var/lib/postgresql/data + - ./scripts/init-nocodb-db.sh:/docker-entrypoint-initdb.d/init-nocodb-db.sh:ro + - ./scripts/init-gancio-db.sh:/docker-entrypoint-initdb.d/init-gancio-db.sh:ro + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${V2_POSTGRES_USER:-changemaker}"] interval: 10s timeout: 5s retries: 5 - - redis: - image: {{registryUrl}}/redis:7-alpine - container_name: {{containerPrefix}}-redis - restart: unless-stopped - command: "redis-server --appendonly yes --maxmemory 512mb --maxmemory-policy noeviction --requirepass {{secrets.redisPassword}}" - volumes: - - {{containerPrefix}}-redis-data:/data + logging: *default-logging networks: - - {{networkName}} + - changemaker-lite + + # Nginx reverse proxy + nginx: + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/changemaker-nginx:{{imageTag}} + container_name: changemaker-v2-nginx + restart: unless-stopped + ports: + - "${NGINX_HTTP_PORT:-80}:80" + - "${NGINX_HTTPS_PORT:-443}:443" + # Embed proxy ports — configurable via .env to avoid conflicts in multi-instance deployments + - "127.0.0.1:${NOCODB_EMBED_PORT:-8881}:${NOCODB_EMBED_PORT:-8881}" + - "127.0.0.1:${N8N_EMBED_PORT:-8882}:${N8N_EMBED_PORT:-8882}" + - "127.0.0.1:${GITEA_EMBED_PORT:-8883}:${GITEA_EMBED_PORT:-8883}" + - "127.0.0.1:${MAILHOG_EMBED_PORT:-8884}:${MAILHOG_EMBED_PORT:-8884}" + - "127.0.0.1:${MINI_QR_EMBED_PORT:-8885}:${MINI_QR_EMBED_PORT:-8885}" + - "127.0.0.1:${EXCALIDRAW_EMBED_PORT:-8886}:${EXCALIDRAW_EMBED_PORT:-8886}" + - "127.0.0.1:${HOMEPAGE_EMBED_PORT:-8887}:${HOMEPAGE_EMBED_PORT:-8887}" + - "127.0.0.1:${VAULTWARDEN_EMBED_PORT:-8890}:${VAULTWARDEN_EMBED_PORT:-8890}" + - "127.0.0.1:${ROCKETCHAT_EMBED_PORT:-8891}:${ROCKETCHAT_EMBED_PORT:-8891}" + - "127.0.0.1:${GANCIO_EMBED_PORT:-8892}:${GANCIO_EMBED_PORT:-8892}" + - "127.0.0.1:${JITSI_EMBED_PORT:-8893}:${JITSI_EMBED_PORT:-8893}" + - "127.0.0.1:${GRAFANA_EMBED_PORT:-8894}:${GRAFANA_EMBED_PORT:-8894}" + - "127.0.0.1:${ALERTMANAGER_EMBED_PORT:-8895}:${ALERTMANAGER_EMBED_PORT:-8895}" healthcheck: - test: ["CMD", "redis-cli", "-a", "{{secrets.redisPassword}}", "ping"] + test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:80/"] + interval: 30s + timeout: 5s + retries: 3 + environment: + - DOMAIN=${DOMAIN:-cmlite.org} + - PANGOLIN_SITE_ID=${PANGOLIN_SITE_ID:-} + # Embed proxy ports (passed to envsubst for nginx template processing) + - NOCODB_EMBED_PORT=${NOCODB_EMBED_PORT:-8881} + - N8N_EMBED_PORT=${N8N_EMBED_PORT:-8882} + - GITEA_EMBED_PORT=${GITEA_EMBED_PORT:-8883} + - MAILHOG_EMBED_PORT=${MAILHOG_EMBED_PORT:-8884} + - MINI_QR_EMBED_PORT=${MINI_QR_EMBED_PORT:-8885} + - EXCALIDRAW_EMBED_PORT=${EXCALIDRAW_EMBED_PORT:-8886} + - HOMEPAGE_EMBED_PORT=${HOMEPAGE_EMBED_PORT:-8887} + - VAULTWARDEN_EMBED_PORT=${VAULTWARDEN_EMBED_PORT:-8890} + - ROCKETCHAT_EMBED_PORT=${ROCKETCHAT_EMBED_PORT:-8891} + - GANCIO_EMBED_PORT=${GANCIO_EMBED_PORT:-8892} + - JITSI_EMBED_PORT=${JITSI_EMBED_PORT:-8893} + - GRAFANA_EMBED_PORT=${GRAFANA_EMBED_PORT:-8894} + - ALERTMANAGER_EMBED_PORT=${ALERTMANAGER_EMBED_PORT:-8895} + volumes: + # Note: conf.d is NOT mounted (configs are generated at startup from templates) + - ./public-web:/usr/share/nginx/public-web:ro + - ./configs/pangolin:/etc/pangolin:ro + depends_on: + api: + condition: service_healthy + admin: + condition: service_healthy + logging: *default-logging + networks: + - changemaker-lite + + # NocoDB v2 — pointed at v2 PostgreSQL as read-only data browser + nocodb-v2: + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/nocodb:0.301.3 + container_name: changemaker-v2-nocodb + restart: unless-stopped + ports: + - "127.0.0.1:${NOCODB_V2_PORT:-8091}:8080" + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/api/v1/health"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 30s + environment: + NC_DB: "pg://changemaker-v2-postgres:5432?u=${V2_POSTGRES_USER:-changemaker}&p=${V2_POSTGRES_PASSWORD:?V2_POSTGRES_PASSWORD must be set in .env}&d=nocodb_meta" + NC_ADMIN_EMAIL: ${NC_ADMIN_EMAIL:-admin@cmlite.org} + NC_ADMIN_PASSWORD: ${NC_ADMIN_PASSWORD:?NC_ADMIN_PASSWORD must be set in .env} + NC_PUBLIC_URL: ${NC_PUBLIC_URL:-http://localhost:8091} + volumes: + - nocodb-v2-data:/usr/app/data + depends_on: + v2-postgres: + condition: service_healthy + logging: *default-logging + networks: + - changemaker-lite + + # NocoDB Init — auto-registers changemaker_v2 as a browsable data source + nocodb-init: + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/alpine:3 + container_name: nocodb-init + depends_on: + nocodb-v2: + condition: service_healthy + v2-postgres: + condition: service_healthy + restart: "no" + environment: + NOCODB_URL: http://changemaker-v2-nocodb:8080 + NC_ADMIN_EMAIL: ${NC_ADMIN_EMAIL:-admin@cmlite.org} + NC_ADMIN_PASSWORD: ${NC_ADMIN_PASSWORD:?NC_ADMIN_PASSWORD must be set in .env} + DB_HOST: changemaker-v2-postgres + DB_PORT: "5432" + DB_USER: ${V2_POSTGRES_USER:-changemaker} + DB_PASSWORD: ${V2_POSTGRES_PASSWORD:?V2_POSTGRES_PASSWORD must be set in .env} + DB_NAME: ${V2_POSTGRES_DB:-changemaker_v2} + volumes: + - ./scripts/nocodb-init.sh:/init.sh:ro + entrypoint: ["/bin/sh", "/init.sh"] + logging: *default-logging + networks: + - changemaker-lite + + # ========================================================================= + # SHARED INFRASTRUCTURE (kept from v1) + # ========================================================================= + + # Shared Redis — sessions, BullMQ queues, cache + redis: + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/redis:7-alpine + container_name: redis-changemaker + command: redis-server --appendonly yes --maxmemory 512mb --maxmemory-policy noeviction --requirepass "${REDIS_PASSWORD}" + ports: + - "127.0.0.1:6379:6379" + volumes: + - redis-data:/data + restart: always + healthcheck: + test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"] interval: 10s timeout: 5s retries: 5 @@ -50,328 +427,94 @@ services: reservations: cpus: '0.25' memory: 256M + networks: + - changemaker-lite logging: driver: "json-file" options: max-size: "5m" max-file: "2" - # ─── Application Services ────────────────────────────────── - - api: -{{#if useRegistry}} - image: {{registryUrl}}/changemaker-api:{{imageTag}} -{{else}} - build: - context: ./api - dockerfile: Dockerfile - target: development -{{/if}} - container_name: {{containerPrefix}}-api - restart: unless-stopped - depends_on: - v2-postgres: - condition: service_healthy - redis: - condition: service_healthy - env_file: .env - environment: - DATABASE_URL: "postgresql://changemaker:{{secrets.postgresPassword}}@{{containerPrefix}}-postgres:5432/changemaker_v2" - REDIS_URL: "redis://:{{secrets.redisPassword}}@{{containerPrefix}}-redis:6379" - PORT: "4000" - NAR_DATA_DIR: /data - LISTMONK_URL: http://{{containerPrefix}}-listmonk:9000 - ADMIN_URL: https://app.{{domain}} - API_URL: https://api.{{domain}} -{{#if enableGancio}} - GANCIO_URL: http://{{containerPrefix}}-gancio:13120 -{{/if}} - ENABLE_MEET: "{{#if enableMeet}}true{{else}}false{{/if}}" - ENABLE_SMS: "{{#if enableSms}}true{{else}}false{{/if}}" - ENABLE_SOCIAL: "{{#if enableSocial}}true{{else}}false{{/if}}" - ENABLE_PEOPLE: "{{#if enablePeople}}true{{else}}false{{/if}}" -{{#if enableMeet}} - JITSI_APP_ID: changemaker - JITSI_APP_SECRET: "{{secrets.jitsiAppSecret}}" - JITSI_URL: http://{{containerPrefix}}-jitsi-web:80 -{{/if}} -{{#if enableChat}} - ROCKETCHAT_URL: http://{{containerPrefix}}-rocketchat:3000 -{{/if}} - ports: - - "{{ports.api}}:4000" -{{#if enableListmonk}} - - "9002:9002" -{{/if}} - volumes: - - ./assets/uploads:/app/uploads - - ./mkdocs:/mkdocs:rw - - ./data:/data:ro - - ./data/upgrade:/app/upgrade:rw - - ./configs:/app/configs:ro - networks: - - {{networkName}} - healthcheck: - test: ["CMD", "wget", "-q", "--spider", "http://localhost:4000/api/health"] - interval: 15s - timeout: 5s - retries: 3 - start_period: 30s - deploy: - resources: - limits: - cpus: '2' - memory: 1G - reservations: - cpus: '0.25' - memory: 256M - - admin: -{{#if useRegistry}} - image: {{registryUrl}}/changemaker-admin:{{imageTag}} -{{else}} - build: - context: ./admin - target: development -{{/if}} - container_name: {{containerPrefix}}-admin - restart: unless-stopped - depends_on: - - api - environment: - DOMAIN: {{domain}} - NODE_ENV: production - VITE_API_URL: http://{{containerPrefix}}-api:4000 - VITE_MKDOCS_URL: http://{{containerPrefix}}-mkdocs:8000 - VITE_DOMAIN: {{domain}} - VITE_MKDOCS_SITE_PORT: "{{math ports.embed "+" 14}}" -{{#if enableMedia}} - VITE_MEDIA_API_URL: http://{{containerPrefix}}-media-api:4100 -{{/if}} - ports: - - "{{ports.admin}}:3000" - networks: - - {{networkName}} - healthcheck: - test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:3000/"] - interval: 30s - timeout: 5s - retries: 3 - start_period: 20s - -{{#if enableMedia}} - media-api: -{{#if useRegistry}} - image: {{registryUrl}}/changemaker-media-api:{{imageTag}} -{{else}} - build: - context: ./api - dockerfile: Dockerfile.media - target: development -{{/if}} - container_name: {{containerPrefix}}-media-api - restart: unless-stopped - depends_on: - v2-postgres: - condition: service_healthy - redis: - condition: service_healthy - env_file: .env - environment: - DATABASE_URL: "postgresql://changemaker:{{secrets.postgresPassword}}@{{containerPrefix}}-postgres:5432/changemaker_v2" - REDIS_URL: "redis://:{{secrets.redisPassword}}@{{containerPrefix}}-redis:6379" - MEDIA_API_PORT: "4100" - CORS_ORIGINS: https://app.{{domain}},http://localhost:{{ports.admin}} - ENABLE_MEDIA_FEATURES: "true" - MEDIA_ROOT: /media/local - MEDIA_UPLOADS: /media/uploads - volumes: - - ./media:/media:ro - - ./media/local/inbox:/media/local/inbox:rw - - ./media/local/thumbnails:/media/local/thumbnails:rw - - ./media/local/photos:/media/local/photos:rw - - ./media/public:/media/public:rw - networks: - - {{networkName}} - healthcheck: - test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:4100/health"] - interval: 15s - timeout: 5s - retries: 3 - start_period: 30s - deploy: - resources: - limits: - cpus: '2' - memory: 1G - reservations: - cpus: '0.25' - memory: 256M -{{/if}} - - # ─── Reverse Proxy ───────────────────────────────────────── - - nginx: - image: {{registryUrl}}/nginx:alpine - container_name: {{containerPrefix}}-nginx - restart: unless-stopped - depends_on: - - api - - admin - volumes: - - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro - - ./nginx/conf.d:/etc/nginx/conf.d:ro - ports: - - "{{ports.nginx}}:80" - - "{{math ports.embed "+" 0}}:8881" # NocoDB embed proxy - - "{{math ports.embed "+" 1}}:8882" # n8n embed proxy - - "{{math ports.embed "+" 2}}:8883" # Gitea embed proxy - - "{{math ports.embed "+" 3}}:8884" # MailHog embed proxy - - "{{math ports.embed "+" 4}}:8885" # Mini QR embed proxy - - "{{math ports.embed "+" 5}}:8886" # Excalidraw embed proxy - - "{{math ports.embed "+" 6}}:8887" # Homepage embed proxy - - "{{math ports.embed "+" 7}}:8888" # Code Server embed proxy - - "{{math ports.embed "+" 8}}:8889" # MkDocs embed proxy - - "{{math ports.embed "+" 9}}:8890" # Vaultwarden embed proxy - - "{{math ports.embed "+" 10}}:8891" # Rocket.Chat embed proxy - - "{{math ports.embed "+" 11}}:8892" # Gancio embed proxy - - "{{math ports.embed "+" 12}}:8893" # Grafana embed proxy - - "{{math ports.embed "+" 13}}:8894" # Listmonk embed proxy - - "{{math ports.embed "+" 14}}:8895" # MkDocs site embed proxy - - "{{math ports.embed "+" 15}}:8896" # Jitsi Meet embed proxy - networks: - - {{networkName}} - healthcheck: - test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:80/"] - interval: 30s - timeout: 5s - retries: 3 - - # ─── Supporting Services ─────────────────────────────────── - - nocodb-v2: - image: {{registryUrl}}/nocodb:0.301.3 - container_name: {{containerPrefix}}-nocodb - restart: unless-stopped - depends_on: - v2-postgres: - condition: service_healthy - environment: - NC_DB: "pg://{{containerPrefix}}-postgres:5432?u=changemaker&p={{secrets.postgresPassword}}&d=nocodb_meta" - NC_ADMIN_EMAIL: "{{secrets.adminEmail}}" - NC_ADMIN_PASSWORD: "{{secrets.nocodbAdminPassword}}" - volumes: - - {{containerPrefix}}-nocodb-data:/usr/app/data - networks: - - {{networkName}} - - mailhog: - image: {{registryUrl}}/mailhog:v1.0.1 - container_name: {{containerPrefix}}-mailhog - restart: unless-stopped - networks: - - {{networkName}} - logging: - driver: "json-file" - options: - max-size: "5m" - max-file: "2" - - mkdocs: - image: {{registryUrl}}/mkdocs-material:latest - container_name: {{containerPrefix}}-mkdocs - restart: unless-stopped - volumes: - - ./mkdocs:/docs:rw - - ./assets/images:/docs/assets/images:rw - user: "1000:1000" - environment: - SITE_URL: https://{{domain}} - ADMIN_PORT: "{{ports.admin}}" - ADMIN_URL: https://app.{{domain}} - BASE_DOMAIN: https://{{domain}} - API_URL: https://api.{{domain}} - API_PORT: "{{ports.api}}" -{{#if enableMedia}} - MEDIA_API_PUBLIC_URL: https://media.{{domain}} - MEDIA_API_PORT: "4100" -{{/if}} -{{#if enableGancio}} - GANCIO_URL: http://{{containerPrefix}}-gancio:13120 - GANCIO_PORT: "8092" -{{/if}} - command: serve --dev-addr=0.0.0.0:8000 --watch-theme --livereload - networks: - - {{networkName}} - -{{#if enableListmonk}} - listmonk-db: - image: {{registryUrl}}/postgres:17-alpine - container_name: {{containerPrefix}}-listmonk-db - restart: unless-stopped - environment: - POSTGRES_USER: listmonk - POSTGRES_PASSWORD: "{{secrets.listmonkAdminPassword}}" - POSTGRES_DB: listmonk - volumes: - - {{containerPrefix}}-listmonk-data:/var/lib/postgresql/data - networks: - - {{networkName}} - healthcheck: - test: ["CMD-SHELL", "pg_isready -U listmonk"] - interval: 10s - timeout: 5s - retries: 6 - + # Listmonk — Email marketing (kept as Docker image, controlled via REST API) listmonk-app: - image: {{registryUrl}}/listmonk:v6.0.0 - container_name: {{containerPrefix}}-listmonk + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/listmonk:v6.0.0 + container_name: listmonk-app restart: unless-stopped - depends_on: - listmonk-db: - condition: service_healthy - command: [sh, -c, "./listmonk --install --idempotent --yes --config '' && ./listmonk --upgrade --yes --config '' && ./listmonk --config ''"] - environment: - LISTMONK_app__address: "0.0.0.0:9000" - LISTMONK_db__host: {{containerPrefix}}-listmonk-db - LISTMONK_db__port: "5432" - LISTMONK_db__user: listmonk - LISTMONK_db__password: "{{secrets.listmonkAdminPassword}}" - LISTMONK_db__database: listmonk - LISTMONK_db__ssl_mode: disable - TZ: Etc/UTC - LISTMONK_ADMIN_USER: admin - LISTMONK_ADMIN_PASSWORD: "{{secrets.listmonkAdminPassword}}" - volumes: - - ./assets/uploads:/listmonk/uploads:rw - networks: - - {{networkName}} + ports: + - "127.0.0.1:${LISTMONK_PORT:-9001}:9000" healthcheck: test: ["CMD", "wget", "-q", "--spider", "http://localhost:9000/"] interval: 30s timeout: 5s retries: 3 start_period: 30s + depends_on: + listmonk-db: + condition: service_healthy + command: [sh, -c, "./listmonk --install --idempotent --yes --config '' && ./listmonk --upgrade --yes --config '' && ./listmonk --config ''"] + environment: + LISTMONK_app__address: 0.0.0.0:9000 + LISTMONK_db__user: ${LISTMONK_DB_USER:-listmonk} + LISTMONK_db__password: ${LISTMONK_DB_PASSWORD:?LISTMONK_DB_PASSWORD must be set in .env} + LISTMONK_db__database: ${LISTMONK_DB_NAME:-listmonk} + LISTMONK_db__host: listmonk-db + LISTMONK_db__port: 5432 + LISTMONK_db__ssl_mode: disable + TZ: Etc/UTC + LISTMONK_ADMIN_USER: ${LISTMONK_WEB_ADMIN_USER:-admin} + LISTMONK_ADMIN_PASSWORD: ${LISTMONK_WEB_ADMIN_PASSWORD:-} + volumes: + - ./assets/uploads:/listmonk/uploads:rw + logging: *default-logging + networks: + - changemaker-lite + listmonk-db: + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/postgres:17-alpine + container_name: listmonk-db + restart: unless-stopped + ports: + - "127.0.0.1:${LISTMONK_DB_PORT:-5434}:5432" + environment: + POSTGRES_USER: ${LISTMONK_DB_USER:-listmonk} + POSTGRES_PASSWORD: ${LISTMONK_DB_PASSWORD:?LISTMONK_DB_PASSWORD must be set in .env} + POSTGRES_DB: ${LISTMONK_DB_NAME:-listmonk} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${LISTMONK_DB_USER:-listmonk}"] + interval: 10s + timeout: 5s + retries: 6 + volumes: + - listmonk-data:/var/lib/postgresql/data + logging: *default-logging + networks: + - changemaker-lite + + # One-shot: creates the Listmonk API user for V2 integration after tables exist. + # Safe to re-run (upserts). Exits 0 on success. Set LISTMONK_API_TOKEN in .env. listmonk-init: - image: {{registryUrl}}/postgres:17-alpine - container_name: {{containerPrefix}}-listmonk-init + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/postgres:17-alpine + container_name: listmonk-init depends_on: listmonk-app: - condition: service_started + condition: service_healthy restart: "no" environment: - PGPASSWORD: "{{secrets.listmonkAdminPassword}}" - LISTMONK_API_USER: v2-api - LISTMONK_API_TOKEN: "{{secrets.listmonkApiToken}}" - LISTMONK_SMTP_HOST: {{containerPrefix}}-mailhog - LISTMONK_SMTP_PORT: "1025" + PGPASSWORD: ${LISTMONK_DB_PASSWORD:?LISTMONK_DB_PASSWORD must be set in .env} + LISTMONK_API_USER: ${LISTMONK_API_USER:-v2-api} + LISTMONK_API_TOKEN: ${LISTMONK_API_TOKEN:-} + LISTMONK_SMTP_HOST: ${LISTMONK_SMTP_HOST:-mailhog-changemaker} + LISTMONK_SMTP_PORT: ${LISTMONK_SMTP_PORT:-1025} + LISTMONK_SMTP_USER: ${LISTMONK_SMTP_USER:-} + LISTMONK_SMTP_PASSWORD: ${LISTMONK_SMTP_PASSWORD:-} + LISTMONK_SMTP_TLS_TYPE: ${LISTMONK_SMTP_TLS_TYPE:-none} + LISTMONK_SMTP_FROM: ${LISTMONK_SMTP_FROM:-Changemaker Lite } entrypoint: ["/bin/sh", "-c"] command: - | echo "[listmonk-init] Waiting for Listmonk tables..." for i in $$(seq 1 30); do - if psql -h {{containerPrefix}}-listmonk-db -U listmonk -d listmonk -c "SELECT 1 FROM users LIMIT 1" >/dev/null 2>&1; then + if psql -h listmonk-db -U ${LISTMONK_DB_USER:-listmonk} -d ${LISTMONK_DB_NAME:-listmonk} -c "SELECT 1 FROM users LIMIT 1" >/dev/null 2>&1; then break fi sleep 2 @@ -379,7 +522,7 @@ services: if [ -n "$$LISTMONK_API_TOKEN" ]; then echo "[listmonk-init] Upserting API user '$$LISTMONK_API_USER'..." - psql -h {{containerPrefix}}-listmonk-db -U listmonk -d listmonk -q < /gancio-data/config.json <<'EOF' - { - "baseurl": "https://events.{{domain}}", - "title": "Events", - "description": "Community Events", - "server": { "host": "0.0.0.0", "port": 13120 }, - "db": { - "dialect": "postgres", - "host": "{{containerPrefix}}-postgres", - "port": 5432, - "database": "gancio", - "username": "changemaker", - "password": "{{secrets.postgresPassword}}" - } - } - EOF - echo "[gancio-config] config.json created" - else - echo "[gancio-config] config.json already exists, skipping" - fi - restart: "no" - networks: - - {{networkName}} - - gancio: - image: {{registryUrl}}/gancio:1.28.2 - container_name: {{containerPrefix}}-gancio - restart: unless-stopped - depends_on: - v2-postgres: - condition: service_healthy - gancio-config: - condition: service_completed_successfully - environment: - GANCIO_DATA: /home/node/data - NODE_ENV: production - GANCIO_DB_DIALECT: postgres - GANCIO_DB_HOST: {{containerPrefix}}-postgres - GANCIO_DB_PORT: "5432" - GANCIO_DB_DATABASE: gancio - GANCIO_DB_USERNAME: changemaker - GANCIO_DB_PASSWORD: "{{secrets.postgresPassword}}" - server__baseurl: https://events.{{domain}} - volumes: - - {{containerPrefix}}-gancio-data:/home/node/data - networks: - - {{networkName}} - healthcheck: - test: ["CMD", "node", "-e", "require('http').get('http://localhost:13120/', r => process.exit(r.statusCode < 400 ? 0 : 1)).on('error', () => process.exit(1))"] - interval: 30s - timeout: 10s - retries: 5 - start_period: 60s - - # Gancio post-start seeder — seeds theme settings after Gancio creates its tables - gancio-init: - image: {{registryUrl}}/postgres:16-alpine - container_name: {{containerPrefix}}-gancio-init - depends_on: - gancio: - condition: service_healthy - environment: - PGHOST: {{containerPrefix}}-postgres - PGUSER: changemaker - PGPASSWORD: "{{secrets.postgresPassword}}" - PGDATABASE: gancio - entrypoint: ["/bin/sh", "-c"] - command: - - | - echo "[gancio-init] Seeding Gancio default theme settings..." - psql -c "INSERT INTO settings (key, value, is_secret, \"createdAt\", \"updatedAt\") VALUES - ('dark_colors', '{\"primary\": \"#FF6E40\", \"error\": \"#FF5252\", \"info\": \"#2196F3\", \"success\": \"#4CAF50\", \"warning\": \"#FB8C00\"}', false, NOW(), NOW()), - ('light_colors', '{\"primary\": \"#FF4500\", \"error\": \"#FF5252\", \"info\": \"#2196F3\", \"success\": \"#4CAF50\", \"warning\": \"#FB8C00\"}', false, NOW(), NOW()) - ON CONFLICT (key) DO NOTHING;" - echo "[gancio-init] Theme settings seeded" - echo "[gancio-init] Done" - restart: "no" - networks: - - {{networkName}} -{{/if}} - -{{#if enableChat}} - nats-rocketchat: - image: {{registryUrl}}/nats:2.11-alpine - container_name: {{containerPrefix}}-nats - restart: unless-stopped - command: --http_port 8222 - networks: - - {{networkName}} - - mongodb-rocketchat: - image: {{registryUrl}}/mongo:6.0 - container_name: {{containerPrefix}}-mongodb - restart: unless-stopped - entrypoint: ["/bin/bash", "-c", "if [ ! -f /data/replica.key ]; then openssl rand -base64 756 > /data/replica.key; fi && chmod 400 /data/replica.key && chown 999:999 /data/replica.key && exec mongod --replSet rs0 --bind_ip_all --auth --keyFile /data/replica.key"] - environment: - MONGO_INITDB_ROOT_USERNAME: "${MONGO_ROOT_USER:-rocketchat}" - MONGO_INITDB_ROOT_PASSWORD: "${MONGO_ROOT_PASSWORD}" - volumes: - - {{containerPrefix}}-mongodb-data:/data/db - networks: - - {{networkName}} - healthcheck: - test: ["CMD", "mongosh", "-u", "${MONGO_ROOT_USER:-rocketchat}", "-p", "${MONGO_ROOT_PASSWORD}", "--authenticationDatabase", "admin", "--quiet", "--eval", "try { rs.status().ok } catch(e) { rs.initiate({_id:'rs0',members:[{_id:0,host:'{{containerPrefix}}-mongodb:27017'}]}).ok }"] - interval: 10s - timeout: 10s - retries: 10 - start_period: 30s - - rocketchat: - image: {{registryUrl}}/rocketchat:7.9.7 - container_name: {{containerPrefix}}-rocketchat - restart: unless-stopped - depends_on: - mongodb-rocketchat: - condition: service_healthy - nats-rocketchat: - condition: service_started - environment: - ROOT_URL: http://chat.{{domain}} - MONGO_URL: mongodb://${MONGO_ROOT_USER:-rocketchat}:${MONGO_ROOT_PASSWORD}@{{containerPrefix}}-mongodb:27017/rocketchat?replicaSet=rs0&authSource=admin - MONGO_OPLOG_URL: mongodb://${MONGO_ROOT_USER:-rocketchat}:${MONGO_ROOT_PASSWORD}@{{containerPrefix}}-mongodb:27017/local?replicaSet=rs0&authSource=admin - TRANSPORTER: monolith+nats://{{containerPrefix}}-nats:4222 - PORT: "3000" - ADMIN_USERNAME: rcadmin - ADMIN_NAME: Admin - ADMIN_EMAIL: "{{secrets.adminEmail}}" - ADMIN_PASS: "{{secrets.rocketchatAdminPassword}}" - CREATE_TOKENS_FOR_USERS: "true" - OVERWRITE_SETTING_Iframe_Integration_send_enable: "true" - OVERWRITE_SETTING_Iframe_Integration_receive_enable: "true" - OVERWRITE_SETTING_Iframe_Integration_receive_origin: http://app.{{domain}},https://app.{{domain}} -{{#if enableMeet}} - OVERWRITE_SETTING_Jitsi_Enabled: "true" - OVERWRITE_SETTING_Jitsi_Domain: meet.{{domain}} - OVERWRITE_SETTING_Jitsi_URL_Room_Prefix: RocketChat - OVERWRITE_SETTING_Jitsi_Enable_Channels: "true" - OVERWRITE_SETTING_Jitsi_Open_New_Window: "false" - OVERWRITE_SETTING_Jitsi_Enabled_TokenAuth: "true" - OVERWRITE_SETTING_Jitsi_Application_ID: changemaker - OVERWRITE_SETTING_Jitsi_Application_Secret: "{{secrets.jitsiAppSecret}}" - OVERWRITE_SETTING_VideoConf_Default_Provider: jitsi -{{/if}} - volumes: - - {{containerPrefix}}-rocketchat-uploads:/app/uploads - networks: - - {{networkName}} - healthcheck: - test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:3000/api/info"] - interval: 30s - timeout: 10s - retries: 10 - start_period: 90s -{{/if}} - -{{#if enablePangolin}} - # ─── Pangolin Tunnel ─────────────────────────────────────── - - newt: - image: {{registryUrl}}/newt:latest - container_name: {{containerPrefix}}-newt - restart: unless-stopped - depends_on: - - nginx - environment: - PANGOLIN_ENDPOINT: "{{pangolin.endpoint}}" - NEWT_ID: "{{pangolin.newtId}}" - NEWT_SECRET: "{{pangolin.newtSecret}}" - networks: - - {{networkName}} -{{/if}} - -{{#if enableMeet}} - # ─── Jitsi Meet (Video Conferencing) ──────────────────── - - jitsi-web: - image: {{registryUrl}}/jitsi-web:stable-9823 - container_name: {{containerPrefix}}-jitsi-web - restart: unless-stopped - depends_on: - - jitsi-prosody - environment: - XMPP_SERVER: {{containerPrefix}}-jitsi-prosody - XMPP_DOMAIN: meet.jitsi - XMPP_AUTH_DOMAIN: auth.meet.jitsi - XMPP_BOSH_URL_BASE: http://{{containerPrefix}}-jitsi-prosody:5280 - XMPP_MUC_DOMAIN: muc.meet.jitsi - PUBLIC_URL: https://meet.{{domain}} - TZ: America/Edmonton - ENABLE_AUTH: "1" - AUTH_TYPE: jwt - JWT_APP_ID: changemaker - JWT_APP_SECRET: "{{secrets.jitsiAppSecret}}" - JWT_ACCEPTED_ISSUERS: changemaker - JWT_ACCEPTED_AUDIENCES: changemaker - volumes: - - {{containerPrefix}}-jitsi-web-config:/config - networks: - - {{networkName}} - - jitsi-prosody: - image: {{registryUrl}}/jitsi-prosody:stable-9823 - container_name: {{containerPrefix}}-jitsi-prosody - restart: unless-stopped - environment: - XMPP_DOMAIN: meet.jitsi - XMPP_AUTH_DOMAIN: auth.meet.jitsi - XMPP_MUC_DOMAIN: muc.meet.jitsi - XMPP_INTERNAL_MUC_DOMAIN: internal-muc.meet.jitsi - XMPP_RECORDER_DOMAIN: recorder.meet.jitsi - XMPP_CROSS_DOMAIN: "true" - JICOFO_AUTH_USER: focus - JICOFO_AUTH_PASSWORD: "{{secrets.jitsiJicofoAuthPassword}}" - JVB_AUTH_USER: jvb - JVB_AUTH_PASSWORD: "{{secrets.jitsiJvbAuthPassword}}" - TZ: America/Edmonton - ENABLE_AUTH: "1" - AUTH_TYPE: jwt - JWT_APP_ID: changemaker - JWT_APP_SECRET: "{{secrets.jitsiAppSecret}}" - JWT_ACCEPTED_ISSUERS: changemaker - JWT_ACCEPTED_AUDIENCES: changemaker - JWT_ALLOW_EMPTY: "0" - volumes: - - {{containerPrefix}}-jitsi-prosody-config:/config - - {{containerPrefix}}-jitsi-prosody-plugins:/prosody-plugins-custom - networks: - - {{networkName}} - - jitsi-jicofo: - image: {{registryUrl}}/jitsi-jicofo:stable-9823 - container_name: {{containerPrefix}}-jitsi-jicofo - restart: unless-stopped - depends_on: - - jitsi-prosody - environment: - XMPP_SERVER: {{containerPrefix}}-jitsi-prosody - XMPP_DOMAIN: meet.jitsi - XMPP_AUTH_DOMAIN: auth.meet.jitsi - XMPP_INTERNAL_MUC_DOMAIN: internal-muc.meet.jitsi - XMPP_MUC_DOMAIN: muc.meet.jitsi - JICOFO_AUTH_USER: focus - JICOFO_AUTH_PASSWORD: "{{secrets.jitsiJicofoAuthPassword}}" - TZ: America/Edmonton - volumes: - - {{containerPrefix}}-jitsi-jicofo-config:/config - networks: - - {{networkName}} - - jitsi-jvb: - image: {{registryUrl}}/jitsi-jvb:stable-9823 - container_name: {{containerPrefix}}-jitsi-jvb - restart: unless-stopped - depends_on: - - jitsi-prosody - environment: - XMPP_SERVER: {{containerPrefix}}-jitsi-prosody - XMPP_DOMAIN: meet.jitsi - XMPP_AUTH_DOMAIN: auth.meet.jitsi - XMPP_INTERNAL_MUC_DOMAIN: internal-muc.meet.jitsi - JVB_AUTH_USER: jvb - JVB_AUTH_PASSWORD: "{{secrets.jitsiJvbAuthPassword}}" - JVB_STUN_SERVERS: meet-jit-si-turnrelay.jitsi.net:443 - JVB_PORT: "10000" - JVB_ADVERTISE_IPS: "{{jvbAdvertiseIp}}" - TZ: America/Edmonton - ports: - - "10000:10000/udp" - volumes: - - {{containerPrefix}}-jitsi-jvb-config:/config - networks: - - {{networkName}} -{{/if}} - - # ─── Always-On Utilities ────────────────────────────────── - - mini-qr: - image: {{registryUrl}}/mini-qr:v0.26.0 - container_name: {{containerPrefix}}-mini-qr - restart: unless-stopped - networks: - - {{networkName}} - - mkdocs-site-server: - image: {{registryUrl}}/nginx:alpine - container_name: {{containerPrefix}}-mkdocs-site - restart: unless-stopped - volumes: - - ./mkdocs/site:/usr/share/nginx/html:ro - networks: - - {{networkName}} - -{{#if enableDevTools}} - # ─── Dev Tools ──────────────────────────────────────────── + # ========================================================================= + # PLATFORM SERVICES (kept from v1) + # ========================================================================= + # Code Server — Browser IDE (LinuxServer upstream, no custom build) code-server: - image: {{registryUrl}}/code-server:latest - container_name: {{containerPrefix}}-code-server + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/code-server:latest + container_name: code-server-changemaker + environment: + - PUID=${USER_ID:-1000} + - PGID=${GROUP_ID:-1000} + - TZ=${TZ:-UTC} + - DEFAULT_WORKSPACE=/config/workspace + volumes: + - ./configs/code-server:/config + - ./nginx:/config/workspace/nginx + - ./configs:/config/workspace/configs + - ./scripts:/config/workspace/scripts + - ./mkdocs:/config/workspace/mkdocs + - ./docker-compose.yml:/config/workspace/docker-compose.yml + # NOTE: .env intentionally excluded — secrets must not be accessible via Code Server + ports: + - "127.0.0.1:${CODE_SERVER_PORT:-8888}:8443" restart: unless-stopped - environment: - PASSWORD: "{{secrets.nocodbAdminPassword}}" - SUDO_PASSWORD: "{{secrets.nocodbAdminPassword}}" - volumes: - - .:/config/workspace:rw + logging: *default-logging networks: - - {{networkName}} + - changemaker-lite - gitea: - image: {{registryUrl}}/gitea:1.23.7 - container_name: {{containerPrefix}}-gitea + # MkDocs — Live documentation preview + mkdocs: + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/mkdocs-material:latest + container_name: mkdocs-changemaker + volumes: + - ./mkdocs:/docs:rw + - ./assets/images:/docs/assets/images:rw + - ./scripts/mkdocs-build-trigger.py:/scripts/mkdocs-build-trigger.py:ro + - ./scripts/mkdocs-entrypoint.sh:/scripts/mkdocs-entrypoint.sh:ro + user: "${USER_ID:-1000}:${GROUP_ID:-1000}" + ports: + - "127.0.0.1:${MKDOCS_PORT:-4003}:8000" + environment: + - SITE_URL=${BASE_DOMAIN:-https://cmlite.org} + - ADMIN_PORT=${ADMIN_PORT:-3000} + - ADMIN_URL=${ADMIN_URL:-} + - MEDIA_API_PUBLIC_URL=${MEDIA_API_PUBLIC_URL:-http://localhost:4100} + - MEDIA_API_PORT=${MEDIA_API_PORT:-4100} + - BASE_DOMAIN=${BASE_DOMAIN:-} + - API_URL=${API_URL:-} + - API_PORT=${API_PORT:-4000} + - GANCIO_URL=${GANCIO_URL:-http://gancio-changemaker:13120} + - GANCIO_PORT=${GANCIO_PORT:-8092} + entrypoint: ["/bin/sh", "/scripts/mkdocs-entrypoint.sh"] restart: unless-stopped - depends_on: - v2-postgres: - condition: service_healthy - environment: - GITEA__database__DB_TYPE: postgres - GITEA__database__HOST: {{containerPrefix}}-postgres:5432 - GITEA__database__NAME: gitea - GITEA__database__USER: changemaker - GITEA__database__PASSWD: "{{secrets.postgresPassword}}" - GITEA__server__ROOT_URL: https://git.{{domain}} - GITEA__server__DOMAIN: git.{{domain}} - GITEA__server__HTTP_PORT: "3000" - GITEA__server__PROTOCOL: http - GITEA__server__ENABLE_GZIP: "true" - GITEA__server__X_FRAME_OPTIONS: "" - GITEA__security__INSTALL_LOCK: "true" - GITEA__attachment__MAX_SIZE: "1024" - GITEA__repository__MAX_CREATION_LIMIT: "-1" - GITEA__server__LFS_START_SERVER: "true" - # Reverse proxy auth — nginx injects X-WEBAUTH-USER for SSO - GITEA__service__ENABLE_REVERSE_PROXY_AUTHENTICATION: "true" - GITEA__service__ENABLE_REVERSE_PROXY_AUTO_REGISTRATION: "false" - GITEA__service__ENABLE_REVERSE_PROXY_EMAIL: "false" - GITEA__service__REVERSE_PROXY_AUTHENTICATION_HEADER: "X-WEBAUTH-USER" - GITEA__service__REQUIRE_SIGNIN_VIEW: "true" - volumes: - - {{containerPrefix}}-gitea-data:/data + logging: *default-logging networks: - - {{networkName}} - healthcheck: - test: ["CMD", "curl", "-fsSL", "http://localhost:3000/api/healthz"] - interval: 30s - timeout: 10s - retries: 5 - start_period: 30s + - changemaker-lite - # Gitea init — creates admin user after Gitea is healthy (idempotent) - # Must run as git user (UID 1000) — Gitea refuses to run as root - gitea-init: - image: {{registryUrl}}/gitea:1.23.7 - container_name: {{containerPrefix}}-gitea-init - user: "1000:1000" - depends_on: - gitea: - condition: service_healthy + # MkDocs built site — Nginx static server + mkdocs-site-server: + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/ls-nginx:1.28.2 + container_name: mkdocs-site-server-changemaker environment: - GITEA__database__DB_TYPE: postgres - GITEA__database__HOST: {{containerPrefix}}-postgres:5432 - GITEA__database__NAME: gitea - GITEA__database__USER: changemaker - GITEA__database__PASSWD: "{{secrets.postgresPassword}}" - GITEA__security__INSTALL_LOCK: "true" + - PUID=${USER_ID:-1000} + - PGID=${GROUP_ID:-1000} + - TZ=Etc/UTC volumes: - - {{containerPrefix}}-gitea-data:/data - entrypoint: ["/bin/sh", "-c"] - command: - - | - echo "[gitea-init] Running migrations (idempotent)..." - gitea migrate 2>/dev/null || true - - if gitea admin user list --admin 2>/dev/null | grep -q "admin"; then - echo "[gitea-init] Admin user already exists, skipping" - else - echo "[gitea-init] Creating admin user..." - gitea admin user create \ - --admin \ - --username admin \ - --password "{{secrets.giteaAdminPassword}}" \ - --email "{{secrets.adminEmail}}" \ - --must-change-password=false - echo "[gitea-init] Admin user created" - fi - - echo "[gitea-init] Done" - restart: "no" + - ./mkdocs/site:/config/www + - ./configs/mkdocs-site/default.conf:/config/nginx/site-confs/default.conf + ports: + - "127.0.0.1:${MKDOCS_SITE_SERVER_PORT:-4004}:80" + restart: unless-stopped + logging: *default-logging networks: - - {{networkName}} + - changemaker-lite + # n8n — Workflow automation n8n: - image: {{registryUrl}}/n8n:latest - container_name: {{containerPrefix}}-n8n + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/n8n:latest + container_name: n8n-changemaker restart: unless-stopped - environment: - N8N_ENCRYPTION_KEY: "{{secrets.n8nEncryptionKey}}" - WEBHOOK_URL: https://n8n.{{domain}} - N8N_HOST: n8n.{{domain}} - N8N_PROTOCOL: https - N8N_SECURE_COOKIE: "false" - volumes: - - {{containerPrefix}}-n8n-data:/home/node/.n8n - networks: - - {{networkName}} + ports: + - "127.0.0.1:${N8N_PORT:-5678}:5678" healthcheck: test: ["CMD", "wget", "-q", "--spider", "http://localhost:5678/healthz"] interval: 30s timeout: 5s retries: 3 - - docker-socket-proxy: - image: {{registryUrl}}/docker-socket-proxy:v0.4.2 - container_name: {{containerPrefix}}-docker-socket-proxy - restart: unless-stopped + start_period: 30s environment: - CONTAINERS: 1 - IMAGES: 1 - INFO: 1 - NETWORKS: 0 - VOLUMES: 0 - POST: 0 + - N8N_HOST=${N8N_HOST:-n8n.cmlite.org} + - N8N_PORT=5678 + - N8N_PROTOCOL=https + - NODE_ENV=production + - WEBHOOK_URL=https://${N8N_HOST:-n8n.cmlite.org}/ + - GENERIC_TIMEZONE=${GENERIC_TIMEZONE:-UTC} + - N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY:?N8N_ENCRYPTION_KEY must be set in .env} + - N8N_USER_MANAGEMENT_DISABLED=false + - N8N_DEFAULT_USER_EMAIL=${N8N_USER_EMAIL:-admin@example.com} + - N8N_DEFAULT_USER_PASSWORD=${N8N_USER_PASSWORD:?N8N_USER_PASSWORD must be set in .env} volumes: - - /var/run/docker.sock:/var/run/docker.sock:ro + - n8n-data:/home/node/.n8n + - ./local-files:/files + logging: *default-logging networks: - - {{networkName}} + - changemaker-lite + # Homepage dashboard homepage: - image: {{registryUrl}}/homepage:v0.7.2 - container_name: {{containerPrefix}}-homepage + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/homepage:v0.7.2 + container_name: homepage-changemaker + ports: + - "127.0.0.1:${HOMEPAGE_PORT:-3010}:3000" + volumes: + - ./configs/homepage:/app/config + - ./assets/icons:/app/public/icons + - ./assets/images:/app/public/images + # Docker socket access removed for security — configure homepage widgets via config files instead + environment: + - PUID=${USER_ID:-1000} + - PGID=${DOCKER_GROUP_ID:-984} + - TZ=Etc/UTC + - HOMEPAGE_ALLOWED_HOSTS=* + - HOMEPAGE_VAR_BASE_URL=${HOMEPAGE_VAR_BASE_URL:-http://localhost} + restart: unless-stopped + logging: *default-logging + networks: + - changemaker-lite + + # Gitea — Git hosting + gitea-app: + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/gitea:1.23.7 + container_name: gitea-changemaker + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 30s + environment: + - USER_UID=${USER_ID:-1000} + - USER_GID=${GROUP_ID:-1000} + - GITEA__database__DB_TYPE=${GITEA_DB_TYPE:-mysql} + - GITEA__database__HOST=${GITEA_DB_HOST:-gitea-db:3306} + - GITEA__database__NAME=${GITEA_DB_NAME:-gitea} + - GITEA__database__USER=${GITEA_DB_USER:-gitea} + - GITEA__database__PASSWD=${GITEA_DB_PASSWD} + - GITEA__server__ROOT_URL=${GITEA_ROOT_URL} + - GITEA__server__HTTP_PORT=3000 + - GITEA__server__PROTOCOL=http + - GITEA__server__DOMAIN=${GITEA_DOMAIN} + - GITEA__server__ENABLE_GZIP=true + - GITEA__server__X_FRAME_OPTIONS= + # Increase upload limits for large pushes + - GITEA__server__LFS_MAX_FILE_SIZE=1024 + - GITEA__repository__upload__FILE_MAX_SIZE=1024 + - GITEA__repository__upload__MAX_FILES=1000 + # Reverse proxy auth — nginx injects X-WEBAUTH-USER for SSO + - GITEA__service__ENABLE_REVERSE_PROXY_AUTHENTICATION=true + - GITEA__service__ENABLE_REVERSE_PROXY_AUTO_REGISTRATION=false + - GITEA__service__ENABLE_REVERSE_PROXY_EMAIL=false + - GITEA__service__REVERSE_PROXY_AUTHENTICATION_HEADER=X-WEBAUTH-USER + - GITEA__service__REQUIRE_SIGNIN_VIEW=true + # Skip installation wizard — admin user created by gitea-init container + - GITEA__security__INSTALL_LOCK=true + restart: unless-stopped + volumes: + - gitea-data:/data + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + ports: + - "127.0.0.1:${GITEA_WEB_PORT:-3030}:3000" + - "127.0.0.1:${GITEA_SSH_PORT:-2222}:22" + depends_on: + gitea-db: + condition: service_healthy + logging: *default-logging + networks: + - changemaker-lite + + gitea-db: + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/mysql:8 + container_name: gitea-mysql restart: unless-stopped environment: - DOCKER_HOST: tcp://{{containerPrefix}}-docker-socket-proxy:2375 + - MYSQL_ROOT_PASSWORD=${GITEA_DB_ROOT_PASSWORD} + - MYSQL_USER=${GITEA_DB_USER:-gitea} + - MYSQL_PASSWORD=${GITEA_DB_PASSWD} + - MYSQL_DATABASE=${GITEA_DB_NAME:-gitea} volumes: - - {{containerPrefix}}-homepage-data:/app/config - depends_on: - - docker-socket-proxy + - mysql-data:/var/lib/mysql + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "${GITEA_DB_USER:-gitea}", "-p${GITEA_DB_PASSWD}"] + interval: 10s + timeout: 5s + retries: 5 + logging: *default-logging networks: - - {{networkName}} + - changemaker-lite - excalidraw: - image: {{registryUrl}}/excalidraw:latest - container_name: {{containerPrefix}}-excalidraw - restart: unless-stopped + # Gitea Init — creates admin user on first boot (after gitea-app is healthy) + gitea-init: + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/gitea:1.23.7 + container_name: gitea-init + depends_on: + gitea-app: + condition: service_healthy + restart: "no" + environment: + - GITEA_ADMIN_USER=${GITEA_ADMIN_USER:-admin} + - GITEA_ADMIN_PASSWORD=${GITEA_ADMIN_PASSWORD:-${INITIAL_ADMIN_PASSWORD}} + - GITEA_ADMIN_EMAIL=${INITIAL_ADMIN_EMAIL:-admin@cmlite.org} + volumes: + - gitea-data:/data + - ./scripts/gitea-init.sh:/init.sh:ro + entrypoint: ["/bin/sh", "/init.sh"] + logging: *default-logging networks: - - {{networkName}} + - changemaker-lite + + # Mini QR — QR code generator + mini-qr: + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/mini-qr:v0.26.0 + container_name: mini-qr + ports: + - "127.0.0.1:${MINI_QR_PORT:-8089}:8080" + restart: unless-stopped + logging: *default-logging + networks: + - changemaker-lite + + # Excalidraw — Collaborative whiteboard + excalidraw: + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/excalidraw:latest + container_name: excalidraw-changemaker + restart: unless-stopped + ports: + - "127.0.0.1:${EXCALIDRAW_PORT:-8090}:80" + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://localhost:80/"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 20s + environment: + - VITE_APP_COLLAB_SERVER_URL=${EXCALIDRAW_WS_URL:-wss://draw.cmlite.org} + logging: *default-logging + networks: + - changemaker-lite # Vaultwarden — Password manager (Bitwarden-compatible) vaultwarden: - image: {{registryUrl}}/vaultwarden:1.35.4 - container_name: {{containerPrefix}}-vaultwarden + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/vaultwarden:1.35.4 + container_name: vaultwarden-changemaker restart: unless-stopped - environment: - ADMIN_TOKEN: "{{secrets.vaultwardenAdminToken}}" - DOMAIN: https://vault.{{domain}} - SIGNUPS_ALLOWED: "false" - WEBSOCKET_ENABLED: "true" - ROCKET_PORT: "80" - LOG_LEVEL: info - SMTP_HOST: {{containerPrefix}}-mailhog - SMTP_PORT: "1025" - SMTP_FROM: "noreply@{{domain}}" - SMTP_FROM_NAME: Vaultwarden - SMTP_SECURITY: "off" - SMTP_USERNAME: "" - SMTP_PASSWORD: "" - volumes: - - {{containerPrefix}}-vaultwarden-data:/data - networks: - - {{networkName}} + ports: + - "127.0.0.1:${VAULTWARDEN_PORT:-8445}:80" healthcheck: test: ["CMD", "curl", "-sf", "http://localhost:80/alive"] interval: 30s timeout: 5s retries: 3 start_period: 15s + environment: + - ADMIN_TOKEN=${VAULTWARDEN_ADMIN_TOKEN:-} + - DOMAIN=${VAULTWARDEN_DOMAIN:-https://vault.cmlite.org} + - SIGNUPS_ALLOWED=${VAULTWARDEN_SIGNUPS_ALLOWED:-false} + - WEBSOCKET_ENABLED=${VAULTWARDEN_WEBSOCKET_ENABLED:-true} + - ROCKET_PORT=80 + - LOG_LEVEL=info + - SMTP_HOST=${SMTP_HOST:-mailhog-changemaker} + - SMTP_PORT=${SMTP_PORT:-1025} + - SMTP_FROM=${SMTP_USER:-noreply@cmlite.org} + - SMTP_FROM_NAME=${SMTP_FROM_NAME:-Vaultwarden} + - SMTP_SECURITY=${VAULTWARDEN_SMTP_SECURITY:-off} + - SMTP_USERNAME=${SMTP_USER:-} + - SMTP_PASSWORD=${SMTP_PASS:-} + volumes: + - vaultwarden-data:/data + logging: *default-logging + networks: + - changemaker-lite - # Vaultwarden init — invites the initial admin user (safe to re-run) + # One-shot: invites the initial admin user into Vaultwarden after it starts. + # Uses the admin panel API to send an invitation email (lands in MailHog or real SMTP). + # Safe to re-run (Vaultwarden ignores duplicate invites for existing users). Exits 0 on success. vaultwarden-init: - image: {{registryUrl}}/alpine-curl:8.11.1 - container_name: {{containerPrefix}}-vaultwarden-init + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/alpine-curl:8.11.1 + container_name: vaultwarden-init depends_on: vaultwarden: condition: service_healthy restart: "no" + environment: + VAULTWARDEN_URL: http://vaultwarden-changemaker:80 + VAULTWARDEN_ADMIN_TOKEN: ${VAULTWARDEN_ADMIN_TOKEN:-} + INVITE_EMAIL: ${INITIAL_ADMIN_EMAIL:-admin@cmlite.org} entrypoint: ["/bin/sh", "-c"] command: - | echo "[vaultwarden-init] Waiting for Vaultwarden..." - for i in $$(seq 1 20); do - if curl -sf http://{{containerPrefix}}-vaultwarden:80/alive >/dev/null 2>&1; then + for i in $(seq 1 20); do + if curl -sf http://vaultwarden-changemaker:80/alive >/dev/null 2>&1; then break fi sleep 2 done - VAULTWARDEN_ADMIN_TOKEN="{{secrets.vaultwardenAdminToken}}" - VAULTWARDEN_URL="http://{{containerPrefix}}-vaultwarden:80" - INVITE_EMAIL="{{secrets.adminEmail}}" - if [ -z "$$VAULTWARDEN_ADMIN_TOKEN" ]; then echo "[vaultwarden-init] VAULTWARDEN_ADMIN_TOKEN not set, skipping invite" exit 0 fi echo "[vaultwarden-init] Authenticating with admin panel..." - SESSION_COOKIE=$$(mktemp) - HTTP_CODE=$$(curl -s -o /dev/null -w "%{http_code}" \ + SESSION_COOKIE=$(mktemp) + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \ -c "$$SESSION_COOKIE" \ -X POST "$$VAULTWARDEN_URL/admin" \ -H "Content-Type: application/x-www-form-urlencoded" \ @@ -935,14 +887,14 @@ services: echo "[vaultwarden-init] Authenticated" echo "[vaultwarden-init] Inviting $$INVITE_EMAIL..." - INVITE_CODE=$$(curl -s -w "\n%{http_code}" \ + INVITE_CODE=$(curl -s -w "\n%{http_code}" \ -b "$$SESSION_COOKIE" \ -X POST "$$VAULTWARDEN_URL/admin/invite" \ -H "Content-Type: application/json" \ -d "{\"email\":\"$$INVITE_EMAIL\"}") - INVITE_HTTP=$$(echo "$$INVITE_CODE" | tail -1) - INVITE_BODY=$$(echo "$$INVITE_CODE" | head -n -1) + INVITE_HTTP=$(echo "$$INVITE_CODE" | tail -1) + INVITE_BODY=$(echo "$$INVITE_CODE" | head -n -1) if [ "$$INVITE_HTTP" = "200" ] || [ "$$INVITE_HTTP" = "422" ]; then echo "[vaultwarden-init] Invite sent (or user already exists)" @@ -952,140 +904,601 @@ services: rm -f "$$SESSION_COOKIE" echo "[vaultwarden-init] Done" + logging: *default-logging networks: - - {{networkName}} -{{/if}} + - changemaker-lite -{{#if enableMonitoring}} - # ─── Monitoring Stack ────────────────────────────────────── + # Rocket.Chat — Team coordination chat + rocketchat: + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/rocketchat:7.9.7 + container_name: rocketchat-changemaker + restart: unless-stopped + depends_on: + mongodb-rocketchat: + condition: service_healthy + nats-rocketchat: + condition: service_started + environment: + - ROOT_URL=http://chat.${DOMAIN:-cmlite.org} + - MONGO_URL=mongodb://${MONGO_ROOT_USER:-rocketchat}:${MONGO_ROOT_PASSWORD}@mongodb-rocketchat:27017/rocketchat?replicaSet=rs0&authSource=admin + - MONGO_OPLOG_URL=mongodb://${MONGO_ROOT_USER:-rocketchat}:${MONGO_ROOT_PASSWORD}@mongodb-rocketchat:27017/local?replicaSet=rs0&authSource=admin + - TRANSPORTER=monolith+nats://nats-rocketchat:4222 + - PORT=3000 + - ADMIN_USERNAME=${ROCKETCHAT_ADMIN_USER:-rcadmin} + - ADMIN_NAME=Admin + - ADMIN_EMAIL=${INITIAL_ADMIN_EMAIL:-admin@cmlite.org} + - ADMIN_PASS=${ROCKETCHAT_ADMIN_PASSWORD:?ROCKETCHAT_ADMIN_PASSWORD must be set in .env} + - CREATE_TOKENS_FOR_USERS=true + - OVERWRITE_SETTING_Iframe_Integration_send_enable=true + - OVERWRITE_SETTING_Iframe_Integration_receive_enable=true + - OVERWRITE_SETTING_Iframe_Integration_receive_origin=http://app.${DOMAIN:-cmlite.org},https://app.${DOMAIN:-cmlite.org},http://localhost:${ADMIN_PORT:-3000} + # Jitsi integration (JWT-authenticated video calls) + - OVERWRITE_SETTING_Jitsi_Enabled=${ENABLE_MEET:-false} + - OVERWRITE_SETTING_Jitsi_Domain=meet.${DOMAIN:-cmlite.org} + - OVERWRITE_SETTING_Jitsi_URL_Room_Prefix=RocketChat + - OVERWRITE_SETTING_Jitsi_Enable_Channels=true + - OVERWRITE_SETTING_Jitsi_Open_New_Window=false + - OVERWRITE_SETTING_Jitsi_Enabled_TokenAuth=true + - OVERWRITE_SETTING_Jitsi_Application_ID=${JITSI_APP_ID:-changemaker} + - OVERWRITE_SETTING_Jitsi_Application_Secret=${JITSI_APP_SECRET:-} + # Conference Call provider (marketplace app-based system in RC 7.x) + - OVERWRITE_SETTING_VideoConf_Default_Provider=jitsi + volumes: + - rocketchat-uploads:/app/uploads + logging: *default-logging + networks: + - changemaker-lite + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:3000/api/info"] + interval: 30s + timeout: 10s + retries: 10 + start_period: 90s + + # NATS (required by Rocket.Chat 7.x+ for microservice messaging) + nats-rocketchat: + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/nats:2.11-alpine + container_name: nats-rocketchat + restart: unless-stopped + command: --http_port 8222 + logging: *default-logging + networks: + - changemaker-lite + + # MongoDB (required by Rocket.Chat — replica set for oplog tailing) + mongodb-rocketchat: + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/mongo:6.0 + container_name: mongodb-rocketchat + restart: unless-stopped + # Generate keyfile then delegate to Docker's standard entrypoint (creates INITDB user) + entrypoint: ["/bin/bash", "-c", "if [ ! -f /data/replica.key ]; then openssl rand -base64 756 > /data/replica.key; fi && chmod 400 /data/replica.key && chown 999:999 /data/replica.key && exec docker-entrypoint.sh mongod --replSet rs0 --bind_ip_all --keyFile /data/replica.key"] + environment: + MONGO_INITDB_ROOT_USERNAME: ${MONGO_ROOT_USER:-rocketchat} + MONGO_INITDB_ROOT_PASSWORD: ${MONGO_ROOT_PASSWORD:?MONGO_ROOT_PASSWORD must be set in .env} + volumes: + - mongodb-rocketchat-data:/data/db + logging: *default-logging + networks: + - changemaker-lite + healthcheck: + test: ["CMD", "mongosh", "-u", "${MONGO_ROOT_USER:-rocketchat}", "-p", "${MONGO_ROOT_PASSWORD}", "--authenticationDatabase", "admin", "--quiet", "--eval", "try { rs.status().ok } catch(e) { rs.initiate({_id:'rs0',members:[{_id:0,host:'mongodb-rocketchat:27017'}]}).ok }"] + interval: 10s + timeout: 10s + retries: 10 + start_period: 30s + + # Gancio Config Init — Writes /home/node/data/config.json from .env if missing. + # Gancio refuses to start when its DB has tables but the data volume has no + # config.json ("Non empty db! Please move your current db elsewhere than retry"), + # which causes an infinite restart loop. This sidecar runs on every `up` and is + # a no-op when config.json is already present. See docker-compose.yml for the + # full rationale; the two files must stay in parity per scripts/validate-compose-parity.sh. + gancio-config-init: + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/alpine:3 + container_name: gancio-config-init + restart: "no" + volumes: + - gancio-data:/data + environment: + - GANCIO_BASE_URL=${GANCIO_BASE_URL:-https://events.cmlite.org} + - V2_POSTGRES_USER=${V2_POSTGRES_USER:-changemaker} + - V2_POSTGRES_PASSWORD=${V2_POSTGRES_PASSWORD:?V2_POSTGRES_PASSWORD must be set in .env} + entrypoint: ["sh", "-c"] + command: + - | + set -e + if [ -s /data/config.json ]; then + echo "Gancio config.json present — skipping" + exit 0 + fi + echo "Gancio config.json missing — regenerating from .env" + printf '{"baseurl":"%s","server":{"host":"0.0.0.0","port":13120},"db":{"dialect":"postgres","host":"changemaker-v2-postgres","port":5432,"database":"gancio","username":"%s","password":"%s"}}' \ + "$$GANCIO_BASE_URL" "$$V2_POSTGRES_USER" "$$V2_POSTGRES_PASSWORD" > /data/config.json + chown 1000:1000 /data/config.json + echo "Gancio config.json regenerated" + logging: *default-logging + networks: + - changemaker-lite + + # Gancio — Event management platform (uses shared PostgreSQL) + gancio: + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/gancio:1.28.2 + container_name: gancio-changemaker + restart: unless-stopped + depends_on: + v2-postgres: + condition: service_healthy + gancio-config-init: + condition: service_completed_successfully + ports: + - "127.0.0.1:${GANCIO_PORT:-8092}:13120" + healthcheck: + test: ["CMD", "node", "-e", "require('http').get('http://localhost:13120/', r => process.exit(r.statusCode < 400 ? 0 : 1)).on('error', () => process.exit(1))"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 60s + environment: + - GANCIO_DATA=/home/node/data + - NODE_ENV=production + - GANCIO_DB_DIALECT=postgres + - GANCIO_DB_HOST=changemaker-v2-postgres + - GANCIO_DB_PORT=5432 + - GANCIO_DB_DATABASE=gancio + - GANCIO_DB_USERNAME=${V2_POSTGRES_USER:-changemaker} + - GANCIO_DB_PASSWORD=${V2_POSTGRES_PASSWORD:?V2_POSTGRES_PASSWORD must be set in .env} + - server__baseurl=${GANCIO_BASE_URL:-https://events.cmlite.org} + volumes: + - gancio-data:/home/node/data + logging: *default-logging + networks: + - changemaker-lite + + # Gancio Init — Creates admin user + seeds default theme settings after Gancio creates its tables + # Runs once after Gancio is healthy, then exits. Idempotent (ON CONFLICT DO NOTHING). + gancio-init: + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/postgres:16-alpine + container_name: gancio-init + depends_on: + gancio: + condition: service_healthy + environment: + - PGHOST=changemaker-v2-postgres + - PGUSER=${V2_POSTGRES_USER:-changemaker} + - PGPASSWORD=${V2_POSTGRES_PASSWORD:?V2_POSTGRES_PASSWORD must be set in .env} + - PGDATABASE=gancio + - GANCIO_ADMIN_USER=${GANCIO_ADMIN_USER:-admin} + - GANCIO_ADMIN_PASSWORD=${GANCIO_ADMIN_PASSWORD} + entrypoint: ["sh", "-c"] + command: + - | + echo "Ensuring pgcrypto extension exists..." + psql -c "CREATE EXTENSION IF NOT EXISTS pgcrypto;" + + echo "Creating Gancio admin user (if not exists)..." + if [ -n "$$GANCIO_ADMIN_PASSWORD" ]; then + psql -c "INSERT INTO users (email, password, display_name, role, is_admin, is_active, \"createdAt\", \"updatedAt\") + VALUES ('$$GANCIO_ADMIN_USER', crypt('$$GANCIO_ADMIN_PASSWORD', gen_salt('bf', 10)), 'Admin', 'admin', true, true, NOW(), NOW()) + ON CONFLICT (email) DO NOTHING;" + echo "Gancio admin user ensured." + else + echo "WARNING: GANCIO_ADMIN_PASSWORD not set, skipping admin user creation." + fi + + echo "Seeding Gancio default theme settings..." + psql -c "INSERT INTO settings (key, value, is_secret, \"createdAt\", \"updatedAt\") VALUES + ('dark_colors', '{\"primary\": \"#FF6E40\", \"error\": \"#FF5252\", \"info\": \"#2196F3\", \"success\": \"#4CAF50\", \"warning\": \"#FB8C00\"}', false, NOW(), NOW()), + ('light_colors', '{\"primary\": \"#FF4500\", \"error\": \"#FF5252\", \"info\": \"#2196F3\", \"success\": \"#4CAF50\", \"warning\": \"#FB8C00\"}', false, NOW(), NOW()) + ON CONFLICT (key) DO NOTHING;" + echo "Gancio theme settings seeded." + restart: "no" + logging: *default-logging + networks: + - changemaker-lite + + # ========================================================================= + # JITSI MEET (Self-hosted video conferencing with JWT auth) + # ========================================================================= + + # Jitsi Web — Frontend UI (served via nginx at meet.${DOMAIN}) + jitsi-web: + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/jitsi-web:stable-9823 + container_name: jitsi-web-changemaker + restart: unless-stopped + depends_on: + jitsi-prosody: + condition: service_healthy + environment: + - ENABLE_AUTH=1 + - ENABLE_GUESTS=1 + - AUTH_TYPE=jwt + - JWT_APP_ID=${JITSI_APP_ID:-changemaker} + - JWT_APP_SECRET=${JITSI_APP_SECRET:-} + - XMPP_DOMAIN=meet.jitsi + - XMPP_AUTH_DOMAIN=auth.meet.jitsi + - XMPP_MUC_DOMAIN=muc.meet.jitsi + - XMPP_INTERNAL_MUC_DOMAIN=internal-muc.meet.jitsi + - XMPP_BOSH_URL_BASE=http://jitsi-prosody-changemaker:5280 + - XMPP_SERVER=jitsi-prosody-changemaker + - PUBLIC_URL=https://meet.${DOMAIN:-cmlite.org} + - TOKEN_AUTH_URL=https://app.${DOMAIN:-cmlite.org}/jitsi-auth/{room} + - TZ=UTC + volumes: + - jitsi-web-config:/config + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://localhost:80/"] + interval: 30s + timeout: 5s + retries: 5 + start_period: 30s + logging: *default-logging + networks: + - changemaker-lite + + # Jitsi Prosody — XMPP server + JWT validation + jitsi-prosody: + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/jitsi-prosody:stable-9823 + container_name: jitsi-prosody-changemaker + restart: unless-stopped + environment: + - ENABLE_AUTH=1 + - ENABLE_GUESTS=1 + - AUTH_TYPE=jwt + - JWT_APP_ID=${JITSI_APP_ID:-changemaker} + - JWT_APP_SECRET=${JITSI_APP_SECRET:-} + - JWT_ACCEPTED_ISSUERS=${JITSI_APP_ID:-changemaker} + - JWT_ACCEPTED_AUDIENCES=${JITSI_APP_ID:-changemaker} + - XMPP_DOMAIN=meet.jitsi + - XMPP_AUTH_DOMAIN=auth.meet.jitsi + - XMPP_MUC_DOMAIN=muc.meet.jitsi + - XMPP_INTERNAL_MUC_DOMAIN=internal-muc.meet.jitsi + - XMPP_MUC_MODULES=token_verification,token_affiliation + - JICOFO_AUTH_PASSWORD=${JITSI_JICOFO_AUTH_PASSWORD:-} + - JVB_AUTH_PASSWORD=${JITSI_JVB_AUTH_PASSWORD:-} + - TZ=UTC + volumes: + - jitsi-prosody-config:/config + - jitsi-prosody-plugins:/prosody-plugins-custom + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://localhost:5280/health"] + interval: 30s + timeout: 5s + retries: 5 + start_period: 30s + logging: *default-logging + networks: + - changemaker-lite + + # Jitsi Jicofo — Conference focus / room management + jitsi-jicofo: + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/jitsi-jicofo:stable-9823 + container_name: jitsi-jicofo-changemaker + restart: unless-stopped + depends_on: + jitsi-prosody: + condition: service_healthy + environment: + - ENABLE_AUTH=1 + - AUTH_TYPE=jwt + - XMPP_DOMAIN=meet.jitsi + - XMPP_AUTH_DOMAIN=auth.meet.jitsi + - XMPP_MUC_DOMAIN=muc.meet.jitsi + - XMPP_INTERNAL_MUC_DOMAIN=internal-muc.meet.jitsi + - XMPP_SERVER=jitsi-prosody-changemaker + - JICOFO_AUTH_PASSWORD=${JITSI_JICOFO_AUTH_PASSWORD:-} + - TZ=UTC + volumes: + - jitsi-jicofo-config:/config + logging: *default-logging + networks: + - changemaker-lite + + # Jitsi JVB — Video bridge (media relay for audio/video) + jitsi-jvb: + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/jitsi-jvb:stable-9823 + container_name: jitsi-jvb-changemaker + restart: unless-stopped + depends_on: + jitsi-prosody: + condition: service_healthy + ports: + - "127.0.0.1:${JVB_PORT:-10000}:10000/udp" + environment: + - XMPP_DOMAIN=meet.jitsi + - XMPP_AUTH_DOMAIN=auth.meet.jitsi + - XMPP_MUC_DOMAIN=muc.meet.jitsi + - XMPP_INTERNAL_MUC_DOMAIN=internal-muc.meet.jitsi + - XMPP_SERVER=jitsi-prosody-changemaker + - JVB_AUTH_PASSWORD=${JITSI_JVB_AUTH_PASSWORD:-} + - JVB_ADVERTISE_IPS=${JVB_ADVERTISE_IP:-} + - JVB_PORT=${JVB_PORT:-10000} + - TZ=UTC + volumes: + - jitsi-jvb-config:/config + logging: *default-logging + networks: + - changemaker-lite + + # MailHog — Email testing (dev) + mailhog: + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/mailhog:v1.0.1 + container_name: mailhog-changemaker + ports: + - "127.0.0.1:${MAILHOG_WEB_PORT:-8025}:8025" + # SMTP port 1025 is only exposed on the Docker network (containers connect via mailhog-changemaker:1025) + restart: unless-stopped + networks: + - changemaker-lite + logging: + driver: "json-file" + options: + max-size: "5m" + max-file: "2" + + # ========================================================================= + # TUNNEL (Pangolin Newt connector) + # ========================================================================= + + # Newt — Pangolin tunnel connector (connects to Pangolin server) + newt: + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/newt:latest + container_name: newt-changemaker + restart: unless-stopped + environment: + - PANGOLIN_ENDPOINT=${PANGOLIN_ENDPOINT} + - NEWT_ID=${PANGOLIN_NEWT_ID} + - NEWT_SECRET=${PANGOLIN_NEWT_SECRET} + depends_on: + nginx: + condition: service_healthy + logging: *default-logging + networks: + - changemaker-lite + + # Docker socket proxy — read-only access for container status monitoring + docker-socket-proxy: + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/docker-socket-proxy:v0.4.2 + container_name: docker-socket-proxy + restart: unless-stopped + environment: + - CONTAINERS=1 # Allow container inspection + - POST=0 # Block all write operations + - IMAGES=0 + - NETWORKS=0 + - VOLUMES=0 + - EXEC=0 + - SWARM=0 + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + logging: *default-logging + networks: + - changemaker-lite + + # ========================================================================= + # MONITORING (behind profile flag) + # ========================================================================= prometheus: - image: {{registryUrl}}/prometheus:v3.10.0 - container_name: {{containerPrefix}}-prometheus - restart: unless-stopped + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/prometheus:v3.10.0 + container_name: prometheus-changemaker command: - '--config.file=/etc/prometheus/prometheus.yml' - '--storage.tsdb.path=/prometheus' - '--storage.tsdb.retention.time=30d' + ports: + - "127.0.0.1:${PROMETHEUS_PORT:-9090}:9090" volumes: - - ./configs/prometheus:/etc/prometheus:ro - - {{containerPrefix}}-prometheus-data:/prometheus + - ./configs/prometheus:/etc/prometheus + - prometheus-data:/prometheus + restart: always + logging: *default-logging networks: - - {{networkName}} + - changemaker-lite + profiles: + - monitoring grafana: - image: {{registryUrl}}/grafana:12.3.0 - container_name: {{containerPrefix}}-grafana - restart: unless-stopped + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/grafana:12.3.0 + container_name: grafana-changemaker + ports: + - "127.0.0.1:${GRAFANA_PORT:-3001}:3000" environment: - GF_SECURITY_ADMIN_PASSWORD: "{{secrets.grafanaAdminPassword}}" - GF_USERS_ALLOW_SIGN_UP: "false" - GF_SERVER_ROOT_URL: https://grafana.{{domain}} - GF_SECURITY_ALLOW_EMBEDDING: "true" + - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD:?GRAFANA_ADMIN_PASSWORD must be set in .env} + - GF_USERS_ALLOW_SIGN_UP=false + - GF_SERVER_ROOT_URL=${GRAFANA_ROOT_URL:-http://localhost:3001} + - GF_SECURITY_ALLOW_EMBEDDING=true + - GF_AUTH_ANONYMOUS_ENABLED=false + - GF_AUTH_ANONYMOUS_ORG_ROLE=Viewer volumes: - - {{containerPrefix}}-grafana-data:/var/lib/grafana + - grafana-data:/var/lib/grafana - ./configs/grafana:/etc/grafana/provisioning + restart: always depends_on: - prometheus + logging: *default-logging networks: - - {{networkName}} - - alertmanager: - image: {{registryUrl}}/alertmanager:v0.31.1 - container_name: {{containerPrefix}}-alertmanager - restart: unless-stopped - command: - - '--config.file=/etc/alertmanager/alertmanager.yml' - - '--storage.path=/alertmanager' - volumes: - - ./configs/alertmanager:/etc/alertmanager:ro - - {{containerPrefix}}-alertmanager-data:/alertmanager - networks: - - {{networkName}} + - changemaker-lite + profiles: + - monitoring cadvisor: - image: {{registryUrl}}/cadvisor:v0.55.1 - container_name: {{containerPrefix}}-cadvisor - restart: unless-stopped + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/cadvisor:v0.55.1 + container_name: cadvisor-changemaker + ports: + - "127.0.0.1:${CADVISOR_PORT:-8080}:8080" volumes: - /:/rootfs:ro - /var/run:/var/run:ro - /sys:/sys:ro - /var/lib/docker/:/var/lib/docker:ro + - /dev/disk/:/dev/disk:ro + cap_drop: + - ALL + cap_add: + - SYS_PTRACE + - DAC_READ_SEARCH + security_opt: + - no-new-privileges:true + read_only: true + devices: + - /dev/kmsg + restart: always + logging: *default-logging networks: - - {{networkName}} + - changemaker-lite + profiles: + - monitoring node-exporter: - image: {{registryUrl}}/node-exporter:v1.10.2 - container_name: {{containerPrefix}}-node-exporter - restart: unless-stopped + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/node-exporter:v1.10.2 + container_name: node-exporter-changemaker + ports: + - "127.0.0.1:${NODE_EXPORTER_PORT:-9100}:9100" command: - '--path.rootfs=/host' + - '--path.procfs=/host/proc' + - '--path.sysfs=/host/sys' + - '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)' volumes: - - /:/host:ro,rslave + - /proc:/host/proc:ro + - /sys:/host/sys:ro + - /:/rootfs:ro + restart: always + logging: *default-logging networks: - - {{networkName}} + - changemaker-lite + profiles: + - monitoring redis-exporter: - image: {{registryUrl}}/redis_exporter:v1.81.0 - container_name: {{containerPrefix}}-redis-exporter - restart: unless-stopped + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/redis_exporter:v1.81.0 + container_name: redis-exporter-changemaker + ports: + - "127.0.0.1:${REDIS_EXPORTER_PORT:-9121}:9121" environment: - REDIS_ADDR: redis://{{containerPrefix}}-redis:6379 - REDIS_PASSWORD: "{{secrets.redisPassword}}" + - REDIS_ADDR=redis://redis-changemaker:6379 + - REDIS_PASSWORD=${REDIS_PASSWORD} + restart: always depends_on: - redis + logging: *default-logging networks: - - {{networkName}} + - changemaker-lite + profiles: + - monitoring + + alertmanager: + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/alertmanager:v0.31.1 + container_name: alertmanager-changemaker + ports: + - "127.0.0.1:${ALERTMANAGER_PORT:-9093}:9093" + volumes: + - ./configs/alertmanager:/etc/alertmanager + - alertmanager-data:/alertmanager + command: + - '--config.file=/etc/alertmanager/alertmanager.yml' + - '--storage.path=/alertmanager' + restart: always + logging: *default-logging + networks: + - changemaker-lite + profiles: + - monitoring gotify: - image: {{registryUrl}}/gotify:v2.9.0 - container_name: {{containerPrefix}}-gotify - restart: unless-stopped + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/gotify:v2.9.0 + container_name: gotify-changemaker + ports: + - "127.0.0.1:${GOTIFY_PORT:-8889}:80" + environment: + - GOTIFY_DEFAULTUSER_NAME=${GOTIFY_ADMIN_USER:-admin} + - GOTIFY_DEFAULTUSER_PASS=${GOTIFY_ADMIN_PASSWORD:?GOTIFY_ADMIN_PASSWORD must be set in .env} + - TZ=Etc/UTC volumes: - - {{containerPrefix}}-gotify-data:/app/data + - gotify-data:/app/data + restart: always + logging: *default-logging networks: - - {{networkName}} -{{/if}} + - changemaker-lite + profiles: + - monitoring -# ─── Volumes ────────────────────────────────────────────── + # ========================================================================= + # CCP REMOTE AGENT (optional — enabled via COMPOSE_PROFILES=ccp-agent) + # ========================================================================= -volumes: - {{containerPrefix}}-postgres-data: - {{containerPrefix}}-redis-data: - {{containerPrefix}}-nocodb-data: -{{#if enableListmonk}} - {{containerPrefix}}-listmonk-data: -{{/if}} -{{#if enableGancio}} - {{containerPrefix}}-gancio-data: -{{/if}} -{{#if enableChat}} - {{containerPrefix}}-mongodb-data: - {{containerPrefix}}-rocketchat-uploads: -{{/if}} -{{#if enableDevTools}} - {{containerPrefix}}-gitea-data: - {{containerPrefix}}-n8n-data: - {{containerPrefix}}-homepage-data: - {{containerPrefix}}-vaultwarden-data: -{{/if}} -{{#if enableMonitoring}} - {{containerPrefix}}-prometheus-data: - {{containerPrefix}}-grafana-data: - {{containerPrefix}}-alertmanager-data: - {{containerPrefix}}-gotify-data: -{{/if}} -{{#if enableMeet}} - {{containerPrefix}}-jitsi-web-config: - {{containerPrefix}}-jitsi-prosody-config: - {{containerPrefix}}-jitsi-prosody-plugins: - {{containerPrefix}}-jitsi-jicofo-config: - {{containerPrefix}}-jitsi-jvb-config: -{{/if}} + ccp-agent: + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/changemaker-ccp-agent:{{imageTag}} + container_name: ${COMPOSE_PROJECT_NAME:-changemaker-lite}-ccp-agent + restart: unless-stopped + profiles: ["ccp-agent"] + ports: + - "${CCP_AGENT_PORT:-7443}:7443" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ccp-agent-data:/var/lib/ccp-agent + - ccp-agent-certs:/etc/ccp-agent + # Mount the instance directory so the agent can read compose files and + # write status.json + backups (writable; agent already has docker.sock, + # so file write access is not an additional security escalation). + - .:/app/instance + environment: + - AGENT_PORT=7443 + - AGENT_DATA_DIR=/var/lib/ccp-agent + - CCP_URL=${CCP_URL:-} + - CCP_INVITE_CODE=${CCP_INVITE_CODE:-} + - CCP_AGENT_URL=${CCP_AGENT_URL:-} + - INSTANCE_SLUG=${COMPOSE_PROJECT_NAME:-changemaker-lite} + - INSTANCE_DOMAIN=${DOMAIN:-localhost} + - INSTANCE_BASE_PATH=/app/instance + # Pass the host's compose project name so the agent runs `docker compose -p ` + # against the right project (not basename of INSTANCE_BASE_PATH, which is "instance"). + # COMPOSE_PROJECT is read by the agent's TypeScript for slug derivation; + # COMPOSE_PROJECT_NAME is what Docker Compose itself reads when upgrade.sh + # shells out to `docker compose ...` — without it, compose defaults to + # basename(cwd)="instance" and collides with the host's existing containers. + - COMPOSE_PROJECT=${COMPOSE_PROJECT_NAME:-changemaker-lite} + - COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME:-changemaker-lite} + logging: *default-logging + networks: + - changemaker-lite -# ─── Networks ───────────────────────────────────────────── +# ============================================================================= +# NETWORKS & VOLUMES +# ============================================================================= networks: - {{networkName}}: + changemaker-lite: driver: bridge + +volumes: + # v2 + v2-postgres-data: + nocodb-v2-data: + # Shared + redis-data: + listmonk-data: + # Platform + n8n-data: + gitea-data: + mysql-data: + vaultwarden-data: + # Rocket.Chat + rocketchat-uploads: + mongodb-rocketchat-data: + # Gancio + gancio-data: + # Jitsi Meet + jitsi-web-config: + jitsi-prosody-config: + jitsi-prosody-plugins: + jitsi-jicofo-config: + jitsi-jvb-config: + # Monitoring + prometheus-data: + grafana-data: + alertmanager-data: + gotify-data: + # CCP Agent + ccp-agent-data: + ccp-agent-certs: diff --git a/changemaker-control-panel/templates/docker-compose.yml.hbs.OLD-style-pre-approach-c b/changemaker-control-panel/templates/docker-compose.yml.hbs.OLD-style-pre-approach-c new file mode 100644 index 0000000..0803b72 --- /dev/null +++ b/changemaker-control-panel/templates/docker-compose.yml.hbs.OLD-style-pre-approach-c @@ -0,0 +1,1091 @@ +# Changemaker Lite — Instance: {{name}} +# Compose project: {{composeProject}} +# Generated by CCP + +services: + # ─── Core Infrastructure ─────────────────────────────────── + + v2-postgres: + image: {{registryUrl}}/postgres:16-alpine + container_name: {{containerPrefix}}-postgres + restart: unless-stopped + environment: + POSTGRES_USER: changemaker + POSTGRES_PASSWORD: "{{secrets.postgresPassword}}" + POSTGRES_DB: changemaker_v2 + volumes: + - {{containerPrefix}}-postgres-data:/var/lib/postgresql/data + - ./api/prisma/init-nocodb-db.sh:/docker-entrypoint-initdb.d/10-init-nocodb.sh:ro + - ./api/prisma/init-gancio-db.sh:/docker-entrypoint-initdb.d/20-init-gancio.sh:ro + - ./api/prisma/init-gitea-db.sh:/docker-entrypoint-initdb.d/30-init-gitea.sh:ro + ports: + - "127.0.0.1:{{ports.postgres}}:5432" + networks: + - {{networkName}} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U changemaker -d changemaker_v2"] + interval: 10s + timeout: 5s + retries: 5 + + redis: + image: {{registryUrl}}/redis:7-alpine + container_name: {{containerPrefix}}-redis + restart: unless-stopped + command: "redis-server --appendonly yes --maxmemory 512mb --maxmemory-policy noeviction --requirepass {{secrets.redisPassword}}" + volumes: + - {{containerPrefix}}-redis-data:/data + networks: + - {{networkName}} + healthcheck: + test: ["CMD", "redis-cli", "-a", "{{secrets.redisPassword}}", "ping"] + interval: 10s + timeout: 5s + retries: 5 + deploy: + resources: + limits: + cpus: '1' + memory: 512M + reservations: + cpus: '0.25' + memory: 256M + logging: + driver: "json-file" + options: + max-size: "5m" + max-file: "2" + + # ─── Application Services ────────────────────────────────── + + api: +{{#if useRegistry}} + image: {{registryUrl}}/changemaker-api:{{imageTag}} +{{else}} + build: + context: ./api + dockerfile: Dockerfile + target: development +{{/if}} + container_name: {{containerPrefix}}-api + restart: unless-stopped + depends_on: + v2-postgres: + condition: service_healthy + redis: + condition: service_healthy + env_file: .env + environment: + DATABASE_URL: "postgresql://changemaker:{{secrets.postgresPassword}}@{{containerPrefix}}-postgres:5432/changemaker_v2" + REDIS_URL: "redis://:{{secrets.redisPassword}}@{{containerPrefix}}-redis:6379" + PORT: "4000" + NAR_DATA_DIR: /data + LISTMONK_URL: http://{{containerPrefix}}-listmonk:9000 + ADMIN_URL: https://app.{{domain}} + API_URL: https://api.{{domain}} +{{#if enableGancio}} + GANCIO_URL: http://{{containerPrefix}}-gancio:13120 +{{/if}} + ENABLE_MEET: "{{#if enableMeet}}true{{else}}false{{/if}}" + ENABLE_SMS: "{{#if enableSms}}true{{else}}false{{/if}}" + ENABLE_SOCIAL: "{{#if enableSocial}}true{{else}}false{{/if}}" + ENABLE_PEOPLE: "{{#if enablePeople}}true{{else}}false{{/if}}" +{{#if enableMeet}} + JITSI_APP_ID: changemaker + JITSI_APP_SECRET: "{{secrets.jitsiAppSecret}}" + JITSI_URL: http://{{containerPrefix}}-jitsi-web:80 +{{/if}} +{{#if enableChat}} + ROCKETCHAT_URL: http://{{containerPrefix}}-rocketchat:3000 +{{/if}} + ports: + - "{{ports.api}}:4000" +{{#if enableListmonk}} + - "9002:9002" +{{/if}} + volumes: + - ./assets/uploads:/app/uploads + - ./mkdocs:/mkdocs:rw + - ./data:/data:ro + - ./data/upgrade:/app/upgrade:rw + - ./configs:/app/configs:ro + networks: + - {{networkName}} + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://localhost:4000/api/health"] + interval: 15s + timeout: 5s + retries: 3 + start_period: 30s + deploy: + resources: + limits: + cpus: '2' + memory: 1G + reservations: + cpus: '0.25' + memory: 256M + + admin: +{{#if useRegistry}} + image: {{registryUrl}}/changemaker-admin:{{imageTag}} +{{else}} + build: + context: ./admin + target: development +{{/if}} + container_name: {{containerPrefix}}-admin + restart: unless-stopped + depends_on: + - api + environment: + DOMAIN: {{domain}} + NODE_ENV: production + VITE_API_URL: http://{{containerPrefix}}-api:4000 + VITE_MKDOCS_URL: http://{{containerPrefix}}-mkdocs:8000 + VITE_DOMAIN: {{domain}} + VITE_MKDOCS_SITE_PORT: "{{math ports.embed "+" 14}}" +{{#if enableMedia}} + VITE_MEDIA_API_URL: http://{{containerPrefix}}-media-api:4100 +{{/if}} + ports: + - "{{ports.admin}}:3000" + networks: + - {{networkName}} + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:3000/"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 20s + +{{#if enableMedia}} + media-api: +{{#if useRegistry}} + image: {{registryUrl}}/changemaker-media-api:{{imageTag}} +{{else}} + build: + context: ./api + dockerfile: Dockerfile.media + target: development +{{/if}} + container_name: {{containerPrefix}}-media-api + restart: unless-stopped + depends_on: + v2-postgres: + condition: service_healthy + redis: + condition: service_healthy + env_file: .env + environment: + DATABASE_URL: "postgresql://changemaker:{{secrets.postgresPassword}}@{{containerPrefix}}-postgres:5432/changemaker_v2" + REDIS_URL: "redis://:{{secrets.redisPassword}}@{{containerPrefix}}-redis:6379" + MEDIA_API_PORT: "4100" + CORS_ORIGINS: https://app.{{domain}},http://localhost:{{ports.admin}} + ENABLE_MEDIA_FEATURES: "true" + MEDIA_ROOT: /media/local + MEDIA_UPLOADS: /media/uploads + volumes: + - ./media:/media:ro + - ./media/local/inbox:/media/local/inbox:rw + - ./media/local/thumbnails:/media/local/thumbnails:rw + - ./media/local/photos:/media/local/photos:rw + - ./media/public:/media/public:rw + networks: + - {{networkName}} + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:4100/health"] + interval: 15s + timeout: 5s + retries: 3 + start_period: 30s + deploy: + resources: + limits: + cpus: '2' + memory: 1G + reservations: + cpus: '0.25' + memory: 256M +{{/if}} + + # ─── Reverse Proxy ───────────────────────────────────────── + + nginx: + image: {{registryUrl}}/nginx:alpine + container_name: {{containerPrefix}}-nginx + restart: unless-stopped + depends_on: + - api + - admin + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ./nginx/conf.d:/etc/nginx/conf.d:ro + ports: + - "{{ports.nginx}}:80" + - "{{math ports.embed "+" 0}}:8881" # NocoDB embed proxy + - "{{math ports.embed "+" 1}}:8882" # n8n embed proxy + - "{{math ports.embed "+" 2}}:8883" # Gitea embed proxy + - "{{math ports.embed "+" 3}}:8884" # MailHog embed proxy + - "{{math ports.embed "+" 4}}:8885" # Mini QR embed proxy + - "{{math ports.embed "+" 5}}:8886" # Excalidraw embed proxy + - "{{math ports.embed "+" 6}}:8887" # Homepage embed proxy + - "{{math ports.embed "+" 7}}:8888" # Code Server embed proxy + - "{{math ports.embed "+" 8}}:8889" # MkDocs embed proxy + - "{{math ports.embed "+" 9}}:8890" # Vaultwarden embed proxy + - "{{math ports.embed "+" 10}}:8891" # Rocket.Chat embed proxy + - "{{math ports.embed "+" 11}}:8892" # Gancio embed proxy + - "{{math ports.embed "+" 12}}:8893" # Grafana embed proxy + - "{{math ports.embed "+" 13}}:8894" # Listmonk embed proxy + - "{{math ports.embed "+" 14}}:8895" # MkDocs site embed proxy + - "{{math ports.embed "+" 15}}:8896" # Jitsi Meet embed proxy + networks: + - {{networkName}} + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:80/"] + interval: 30s + timeout: 5s + retries: 3 + + # ─── Supporting Services ─────────────────────────────────── + + nocodb-v2: + image: {{registryUrl}}/nocodb:0.301.3 + container_name: {{containerPrefix}}-nocodb + restart: unless-stopped + depends_on: + v2-postgres: + condition: service_healthy + environment: + NC_DB: "pg://{{containerPrefix}}-postgres:5432?u=changemaker&p={{secrets.postgresPassword}}&d=nocodb_meta" + NC_ADMIN_EMAIL: "{{secrets.adminEmail}}" + NC_ADMIN_PASSWORD: "{{secrets.nocodbAdminPassword}}" + volumes: + - {{containerPrefix}}-nocodb-data:/usr/app/data + networks: + - {{networkName}} + + mailhog: + image: {{registryUrl}}/mailhog:v1.0.1 + container_name: {{containerPrefix}}-mailhog + restart: unless-stopped + networks: + - {{networkName}} + logging: + driver: "json-file" + options: + max-size: "5m" + max-file: "2" + + mkdocs: + image: {{registryUrl}}/mkdocs-material:latest + container_name: {{containerPrefix}}-mkdocs + restart: unless-stopped + volumes: + - ./mkdocs:/docs:rw + - ./assets/images:/docs/assets/images:rw + user: "1000:1000" + environment: + SITE_URL: https://{{domain}} + ADMIN_PORT: "{{ports.admin}}" + ADMIN_URL: https://app.{{domain}} + BASE_DOMAIN: https://{{domain}} + API_URL: https://api.{{domain}} + API_PORT: "{{ports.api}}" +{{#if enableMedia}} + MEDIA_API_PUBLIC_URL: https://media.{{domain}} + MEDIA_API_PORT: "4100" +{{/if}} +{{#if enableGancio}} + GANCIO_URL: http://{{containerPrefix}}-gancio:13120 + GANCIO_PORT: "8092" +{{/if}} + command: serve --dev-addr=0.0.0.0:8000 --watch-theme --livereload + networks: + - {{networkName}} + +{{#if enableListmonk}} + listmonk-db: + image: {{registryUrl}}/postgres:17-alpine + container_name: {{containerPrefix}}-listmonk-db + restart: unless-stopped + environment: + POSTGRES_USER: listmonk + POSTGRES_PASSWORD: "{{secrets.listmonkAdminPassword}}" + POSTGRES_DB: listmonk + volumes: + - {{containerPrefix}}-listmonk-data:/var/lib/postgresql/data + networks: + - {{networkName}} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U listmonk"] + interval: 10s + timeout: 5s + retries: 6 + + listmonk-app: + image: {{registryUrl}}/listmonk:v6.0.0 + container_name: {{containerPrefix}}-listmonk + restart: unless-stopped + depends_on: + listmonk-db: + condition: service_healthy + command: [sh, -c, "./listmonk --install --idempotent --yes --config '' && ./listmonk --upgrade --yes --config '' && ./listmonk --config ''"] + environment: + LISTMONK_app__address: "0.0.0.0:9000" + LISTMONK_db__host: {{containerPrefix}}-listmonk-db + LISTMONK_db__port: "5432" + LISTMONK_db__user: listmonk + LISTMONK_db__password: "{{secrets.listmonkAdminPassword}}" + LISTMONK_db__database: listmonk + LISTMONK_db__ssl_mode: disable + TZ: Etc/UTC + LISTMONK_ADMIN_USER: admin + LISTMONK_ADMIN_PASSWORD: "{{secrets.listmonkAdminPassword}}" + volumes: + - ./assets/uploads:/listmonk/uploads:rw + networks: + - {{networkName}} + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://localhost:9000/"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 30s + + listmonk-init: + image: {{registryUrl}}/postgres:17-alpine + container_name: {{containerPrefix}}-listmonk-init + depends_on: + listmonk-app: + condition: service_started + restart: "no" + environment: + PGPASSWORD: "{{secrets.listmonkAdminPassword}}" + LISTMONK_API_USER: v2-api + LISTMONK_API_TOKEN: "{{secrets.listmonkApiToken}}" + LISTMONK_SMTP_HOST: {{containerPrefix}}-mailhog + LISTMONK_SMTP_PORT: "1025" + entrypoint: ["/bin/sh", "-c"] + command: + - | + echo "[listmonk-init] Waiting for Listmonk tables..." + for i in $$(seq 1 30); do + if psql -h {{containerPrefix}}-listmonk-db -U listmonk -d listmonk -c "SELECT 1 FROM users LIMIT 1" >/dev/null 2>&1; then + break + fi + sleep 2 + done + + if [ -n "$$LISTMONK_API_TOKEN" ]; then + echo "[listmonk-init] Upserting API user '$$LISTMONK_API_USER'..." + psql -h {{containerPrefix}}-listmonk-db -U listmonk -d listmonk -q < /gancio-data/config.json <<'EOF' + { + "baseurl": "https://events.{{domain}}", + "title": "Events", + "description": "Community Events", + "server": { "host": "0.0.0.0", "port": 13120 }, + "db": { + "dialect": "postgres", + "host": "{{containerPrefix}}-postgres", + "port": 5432, + "database": "gancio", + "username": "changemaker", + "password": "{{secrets.postgresPassword}}" + } + } + EOF + echo "[gancio-config] config.json created" + else + echo "[gancio-config] config.json already exists, skipping" + fi + restart: "no" + networks: + - {{networkName}} + + gancio: + image: {{registryUrl}}/gancio:1.28.2 + container_name: {{containerPrefix}}-gancio + restart: unless-stopped + depends_on: + v2-postgres: + condition: service_healthy + gancio-config: + condition: service_completed_successfully + environment: + GANCIO_DATA: /home/node/data + NODE_ENV: production + GANCIO_DB_DIALECT: postgres + GANCIO_DB_HOST: {{containerPrefix}}-postgres + GANCIO_DB_PORT: "5432" + GANCIO_DB_DATABASE: gancio + GANCIO_DB_USERNAME: changemaker + GANCIO_DB_PASSWORD: "{{secrets.postgresPassword}}" + server__baseurl: https://events.{{domain}} + volumes: + - {{containerPrefix}}-gancio-data:/home/node/data + networks: + - {{networkName}} + healthcheck: + test: ["CMD", "node", "-e", "require('http').get('http://localhost:13120/', r => process.exit(r.statusCode < 400 ? 0 : 1)).on('error', () => process.exit(1))"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 60s + + # Gancio post-start seeder — seeds theme settings after Gancio creates its tables + gancio-init: + image: {{registryUrl}}/postgres:16-alpine + container_name: {{containerPrefix}}-gancio-init + depends_on: + gancio: + condition: service_healthy + environment: + PGHOST: {{containerPrefix}}-postgres + PGUSER: changemaker + PGPASSWORD: "{{secrets.postgresPassword}}" + PGDATABASE: gancio + entrypoint: ["/bin/sh", "-c"] + command: + - | + echo "[gancio-init] Seeding Gancio default theme settings..." + psql -c "INSERT INTO settings (key, value, is_secret, \"createdAt\", \"updatedAt\") VALUES + ('dark_colors', '{\"primary\": \"#FF6E40\", \"error\": \"#FF5252\", \"info\": \"#2196F3\", \"success\": \"#4CAF50\", \"warning\": \"#FB8C00\"}', false, NOW(), NOW()), + ('light_colors', '{\"primary\": \"#FF4500\", \"error\": \"#FF5252\", \"info\": \"#2196F3\", \"success\": \"#4CAF50\", \"warning\": \"#FB8C00\"}', false, NOW(), NOW()) + ON CONFLICT (key) DO NOTHING;" + echo "[gancio-init] Theme settings seeded" + echo "[gancio-init] Done" + restart: "no" + networks: + - {{networkName}} +{{/if}} + +{{#if enableChat}} + nats-rocketchat: + image: {{registryUrl}}/nats:2.11-alpine + container_name: {{containerPrefix}}-nats + restart: unless-stopped + command: --http_port 8222 + networks: + - {{networkName}} + + mongodb-rocketchat: + image: {{registryUrl}}/mongo:6.0 + container_name: {{containerPrefix}}-mongodb + restart: unless-stopped + entrypoint: ["/bin/bash", "-c", "if [ ! -f /data/replica.key ]; then openssl rand -base64 756 > /data/replica.key; fi && chmod 400 /data/replica.key && chown 999:999 /data/replica.key && exec mongod --replSet rs0 --bind_ip_all --auth --keyFile /data/replica.key"] + environment: + MONGO_INITDB_ROOT_USERNAME: "${MONGO_ROOT_USER:-rocketchat}" + MONGO_INITDB_ROOT_PASSWORD: "${MONGO_ROOT_PASSWORD}" + volumes: + - {{containerPrefix}}-mongodb-data:/data/db + networks: + - {{networkName}} + healthcheck: + test: ["CMD", "mongosh", "-u", "${MONGO_ROOT_USER:-rocketchat}", "-p", "${MONGO_ROOT_PASSWORD}", "--authenticationDatabase", "admin", "--quiet", "--eval", "try { rs.status().ok } catch(e) { rs.initiate({_id:'rs0',members:[{_id:0,host:'{{containerPrefix}}-mongodb:27017'}]}).ok }"] + interval: 10s + timeout: 10s + retries: 10 + start_period: 30s + + rocketchat: + image: {{registryUrl}}/rocketchat:7.9.7 + container_name: {{containerPrefix}}-rocketchat + restart: unless-stopped + depends_on: + mongodb-rocketchat: + condition: service_healthy + nats-rocketchat: + condition: service_started + environment: + ROOT_URL: http://chat.{{domain}} + MONGO_URL: mongodb://${MONGO_ROOT_USER:-rocketchat}:${MONGO_ROOT_PASSWORD}@{{containerPrefix}}-mongodb:27017/rocketchat?replicaSet=rs0&authSource=admin + MONGO_OPLOG_URL: mongodb://${MONGO_ROOT_USER:-rocketchat}:${MONGO_ROOT_PASSWORD}@{{containerPrefix}}-mongodb:27017/local?replicaSet=rs0&authSource=admin + TRANSPORTER: monolith+nats://{{containerPrefix}}-nats:4222 + PORT: "3000" + ADMIN_USERNAME: rcadmin + ADMIN_NAME: Admin + ADMIN_EMAIL: "{{secrets.adminEmail}}" + ADMIN_PASS: "{{secrets.rocketchatAdminPassword}}" + CREATE_TOKENS_FOR_USERS: "true" + OVERWRITE_SETTING_Iframe_Integration_send_enable: "true" + OVERWRITE_SETTING_Iframe_Integration_receive_enable: "true" + OVERWRITE_SETTING_Iframe_Integration_receive_origin: http://app.{{domain}},https://app.{{domain}} +{{#if enableMeet}} + OVERWRITE_SETTING_Jitsi_Enabled: "true" + OVERWRITE_SETTING_Jitsi_Domain: meet.{{domain}} + OVERWRITE_SETTING_Jitsi_URL_Room_Prefix: RocketChat + OVERWRITE_SETTING_Jitsi_Enable_Channels: "true" + OVERWRITE_SETTING_Jitsi_Open_New_Window: "false" + OVERWRITE_SETTING_Jitsi_Enabled_TokenAuth: "true" + OVERWRITE_SETTING_Jitsi_Application_ID: changemaker + OVERWRITE_SETTING_Jitsi_Application_Secret: "{{secrets.jitsiAppSecret}}" + OVERWRITE_SETTING_VideoConf_Default_Provider: jitsi +{{/if}} + volumes: + - {{containerPrefix}}-rocketchat-uploads:/app/uploads + networks: + - {{networkName}} + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:3000/api/info"] + interval: 30s + timeout: 10s + retries: 10 + start_period: 90s +{{/if}} + +{{#if enablePangolin}} + # ─── Pangolin Tunnel ─────────────────────────────────────── + + newt: + image: {{registryUrl}}/newt:latest + container_name: {{containerPrefix}}-newt + restart: unless-stopped + depends_on: + - nginx + environment: + PANGOLIN_ENDPOINT: "{{pangolin.endpoint}}" + NEWT_ID: "{{pangolin.newtId}}" + NEWT_SECRET: "{{pangolin.newtSecret}}" + networks: + - {{networkName}} +{{/if}} + +{{#if enableMeet}} + # ─── Jitsi Meet (Video Conferencing) ──────────────────── + + jitsi-web: + image: {{registryUrl}}/jitsi-web:stable-9823 + container_name: {{containerPrefix}}-jitsi-web + restart: unless-stopped + depends_on: + - jitsi-prosody + environment: + XMPP_SERVER: {{containerPrefix}}-jitsi-prosody + XMPP_DOMAIN: meet.jitsi + XMPP_AUTH_DOMAIN: auth.meet.jitsi + XMPP_BOSH_URL_BASE: http://{{containerPrefix}}-jitsi-prosody:5280 + XMPP_MUC_DOMAIN: muc.meet.jitsi + PUBLIC_URL: https://meet.{{domain}} + TZ: America/Edmonton + ENABLE_AUTH: "1" + AUTH_TYPE: jwt + JWT_APP_ID: changemaker + JWT_APP_SECRET: "{{secrets.jitsiAppSecret}}" + JWT_ACCEPTED_ISSUERS: changemaker + JWT_ACCEPTED_AUDIENCES: changemaker + volumes: + - {{containerPrefix}}-jitsi-web-config:/config + networks: + - {{networkName}} + + jitsi-prosody: + image: {{registryUrl}}/jitsi-prosody:stable-9823 + container_name: {{containerPrefix}}-jitsi-prosody + restart: unless-stopped + environment: + XMPP_DOMAIN: meet.jitsi + XMPP_AUTH_DOMAIN: auth.meet.jitsi + XMPP_MUC_DOMAIN: muc.meet.jitsi + XMPP_INTERNAL_MUC_DOMAIN: internal-muc.meet.jitsi + XMPP_RECORDER_DOMAIN: recorder.meet.jitsi + XMPP_CROSS_DOMAIN: "true" + JICOFO_AUTH_USER: focus + JICOFO_AUTH_PASSWORD: "{{secrets.jitsiJicofoAuthPassword}}" + JVB_AUTH_USER: jvb + JVB_AUTH_PASSWORD: "{{secrets.jitsiJvbAuthPassword}}" + TZ: America/Edmonton + ENABLE_AUTH: "1" + AUTH_TYPE: jwt + JWT_APP_ID: changemaker + JWT_APP_SECRET: "{{secrets.jitsiAppSecret}}" + JWT_ACCEPTED_ISSUERS: changemaker + JWT_ACCEPTED_AUDIENCES: changemaker + JWT_ALLOW_EMPTY: "0" + volumes: + - {{containerPrefix}}-jitsi-prosody-config:/config + - {{containerPrefix}}-jitsi-prosody-plugins:/prosody-plugins-custom + networks: + - {{networkName}} + + jitsi-jicofo: + image: {{registryUrl}}/jitsi-jicofo:stable-9823 + container_name: {{containerPrefix}}-jitsi-jicofo + restart: unless-stopped + depends_on: + - jitsi-prosody + environment: + XMPP_SERVER: {{containerPrefix}}-jitsi-prosody + XMPP_DOMAIN: meet.jitsi + XMPP_AUTH_DOMAIN: auth.meet.jitsi + XMPP_INTERNAL_MUC_DOMAIN: internal-muc.meet.jitsi + XMPP_MUC_DOMAIN: muc.meet.jitsi + JICOFO_AUTH_USER: focus + JICOFO_AUTH_PASSWORD: "{{secrets.jitsiJicofoAuthPassword}}" + TZ: America/Edmonton + volumes: + - {{containerPrefix}}-jitsi-jicofo-config:/config + networks: + - {{networkName}} + + jitsi-jvb: + image: {{registryUrl}}/jitsi-jvb:stable-9823 + container_name: {{containerPrefix}}-jitsi-jvb + restart: unless-stopped + depends_on: + - jitsi-prosody + environment: + XMPP_SERVER: {{containerPrefix}}-jitsi-prosody + XMPP_DOMAIN: meet.jitsi + XMPP_AUTH_DOMAIN: auth.meet.jitsi + XMPP_INTERNAL_MUC_DOMAIN: internal-muc.meet.jitsi + JVB_AUTH_USER: jvb + JVB_AUTH_PASSWORD: "{{secrets.jitsiJvbAuthPassword}}" + JVB_STUN_SERVERS: meet-jit-si-turnrelay.jitsi.net:443 + JVB_PORT: "10000" + JVB_ADVERTISE_IPS: "{{jvbAdvertiseIp}}" + TZ: America/Edmonton + ports: + - "10000:10000/udp" + volumes: + - {{containerPrefix}}-jitsi-jvb-config:/config + networks: + - {{networkName}} +{{/if}} + + # ─── Always-On Utilities ────────────────────────────────── + + mini-qr: + image: {{registryUrl}}/mini-qr:v0.26.0 + container_name: {{containerPrefix}}-mini-qr + restart: unless-stopped + networks: + - {{networkName}} + + mkdocs-site-server: + image: {{registryUrl}}/nginx:alpine + container_name: {{containerPrefix}}-mkdocs-site + restart: unless-stopped + volumes: + - ./mkdocs/site:/usr/share/nginx/html:ro + networks: + - {{networkName}} + +{{#if enableDevTools}} + # ─── Dev Tools ──────────────────────────────────────────── + + code-server: + image: {{registryUrl}}/code-server:latest + container_name: {{containerPrefix}}-code-server + restart: unless-stopped + environment: + PASSWORD: "{{secrets.nocodbAdminPassword}}" + SUDO_PASSWORD: "{{secrets.nocodbAdminPassword}}" + volumes: + - .:/config/workspace:rw + networks: + - {{networkName}} + + gitea: + image: {{registryUrl}}/gitea:1.23.7 + container_name: {{containerPrefix}}-gitea + restart: unless-stopped + depends_on: + v2-postgres: + condition: service_healthy + environment: + GITEA__database__DB_TYPE: postgres + GITEA__database__HOST: {{containerPrefix}}-postgres:5432 + GITEA__database__NAME: gitea + GITEA__database__USER: changemaker + GITEA__database__PASSWD: "{{secrets.postgresPassword}}" + GITEA__server__ROOT_URL: https://git.{{domain}} + GITEA__server__DOMAIN: git.{{domain}} + GITEA__server__HTTP_PORT: "3000" + GITEA__server__PROTOCOL: http + GITEA__server__ENABLE_GZIP: "true" + GITEA__server__X_FRAME_OPTIONS: "" + GITEA__security__INSTALL_LOCK: "true" + GITEA__attachment__MAX_SIZE: "1024" + GITEA__repository__MAX_CREATION_LIMIT: "-1" + GITEA__server__LFS_START_SERVER: "true" + # Reverse proxy auth — nginx injects X-WEBAUTH-USER for SSO + GITEA__service__ENABLE_REVERSE_PROXY_AUTHENTICATION: "true" + GITEA__service__ENABLE_REVERSE_PROXY_AUTO_REGISTRATION: "false" + GITEA__service__ENABLE_REVERSE_PROXY_EMAIL: "false" + GITEA__service__REVERSE_PROXY_AUTHENTICATION_HEADER: "X-WEBAUTH-USER" + GITEA__service__REQUIRE_SIGNIN_VIEW: "true" + volumes: + - {{containerPrefix}}-gitea-data:/data + networks: + - {{networkName}} + healthcheck: + test: ["CMD", "curl", "-fsSL", "http://localhost:3000/api/healthz"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 30s + + # Gitea init — creates admin user after Gitea is healthy (idempotent) + # Must run as git user (UID 1000) — Gitea refuses to run as root + gitea-init: + image: {{registryUrl}}/gitea:1.23.7 + container_name: {{containerPrefix}}-gitea-init + user: "1000:1000" + depends_on: + gitea: + condition: service_healthy + environment: + GITEA__database__DB_TYPE: postgres + GITEA__database__HOST: {{containerPrefix}}-postgres:5432 + GITEA__database__NAME: gitea + GITEA__database__USER: changemaker + GITEA__database__PASSWD: "{{secrets.postgresPassword}}" + GITEA__security__INSTALL_LOCK: "true" + volumes: + - {{containerPrefix}}-gitea-data:/data + entrypoint: ["/bin/sh", "-c"] + command: + - | + echo "[gitea-init] Running migrations (idempotent)..." + gitea migrate 2>/dev/null || true + + if gitea admin user list --admin 2>/dev/null | grep -q "admin"; then + echo "[gitea-init] Admin user already exists, skipping" + else + echo "[gitea-init] Creating admin user..." + gitea admin user create \ + --admin \ + --username admin \ + --password "{{secrets.giteaAdminPassword}}" \ + --email "{{secrets.adminEmail}}" \ + --must-change-password=false + echo "[gitea-init] Admin user created" + fi + + echo "[gitea-init] Done" + restart: "no" + networks: + - {{networkName}} + + n8n: + image: {{registryUrl}}/n8n:latest + container_name: {{containerPrefix}}-n8n + restart: unless-stopped + environment: + N8N_ENCRYPTION_KEY: "{{secrets.n8nEncryptionKey}}" + WEBHOOK_URL: https://n8n.{{domain}} + N8N_HOST: n8n.{{domain}} + N8N_PROTOCOL: https + N8N_SECURE_COOKIE: "false" + volumes: + - {{containerPrefix}}-n8n-data:/home/node/.n8n + networks: + - {{networkName}} + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://localhost:5678/healthz"] + interval: 30s + timeout: 5s + retries: 3 + + docker-socket-proxy: + image: {{registryUrl}}/docker-socket-proxy:v0.4.2 + container_name: {{containerPrefix}}-docker-socket-proxy + restart: unless-stopped + environment: + CONTAINERS: 1 + IMAGES: 1 + INFO: 1 + NETWORKS: 0 + VOLUMES: 0 + POST: 0 + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + networks: + - {{networkName}} + + homepage: + image: {{registryUrl}}/homepage:v0.7.2 + container_name: {{containerPrefix}}-homepage + restart: unless-stopped + environment: + DOCKER_HOST: tcp://{{containerPrefix}}-docker-socket-proxy:2375 + volumes: + - {{containerPrefix}}-homepage-data:/app/config + depends_on: + - docker-socket-proxy + networks: + - {{networkName}} + + excalidraw: + image: {{registryUrl}}/excalidraw:latest + container_name: {{containerPrefix}}-excalidraw + restart: unless-stopped + networks: + - {{networkName}} + + # Vaultwarden — Password manager (Bitwarden-compatible) + vaultwarden: + image: {{registryUrl}}/vaultwarden:1.35.4 + container_name: {{containerPrefix}}-vaultwarden + restart: unless-stopped + environment: + ADMIN_TOKEN: "{{secrets.vaultwardenAdminToken}}" + DOMAIN: https://vault.{{domain}} + SIGNUPS_ALLOWED: "false" + WEBSOCKET_ENABLED: "true" + ROCKET_PORT: "80" + LOG_LEVEL: info + SMTP_HOST: {{containerPrefix}}-mailhog + SMTP_PORT: "1025" + SMTP_FROM: "noreply@{{domain}}" + SMTP_FROM_NAME: Vaultwarden + SMTP_SECURITY: "off" + SMTP_USERNAME: "" + SMTP_PASSWORD: "" + volumes: + - {{containerPrefix}}-vaultwarden-data:/data + networks: + - {{networkName}} + healthcheck: + test: ["CMD", "curl", "-sf", "http://localhost:80/alive"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 15s + + # Vaultwarden init — invites the initial admin user (safe to re-run) + vaultwarden-init: + image: {{registryUrl}}/alpine-curl:8.11.1 + container_name: {{containerPrefix}}-vaultwarden-init + depends_on: + vaultwarden: + condition: service_healthy + restart: "no" + entrypoint: ["/bin/sh", "-c"] + command: + - | + echo "[vaultwarden-init] Waiting for Vaultwarden..." + for i in $$(seq 1 20); do + if curl -sf http://{{containerPrefix}}-vaultwarden:80/alive >/dev/null 2>&1; then + break + fi + sleep 2 + done + + VAULTWARDEN_ADMIN_TOKEN="{{secrets.vaultwardenAdminToken}}" + VAULTWARDEN_URL="http://{{containerPrefix}}-vaultwarden:80" + INVITE_EMAIL="{{secrets.adminEmail}}" + + if [ -z "$$VAULTWARDEN_ADMIN_TOKEN" ]; then + echo "[vaultwarden-init] VAULTWARDEN_ADMIN_TOKEN not set, skipping invite" + exit 0 + fi + + echo "[vaultwarden-init] Authenticating with admin panel..." + SESSION_COOKIE=$$(mktemp) + HTTP_CODE=$$(curl -s -o /dev/null -w "%{http_code}" \ + -c "$$SESSION_COOKIE" \ + -X POST "$$VAULTWARDEN_URL/admin" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "token=$$VAULTWARDEN_ADMIN_TOKEN") + + if [ "$$HTTP_CODE" != "200" ] && [ "$$HTTP_CODE" != "302" ]; then + echo "[vaultwarden-init] Admin auth failed (HTTP $$HTTP_CODE)" + rm -f "$$SESSION_COOKIE" + exit 1 + fi + echo "[vaultwarden-init] Authenticated" + + echo "[vaultwarden-init] Inviting $$INVITE_EMAIL..." + INVITE_CODE=$$(curl -s -w "\n%{http_code}" \ + -b "$$SESSION_COOKIE" \ + -X POST "$$VAULTWARDEN_URL/admin/invite" \ + -H "Content-Type: application/json" \ + -d "{\"email\":\"$$INVITE_EMAIL\"}") + + INVITE_HTTP=$$(echo "$$INVITE_CODE" | tail -1) + INVITE_BODY=$$(echo "$$INVITE_CODE" | head -n -1) + + if [ "$$INVITE_HTTP" = "200" ] || [ "$$INVITE_HTTP" = "422" ]; then + echo "[vaultwarden-init] Invite sent (or user already exists)" + else + echo "[vaultwarden-init] Invite failed (HTTP $$INVITE_HTTP): $$INVITE_BODY" + fi + + rm -f "$$SESSION_COOKIE" + echo "[vaultwarden-init] Done" + networks: + - {{networkName}} +{{/if}} + +{{#if enableMonitoring}} + # ─── Monitoring Stack ────────────────────────────────────── + + prometheus: + image: {{registryUrl}}/prometheus:v3.10.0 + container_name: {{containerPrefix}}-prometheus + restart: unless-stopped + command: + - '--config.file=/etc/prometheus/prometheus.yml' + - '--storage.tsdb.path=/prometheus' + - '--storage.tsdb.retention.time=30d' + volumes: + - ./configs/prometheus:/etc/prometheus:ro + - {{containerPrefix}}-prometheus-data:/prometheus + networks: + - {{networkName}} + + grafana: + image: {{registryUrl}}/grafana:12.3.0 + container_name: {{containerPrefix}}-grafana + restart: unless-stopped + environment: + GF_SECURITY_ADMIN_PASSWORD: "{{secrets.grafanaAdminPassword}}" + GF_USERS_ALLOW_SIGN_UP: "false" + GF_SERVER_ROOT_URL: https://grafana.{{domain}} + GF_SECURITY_ALLOW_EMBEDDING: "true" + volumes: + - {{containerPrefix}}-grafana-data:/var/lib/grafana + - ./configs/grafana:/etc/grafana/provisioning + depends_on: + - prometheus + networks: + - {{networkName}} + + alertmanager: + image: {{registryUrl}}/alertmanager:v0.31.1 + container_name: {{containerPrefix}}-alertmanager + restart: unless-stopped + command: + - '--config.file=/etc/alertmanager/alertmanager.yml' + - '--storage.path=/alertmanager' + volumes: + - ./configs/alertmanager:/etc/alertmanager:ro + - {{containerPrefix}}-alertmanager-data:/alertmanager + networks: + - {{networkName}} + + cadvisor: + image: {{registryUrl}}/cadvisor:v0.55.1 + container_name: {{containerPrefix}}-cadvisor + restart: unless-stopped + volumes: + - /:/rootfs:ro + - /var/run:/var/run:ro + - /sys:/sys:ro + - /var/lib/docker/:/var/lib/docker:ro + networks: + - {{networkName}} + + node-exporter: + image: {{registryUrl}}/node-exporter:v1.10.2 + container_name: {{containerPrefix}}-node-exporter + restart: unless-stopped + command: + - '--path.rootfs=/host' + volumes: + - /:/host:ro,rslave + networks: + - {{networkName}} + + redis-exporter: + image: {{registryUrl}}/redis_exporter:v1.81.0 + container_name: {{containerPrefix}}-redis-exporter + restart: unless-stopped + environment: + REDIS_ADDR: redis://{{containerPrefix}}-redis:6379 + REDIS_PASSWORD: "{{secrets.redisPassword}}" + depends_on: + - redis + networks: + - {{networkName}} + + gotify: + image: {{registryUrl}}/gotify:v2.9.0 + container_name: {{containerPrefix}}-gotify + restart: unless-stopped + volumes: + - {{containerPrefix}}-gotify-data:/app/data + networks: + - {{networkName}} +{{/if}} + +# ─── Volumes ────────────────────────────────────────────── + +volumes: + {{containerPrefix}}-postgres-data: + {{containerPrefix}}-redis-data: + {{containerPrefix}}-nocodb-data: +{{#if enableListmonk}} + {{containerPrefix}}-listmonk-data: +{{/if}} +{{#if enableGancio}} + {{containerPrefix}}-gancio-data: +{{/if}} +{{#if enableChat}} + {{containerPrefix}}-mongodb-data: + {{containerPrefix}}-rocketchat-uploads: +{{/if}} +{{#if enableDevTools}} + {{containerPrefix}}-gitea-data: + {{containerPrefix}}-n8n-data: + {{containerPrefix}}-homepage-data: + {{containerPrefix}}-vaultwarden-data: +{{/if}} +{{#if enableMonitoring}} + {{containerPrefix}}-prometheus-data: + {{containerPrefix}}-grafana-data: + {{containerPrefix}}-alertmanager-data: + {{containerPrefix}}-gotify-data: +{{/if}} +{{#if enableMeet}} + {{containerPrefix}}-jitsi-web-config: + {{containerPrefix}}-jitsi-prosody-config: + {{containerPrefix}}-jitsi-prosody-plugins: + {{containerPrefix}}-jitsi-jicofo-config: + {{containerPrefix}}-jitsi-jvb-config: +{{/if}} + +# ─── Networks ───────────────────────────────────────────── + +networks: + {{networkName}}: + driver: bridge diff --git a/docs/SESSION_HANDOFF_2026-05-21.md b/docs/SESSION_HANDOFF_2026-05-21.md new file mode 100644 index 0000000..b041006 --- /dev/null +++ b/docs/SESSION_HANDOFF_2026-05-21.md @@ -0,0 +1,169 @@ +# Session Handoff: Approach B Rollout + Approach C Planning (2026-05-21) + +Carries forward all context from a long working session. If you're a fresh agent: read this top-to-bottom before touching anything. + +--- + +## What landed in this session (commits on origin/main) + +| Commit | Description | +|---|---| +| `4a3d9d7` | `feat(upgrade): Approach B - image-only upgrade mode` — 7 files, 666 insertions. scripts/image-upgrade.sh + CCP agent endpoint + CCP backend (driver/service/route/schema) + admin UI "Quick Upgrade" button. | +| `` | docs: session handoff + Approach C Phase 0 initial template overlay | + +Plus several non-tracked deploys: +- v2.10.2 surgical update applied to remaining 6 tenants (soroush, linda, marcelle, bnkops, trbh, pridecorner — pia was done previously). All verified mkdocs untouched, upgrade.sh sha matches `b9f37d59...`. +- Fleet rollout of Approach B: new `image-upgrade.sh` script delivered + new `ccp-agent` image (with `/upgrade/start-image-only` endpoint) deployed to all 7 tenants. Bnkops's ccp-agent was rebuilt from source (builds locally rather than pulled from registry). + +--- + +## Fleet state at session end + +| Tenant | Surgical update v2.10.2 | image-upgrade.sh | New ccp-agent with image-only endpoint | +|---|---|---|---| +| pia | ✅ (prior session) | ✅ | ✅ | +| soroush | ✅ | ✅ | ✅ | +| linda | ✅ | ✅ | ✅ | +| marcelle | ✅ + tested both A and B E2E | ✅ | ✅ | +| bnkops | ✅ | ✅ | ✅ (rebuilt locally) | +| trbh | ✅ | ✅ | ✅ | +| pridecorner | ✅ | ✅ | ✅ | + +Marcelle E2E test results: +- **Approach A (full upgrade)**: v2.10.1 → v2.10.2 in 250s, COMPLETED, no SIGKILL on script. Phase 6 deferred ccp-agent restart fix worked end-to-end through CCP path. +- **Approach B (Quick Upgrade) run 1**: 121s, COMPLETED, mkdocs.yml md5 unchanged. +- **Approach B (Quick Upgrade) run 2**: 100s (cached pull), COMPLETED, mkdocs unchanged again — confirms idempotency. + +--- + +## Fleet backup (Phase 0 work — defensive) + +All 7 tenants backed up to `/media/bunker-admin/BACKUP/fleet//2026-05-21-pre-v2.10.2/`: + +| Node | Tenant | Size | +|---|---|---| +| n1 | pridecorner | 182MB (includes 3 stash patches from March 9) | +| n2 | linda | 26MB | +| n3 | pia | 45MB (post-surgical state) | +| n4 | bnkops | 4.4GB (huge — 2277 mkdocs/docs files) | +| n5 | marcelle | 28MB | +| n6 | trbh | 336MB | +| n7 | soroush | 76MB | + +Each tenant dir has `mkdocs.tar.gz`, `configs-and-nginx.tar.gz`, `config-files.tar.gz`, `host-state.txt`, `git-state.txt` (source installs only), and `MANIFEST.txt`. + +--- + +## Approach C planning + initial overlay + +**Decision: rewrite `docker-compose.yml.hbs` in prod-compose style** to make CCP-driven template re-render safe for the install.sh fleet. + +### Why a rewrite (not sync-by-addition) + +Discovered the CCP template and `docker-compose.prod.yml` use fundamentally different conventions: +| | Old template (`.hbs`) | Canonical prod | +|---|---|---| +| Container names | `{{containerPrefix}}-postgres` (dynamic) | `changemaker-v2-postgres` (hardcoded) | +| Secrets | `{{secrets.postgresPassword}}` (Handlebars-rendered) | `${POSTGRES_PASSWORD}` (env-substituted) | +| Optional services | `{{#if enableX}}` blocks | Always-defined, gated via `COMPOSE_PROFILES` | +| Ports | `{{ports.api}}` | Hardcoded | + +Sync-by-additions can't reconcile these. Rewrite is cleaner long-term. + +### Initial overlay committed this session + +`changemaker-control-panel/templates/docker-compose.yml.hbs.OLD-style-pre-approach-c` — preserved old template for reference. + +`changemaker-control-panel/templates/docker-compose.yml.hbs` — now a near-mirror of `changemaker.lite/docker-compose.prod.yml` (1493 lines + Handlebars header): +- Header comment includes `{{name}}`, `{{slug}}`, `{{composeProject}}` for traceability. +- 5 image refs replaced `${IMAGE_TAG:-latest}` → `{{imageTag}}` so CCP can per-instance override via `Instance.imageTag` once Phase 1 lands. +- All other variation flows through env-var substitution from tenant's `.env`. + +### Remaining Approach C work (next session) + +See `/home/bunker-admin/.claude/plans/insight-temporal-bachman.md` for the full plan. Quick summary of what's next: + +**Phase 0 completion (next session):** +- Audit `env.hbs` against the new compose's expected env vars. Add missing. +- Sync static config files in `templates/`: nginx/, configs/prometheus/, configs/alertmanager/, configs/grafana/. They may have drifted too. +- Write a one-off render harness (`api/scripts/render-for-instance.ts`) that loads an instance row, builds context, renders templates to scratch dir. +- Render against marcelle, linda, pia. Diff against their actual files. Iterate the template until diff is per-instance values only (`COMPOSE_PROJECT_NAME`, ports, secrets — not structure). + +**Phase 1 (~30 min):** Add `Instance.imageTag` Prisma column + migration. Modify `template-engine.ts:211` to use `instance.imageTag || env.IMAGE_TAG`. + +**Phase 2 (~3-4 hr):** Pre-flight diff endpoint. New agent route `POST /instance/:slug/files/diff` + `RemoteDriver.diffFiles()` + `LocalDriver.diffFiles()` + `previewReleaseUpgrade()` in upgrade.service. Includes `envCoverage` check for registered tenants. + +**Phase 3 (~3-4 hr):** `startReleaseUpgrade()` + `runReleaseUpgrade()` in upgrade.service. Split logic for `isRegistered=true` (skip env render) vs `isRegistered=false` (render env). + +**Phase 4 (~30 min):** CCP routes `/upgrade-release` + `/upgrade-release/preview` + Zod schema. + +**Phase 5 (~2-3 hr):** "Upgrade to Release" UI button + preview modal + env-coverage warning. + +**Phase 6 (~1 hr):** Tag v2.10.3 in changemaker.lite, push images with tag, trigger upgrade-release on marcelle via CCP UI, verify mkdocs untouched + containers on new tag. + +**Total remaining: 11-14 hours.** Recommended split: +- Session 2: complete Phase 0 (render harness + iterate template + env.hbs sync + static file syncs). ~half day. +- Session 3: Phases 1-5. ~half day. +- Session 4: Phase 6 E2E test. ~1 hour. + +--- + +## Critical files for Approach C + +**Already modified this session:** +- `changemaker-control-panel/templates/docker-compose.yml.hbs` — overlay from prod compose with minimal Handlebars markup. +- `changemaker-control-panel/templates/docker-compose.yml.hbs.OLD-style-pre-approach-c` — preserved old template. + +**To be modified in next sessions (per plan):** +- `changemaker-control-panel/templates/env.hbs` (Phase 0 audit) +- `changemaker-control-panel/templates/configs/**` (Phase 0 syncs) +- `changemaker-control-panel/api/prisma/schema.prisma` (Phase 1) +- `changemaker-control-panel/api/prisma/migrations/_add_instance_image_tag/` (Phase 1) +- `changemaker-control-panel/api/src/services/template-engine.ts` line 211 (Phase 1) +- `changemaker-control-panel/api/src/services/upgrade.service.ts` (Phases 2-3) +- `changemaker-control-panel/api/src/services/remote-driver.ts` + `local-driver.ts` + `execution-driver.ts` (Phase 2) +- `changemaker-control-panel/agent/src/routes/files.routes.ts` + `services/file.service.ts` (Phase 2) +- `changemaker-control-panel/api/src/modules/instances/instances.routes.ts` + `instances.schemas.ts` (Phase 4) +- `changemaker-control-panel/admin/src/pages/InstanceDetailPage.tsx` (Phase 5) + +--- + +## Memory key gotchas (write to MEMORY.md next session) + +1. **CCP template vs prod compose: were divergent, now aligned.** As of this session, `templates/docker-compose.yml.hbs` is structurally a near-mirror of `docker-compose.prod.yml`. Going forward, any new service in prod compose must be ported into the template manually (or via a future CI drift check). + +2. **bnkops's ccp-agent is locally built**, not pulled from registry. Has a `build:` directive in compose. The other 6 tenants pull `gitea.bnkops.com/admin/changemaker-ccp-agent:latest`. + +3. **install.sh tenants (`isRegistered=true`)** lack `encryptedSecrets` in CCP DB. Approach C must skip `env.hbs` rendering for them — they keep their tarball-provisioned `.env`. The pre-flight envCoverage check is the safety net. + +4. **n4 SSH lacks marcelle's host key by default** — first `ssh n4 → marcelle` connection needs `StrictHostKeyChecking=accept-new` or interactive accept. Other tenants in the lab have the same pattern. + +5. **`docker save | ssh ... docker load` is the registry-less image distribution path** when n4 doesn't have docker login to gitea.bnkops.com. Worked well for the ccp-agent rollout this session. + +6. **`set -o pipefail` + `grep -q` shorts the pipeline** because grep closes the pipe early on first match, sending SIGPIPE to the writer. Solution: capture upstream output into a variable, then grep against the variable. (Bug found + fixed in `scripts/image-upgrade.sh` during this session.) + +--- + +## CCP access (unchanged) + +``` +URL: http://n4-bnkops.taile33572.ts.net:5100 (UI) + http://n4-bnkops.taile33572.ts.net:5000 (API) +User: admin@thebunkerops.ca +Password: NRTgHdC7Zxxs2P2UmNwnEbn3jTwU8uJN (seed) +Role: SUPER_ADMIN +``` + +--- + +## Where to start next session + +Recommended: + +1. **Read this doc + `/home/bunker-admin/.claude/plans/insight-temporal-bachman.md` (Approach C plan)** first. +2. **Phase 0 completion:** finish the template rewrite. Build a render harness (`api/scripts/render-for-instance.ts`), render against marcelle/linda/pia, iterate until structural-clean. +3. Commit Phase 0 as standalone PR with rendered-vs-actual diffs in description. +4. Move to Phases 1-5 in a second commit/PR. +5. Phase 6 manual E2E. + +Approach B is in production-ready state across the fleet. Approach C is the longer-term path for releases that change orchestration.