Three related fixes uncovered during a marcelle CCP registration test:
1. ccp-agent image was missing bash + curl + jq + python3, so every
spawn('bash', ...) in upgrade.routes.ts and backup.routes.ts failed
silently with ENOENT. CCP kept reading stale status.json files from
disk, masking that no agent had successfully checked for updates in
weeks. apk-add the missing tools.
2. ccp-agent's /app/instance mount was :ro, blocking the agent from
writing data/upgrade/status.json (and result/progress/backups).
Agent already has docker.sock — removing :ro is not a security
escalation. Patched both docker-compose.yml and docker-compose.prod.yml.
3. Gitea 1.23.x only initializes Release.CreatedUnix inside its
createTag() helper, which is skipped if the tag already exists on
origin. The old DEV_WORKFLOW pattern (push tag, then run
build-release.sh --upload) was triggering this — releases got
created_unix=0 and lost /releases/latest sort order to v2.9.14.
build-release.sh now removes the remote tag first and POSTs with
target_commitish so Gitea creates the tag and release atomically.
After these fixes, CCP's "Check for Updates" path returns truthful
data end-to-end (verified on marcelle: v2.9.15 -> v2.10.1, 1 behind).
Bunker Admin
1489 lines
57 KiB
YAML
1489 lines
57 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: 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:${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}
|
|
# 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:${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: 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
|
|
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_healthy
|
|
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:
|
|
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:
|
|
- 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:-${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:
|
|
- 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 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: ${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
|
|
# 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 <project>`
|
|
# against the right project (not basename of INSTANCE_BASE_PATH, which is "instance").
|
|
- COMPOSE_PROJECT=${COMPOSE_PROJECT_NAME:-changemaker-lite}
|
|
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:
|