changemaker.lite/docker-compose.prod.yml
bunker-admin 5f0ae6bc5a Revert NocoDB auto sign-in, keep CSP fix for embed proxy
NocoDB v2 stores auth tokens in-memory (Pinia store), not in cookies
accessible to external pages. The auth bridge approach can't inject
tokens into NocoDB's SPA state. Reverted to the original banner
approach ("sign in to NocoDB in a new tab").

Kept: CSP fix (frame-ancestors http://localhost:* instead of just
localhost, which only matched port 80).

Bunker Admin
2026-04-09 14:01:02 -06:00

1430 lines
54 KiB
YAML

###############################################################################
###############################################################################
# Changemaker Lite v2 — Production Docker Compose
# Pre-built images only. No source code mounts, no build blocks.
# Generated from docker-compose.yml by build-release.sh
###############################################################################
###############################################################################
x-logging: &default-logging
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
services:
# =========================================================================
# V2 CORE SERVICES
# =========================================================================
# Unified Express.js API
api:
image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/changemaker-api:${IMAGE_TAG:-latest}
container_name: changemaker-v2-api
restart: unless-stopped
ports:
- "127.0.0.1:${API_PORT:-4000}:4000"
- "127.0.0.1:${LISTMONK_PROXY_PORT:-9002}:9002"
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:4000/api/health"]
interval: 15s
timeout: 5s
retries: 3
start_period: 30s
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}
- 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:-7d}
- 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:-}
- 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:${IMAGE_TAG:-latest}
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}
- CORS_ORIGINS=${CORS_ORIGINS:-http://localhost:3000,http://localhost:3100}
- ENCRYPTION_KEY=${ENCRYPTION_KEY}
- ENABLE_MEDIA_FEATURES=${ENABLE_MEDIA_FEATURES:-true}
- 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}/public:/media/public:rw
deploy:
resources:
limits:
cpus: '2'
memory: 1G
reservations:
cpus: '0.25'
memory: 256M
depends_on:
v2-postgres:
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:${IMAGE_TAG:-latest}
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: 20s
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
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
logging: *default-logging
networks:
- changemaker-lite
# Nginx reverse proxy
nginx:
image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/changemaker-nginx:${IMAGE_TAG:-latest}
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", "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
deploy:
resources:
limits:
cpus: '1'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M
networks:
- changemaker-lite
logging:
driver: "json-file"
options:
max-size: "5m"
max-file: "2"
# Listmonk — Email marketing (kept as Docker image, controlled via REST API)
listmonk-app:
image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/listmonk:v6.0.0
container_name: listmonk-app
restart: unless-stopped
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: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/postgres:17-alpine
container_name: listmonk-init
depends_on:
listmonk-app:
condition: service_started
restart: "no"
environment:
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 <noreply@cmlite.org>}
entrypoint: ["/bin/sh", "-c"]
command:
- |
echo "[listmonk-init] Waiting for Listmonk tables..."
for i in $$(seq 1 30); do
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
done
if [ -n "$$LISTMONK_API_TOKEN" ]; then
echo "[listmonk-init] Upserting API user '$$LISTMONK_API_USER'..."
psql -h listmonk-db -U ${LISTMONK_DB_USER:-listmonk} -d ${LISTMONK_DB_NAME:-listmonk} -q <<SQL
INSERT INTO users (username, password, password_login, email, name, type, user_role_id, status)
VALUES ('$$LISTMONK_API_USER', '$$LISTMONK_API_TOKEN', true, '$$LISTMONK_API_USER@api.internal', '$$LISTMONK_API_USER', 'api', 1, 'enabled')
ON CONFLICT (username) DO UPDATE SET password = EXCLUDED.password, status = 'enabled', user_role_id = 1;
SQL
echo "[listmonk-init] API user configured"
else
echo "[listmonk-init] LISTMONK_API_TOKEN not set, skipping API user"
fi
if [ -n "$$LISTMONK_SMTP_HOST" ]; then
echo "[listmonk-init] Configuring SMTP..."
# Always include MailHog as first provider
MAILHOG_ENTRY='{"host":"mailhog-changemaker","port":1025,"username":"","password":"","tls_type":"none","auth_protocol":"none","enabled":true,"max_conns":5,"idle_timeout":"15s","wait_timeout":"5s","max_msg_retries":2,"tls_skip_verify":false,"email_headers":[],"hello_hostname":""}'
if [ -n "$$LISTMONK_SMTP_USER" ]; then
# Production SMTP as second provider
PROD_ENTRY='{"host":"'"$$LISTMONK_SMTP_HOST"'","port":'"$$LISTMONK_SMTP_PORT"',"username":"'"$$LISTMONK_SMTP_USER"'","password":"'"$$LISTMONK_SMTP_PASSWORD"'","tls_type":"'"$$LISTMONK_SMTP_TLS_TYPE"'","auth_protocol":"plain","enabled":true,"max_conns":5,"idle_timeout":"15s","wait_timeout":"5s","max_msg_retries":2,"tls_skip_verify":false,"email_headers":[],"hello_hostname":""}'
SMTP_VALUE="[$$MAILHOG_ENTRY,$$PROD_ENTRY]"
echo "[listmonk-init] Dual SMTP: MailHog + $$LISTMONK_SMTP_HOST:$$LISTMONK_SMTP_PORT"
else
SMTP_VALUE="[$$MAILHOG_ENTRY]"
echo "[listmonk-init] Single SMTP: MailHog (no production credentials)"
fi
psql -h listmonk-db -U ${LISTMONK_DB_USER:-listmonk} -d ${LISTMONK_DB_NAME:-listmonk} -q <<SQL
UPDATE settings SET value = '$$SMTP_VALUE' WHERE key = 'smtp';
UPDATE settings SET value = '"$$LISTMONK_SMTP_FROM"' WHERE key = 'app.from_email';
SQL
echo "[listmonk-init] SMTP configured"
else
echo "[listmonk-init] LISTMONK_SMTP_HOST not set, skipping SMTP config"
fi
echo "[listmonk-init] Done"
logging: *default-logging
networks:
- changemaker-lite
# =========================================================================
# PLATFORM SERVICES (kept from v1)
# =========================================================================
# Code Server — Browser IDE (LinuxServer upstream, no custom build)
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
logging: *default-logging
networks:
- changemaker-lite
# 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
logging: *default-logging
networks:
- changemaker-lite
# 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:
- PUID=${USER_ID:-1000}
- PGID=${GROUP_ID:-1000}
- TZ=Etc/UTC
volumes:
- ./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:
- changemaker-lite
# n8n — Workflow automation
n8n:
image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/n8n:latest
container_name: n8n-changemaker
restart: unless-stopped
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
start_period: 30s
environment:
- 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:
- n8n-data:/home/node/.n8n
- ./local-files:/files
logging: *default-logging
networks:
- changemaker-lite
# Homepage dashboard
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
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:
- 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:
- 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:
- changemaker-lite
# 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:-}
- 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:
- 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: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/vaultwarden:1.35.4
container_name: vaultwarden-changemaker
restart: unless-stopped
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
# 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: ${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://vaultwarden-changemaker:80/alive >/dev/null 2>&1; then
break
fi
sleep 2
done
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"
logging: *default-logging
networks:
- changemaker-lite
# 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 — 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
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
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: ${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
- prometheus-data:/prometheus
restart: always
logging: *default-logging
networks:
- changemaker-lite
profiles:
- monitoring
grafana:
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=${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:
- grafana-data:/var/lib/grafana
- ./configs/grafana:/etc/grafana/provisioning
restart: always
depends_on:
- prometheus
logging: *default-logging
networks:
- changemaker-lite
profiles:
- monitoring
cadvisor:
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:
- changemaker-lite
profiles:
- monitoring
node-exporter:
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:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
restart: always
logging: *default-logging
networks:
- changemaker-lite
profiles:
- monitoring
redis-exporter:
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://redis-changemaker:6379
- REDIS_PASSWORD=${REDIS_PASSWORD}
restart: always
depends_on:
- redis
logging: *default-logging
networks:
- 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: ${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:
- gotify-data:/app/data
restart: always
logging: *default-logging
networks:
- changemaker-lite
profiles:
- monitoring
# =========================================================================
# CCP REMOTE AGENT (optional — enabled via COMPOSE_PROFILES=ccp-agent)
# =========================================================================
ccp-agent:
image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/changemaker-ccp-agent:${IMAGE_TAG:-latest}
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
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
logging: *default-logging
networks:
- changemaker-lite
# =============================================================================
# NETWORKS & VOLUMES
# =============================================================================
networks:
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: