docker-compose.yml explicitly enumerates each env var passed to containers, so the new JWT_INVITE_SECRET needed to be wired through the environment block or the API would fail Zod env validation at startup. Bunker Admin
1275 lines
45 KiB
YAML
1275 lines
45 KiB
YAML
###############################################################################
|
|
# Changemaker Lite v2 — Docker Compose
|
|
###############################################################################
|
|
|
|
services:
|
|
# =========================================================================
|
|
# V2 CORE SERVICES
|
|
# =========================================================================
|
|
|
|
# Unified Express.js API
|
|
api:
|
|
build:
|
|
context: ./api
|
|
target: ${BUILD_TARGET:-development}
|
|
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:-development}
|
|
- PORT=4000
|
|
- 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}
|
|
- 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:-true}
|
|
- 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}
|
|
# Monitoring embed ports (for iframe embedding without DNS/subdomain)
|
|
- GRAFANA_EMBED_PORT=${GRAFANA_EMBED_PORT:-8894}
|
|
- ALERTMANAGER_EMBED_PORT=${ALERTMANAGER_EMBED_PORT:-8895}
|
|
# SMS Campaigns (Termux Android Bridge)
|
|
- ENABLE_SMS=${ENABLE_SMS:-false}
|
|
- 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 status via socket proxy (read-only, containers endpoint only)
|
|
- DOCKER_PROXY_URL=http://docker-socket-proxy:2375
|
|
volumes:
|
|
- ./api:/app
|
|
- /app/node_modules
|
|
- ./assets/uploads:/app/uploads
|
|
- ./mkdocs:/mkdocs:rw
|
|
- ./data:/data:ro
|
|
- ./data/upgrade:/app/upgrade:rw
|
|
- ./configs:/app/configs:ro
|
|
deploy:
|
|
resources:
|
|
limits:
|
|
cpus: '2'
|
|
memory: 1G
|
|
reservations:
|
|
cpus: '0.25'
|
|
memory: 256M
|
|
depends_on:
|
|
v2-postgres:
|
|
condition: service_healthy
|
|
redis:
|
|
condition: service_healthy
|
|
networks:
|
|
- changemaker-lite
|
|
|
|
# Fastify Media API (Microservice for Media Management)
|
|
media-api:
|
|
build:
|
|
context: ./api
|
|
dockerfile: Dockerfile.media
|
|
target: ${BUILD_TARGET:-development}
|
|
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:-development}
|
|
- 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}
|
|
- CORS_ORIGINS=${CORS_ORIGINS:-http://localhost:3000,http://localhost:3100}
|
|
- 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:
|
|
- ./api:/app
|
|
- /app/node_modules
|
|
- ${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
|
|
networks:
|
|
- changemaker-lite
|
|
|
|
# React Admin GUI (Vite dev server)
|
|
admin:
|
|
build:
|
|
context: ./admin
|
|
target: ${BUILD_TARGET:-development}
|
|
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:-development}
|
|
- 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}
|
|
volumes:
|
|
- ./admin:/app
|
|
- /app/node_modules
|
|
depends_on:
|
|
- api
|
|
networks:
|
|
- changemaker-lite
|
|
|
|
# PostgreSQL 16 (v2 database)
|
|
v2-postgres:
|
|
image: 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
|
|
- ./api/prisma/init-nocodb-db.sh:/docker-entrypoint-initdb.d/init-nocodb-db.sh:ro
|
|
- ./api/prisma/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
|
|
networks:
|
|
- changemaker-lite
|
|
|
|
# Nginx reverse proxy
|
|
nginx:
|
|
build:
|
|
context: ./nginx
|
|
container_name: changemaker-v2-nginx
|
|
restart: unless-stopped
|
|
ports:
|
|
- "${NGINX_HTTP_PORT:-80}:80"
|
|
- "${NGINX_HTTPS_PORT:-443}:443"
|
|
- "127.0.0.1:8881:8881" # NocoDB embed proxy (strips X-Frame-Options)
|
|
- "127.0.0.1:8882:8882" # n8n embed proxy
|
|
- "127.0.0.1:8883:8883" # Gitea embed proxy
|
|
- "127.0.0.1:8884:8884" # MailHog embed proxy
|
|
- "127.0.0.1:8885:8885" # Mini QR embed proxy
|
|
- "127.0.0.1:8886:8886" # Excalidraw embed proxy
|
|
- "127.0.0.1:8887:8887" # Homepage embed proxy
|
|
- "127.0.0.1:8890:8890" # Vaultwarden embed proxy
|
|
- "127.0.0.1:8891:8891" # Rocket.Chat embed proxy
|
|
- "127.0.0.1:8892:8892" # Gancio embed proxy
|
|
- "127.0.0.1:8893:8893" # Jitsi Meet embed proxy
|
|
- "127.0.0.1:8894:8894" # Grafana embed proxy
|
|
- "127.0.0.1:8895:8895" # Alertmanager embed proxy
|
|
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:-}
|
|
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
|
|
- admin
|
|
networks:
|
|
- changemaker-lite
|
|
|
|
# NocoDB v2 — pointed at v2 PostgreSQL as read-only data browser
|
|
nocodb-v2:
|
|
image: nocodb/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
|
|
networks:
|
|
- changemaker-lite
|
|
|
|
# NocoDB Init — auto-registers changemaker_v2 as a browsable data source
|
|
nocodb-init:
|
|
image: 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"]
|
|
networks:
|
|
- changemaker-lite
|
|
|
|
# =========================================================================
|
|
# SHARED INFRASTRUCTURE (kept from v1)
|
|
# =========================================================================
|
|
|
|
# Shared Redis — sessions, BullMQ queues, cache
|
|
redis:
|
|
image: redis:7-alpine
|
|
container_name: redis-changemaker
|
|
command: redis-server --appendonly yes --maxmemory 512mb --maxmemory-policy allkeys-lru --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: listmonk/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
|
|
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}
|
|
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
|
|
networks:
|
|
- changemaker-lite
|
|
|
|
listmonk-db:
|
|
image: 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}
|
|
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
|
|
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: postgres:17-alpine
|
|
container_name: listmonk-init
|
|
depends_on:
|
|
listmonk-app:
|
|
condition: service_started
|
|
restart: "no"
|
|
environment:
|
|
PGPASSWORD: ${LISTMONK_DB_PASSWORD:-listmonk}
|
|
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"
|
|
networks:
|
|
- changemaker-lite
|
|
|
|
# =========================================================================
|
|
# PLATFORM SERVICES (kept from v1)
|
|
# =========================================================================
|
|
|
|
# Code Server — Browser IDE
|
|
code-server:
|
|
build:
|
|
context: .
|
|
dockerfile: Dockerfile.code-server
|
|
container_name: code-server-changemaker
|
|
command: /home/coder/project
|
|
environment:
|
|
- DOCKER_USER=${USER_NAME:-coder}
|
|
user: "${USER_ID:-1000}:${GROUP_ID:-1000}"
|
|
volumes:
|
|
- ./configs/code-server/.config:/home/coder/.config
|
|
- ./configs/code-server/.local:/home/coder/.local
|
|
- ./api:/home/coder/project/api
|
|
- ./admin:/home/coder/project/admin
|
|
- ./nginx:/home/coder/project/nginx
|
|
- ./configs:/home/coder/project/configs
|
|
- ./scripts:/home/coder/project/scripts
|
|
- ./mkdocs:/home/coder/project/mkdocs
|
|
- ./docker-compose.yml:/home/coder/project/docker-compose.yml
|
|
# NOTE: .env intentionally excluded — secrets must not be accessible via Code Server
|
|
ports:
|
|
- "127.0.0.1:${CODE_SERVER_PORT:-8888}:8080"
|
|
restart: unless-stopped
|
|
networks:
|
|
- changemaker-lite
|
|
|
|
# MkDocs — Live documentation preview
|
|
mkdocs:
|
|
image: squidfunk/mkdocs-material
|
|
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
|
|
networks:
|
|
- changemaker-lite
|
|
|
|
# MkDocs built site — Nginx static server
|
|
mkdocs-site-server:
|
|
image: lscr.io/linuxserver/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
|
|
networks:
|
|
- changemaker-lite
|
|
|
|
# n8n — Workflow automation
|
|
n8n:
|
|
image: docker.n8n.io/n8nio/n8n
|
|
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
|
|
networks:
|
|
- changemaker-lite
|
|
|
|
# Homepage dashboard
|
|
homepage:
|
|
image: ghcr.io/gethomepage/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
|
|
networks:
|
|
- changemaker-lite
|
|
|
|
# Gitea — Git hosting
|
|
gitea-app:
|
|
image: gitea/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
|
|
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
|
|
networks:
|
|
- changemaker-lite
|
|
|
|
gitea-db:
|
|
image: 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
|
|
networks:
|
|
- changemaker-lite
|
|
|
|
# Mini QR — QR code generator
|
|
mini-qr:
|
|
image: ghcr.io/lyqht/mini-qr:v0.26.0
|
|
container_name: mini-qr
|
|
ports:
|
|
- "127.0.0.1:${MINI_QR_PORT:-8089}:8080"
|
|
restart: unless-stopped
|
|
networks:
|
|
- changemaker-lite
|
|
|
|
# Excalidraw — Collaborative whiteboard
|
|
excalidraw:
|
|
image: excalidraw/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}
|
|
networks:
|
|
- changemaker-lite
|
|
|
|
# Vaultwarden — Password manager (Bitwarden-compatible)
|
|
vaultwarden:
|
|
image: vaultwarden/server: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
|
|
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: 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"
|
|
networks:
|
|
- changemaker-lite
|
|
|
|
# Rocket.Chat — Team coordination chat
|
|
rocketchat:
|
|
image: rocketchat/rocket.chat: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://mongodb-rocketchat:27017/rocketchat?replicaSet=rs0
|
|
- MONGO_OPLOG_URL=mongodb://mongodb-rocketchat:27017/local?replicaSet=rs0
|
|
- 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
|
|
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: nats:2.11-alpine
|
|
container_name: nats-rocketchat
|
|
restart: unless-stopped
|
|
command: --http_port 8222
|
|
networks:
|
|
- changemaker-lite
|
|
|
|
# MongoDB (required by Rocket.Chat — replica set for oplog tailing)
|
|
mongodb-rocketchat:
|
|
image: mongo:6.0
|
|
container_name: mongodb-rocketchat
|
|
restart: unless-stopped
|
|
command: ["mongod", "--replSet", "rs0", "--bind_ip_all"]
|
|
volumes:
|
|
- mongodb-rocketchat-data:/data/db
|
|
networks:
|
|
- changemaker-lite
|
|
healthcheck:
|
|
test: ["CMD", "mongosh", "--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: cisti/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
|
|
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: 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"
|
|
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: 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
|
|
networks:
|
|
- changemaker-lite
|
|
|
|
# Jitsi Prosody — XMPP server + JWT validation
|
|
jitsi-prosody:
|
|
image: 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
|
|
networks:
|
|
- changemaker-lite
|
|
|
|
# Jitsi Jicofo — Conference focus / room management
|
|
jitsi-jicofo:
|
|
image: 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
|
|
networks:
|
|
- changemaker-lite
|
|
|
|
# Jitsi JVB — Video bridge (media relay for audio/video)
|
|
jitsi-jvb:
|
|
image: 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
|
|
networks:
|
|
- changemaker-lite
|
|
|
|
# MailHog — Email testing (dev)
|
|
mailhog:
|
|
image: mailhog/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: fosrl/newt
|
|
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
|
|
networks:
|
|
- changemaker-lite
|
|
|
|
# Docker socket proxy — read-only access for container status monitoring
|
|
docker-socket-proxy:
|
|
image: tecnativa/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
|
|
networks:
|
|
- changemaker-lite
|
|
|
|
# =========================================================================
|
|
# MONITORING (behind profile flag)
|
|
# =========================================================================
|
|
|
|
prometheus:
|
|
image: prom/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
|
|
networks:
|
|
- changemaker-lite
|
|
profiles:
|
|
- monitoring
|
|
|
|
grafana:
|
|
image: grafana/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=true
|
|
- GF_AUTH_ANONYMOUS_ORG_ROLE=Viewer
|
|
volumes:
|
|
- grafana-data:/var/lib/grafana
|
|
- ./configs/grafana:/etc/grafana/provisioning
|
|
restart: always
|
|
depends_on:
|
|
- prometheus
|
|
networks:
|
|
- changemaker-lite
|
|
profiles:
|
|
- monitoring
|
|
|
|
cadvisor:
|
|
image: gcr.io/cadvisor/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
|
|
privileged: true
|
|
read_only: true
|
|
devices:
|
|
- /dev/kmsg
|
|
restart: always
|
|
networks:
|
|
- changemaker-lite
|
|
profiles:
|
|
- monitoring
|
|
|
|
node-exporter:
|
|
image: prom/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
|
|
networks:
|
|
- changemaker-lite
|
|
profiles:
|
|
- monitoring
|
|
|
|
redis-exporter:
|
|
image: oliver006/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-changemaker
|
|
networks:
|
|
- changemaker-lite
|
|
profiles:
|
|
- monitoring
|
|
|
|
alertmanager:
|
|
image: prom/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
|
|
networks:
|
|
- changemaker-lite
|
|
profiles:
|
|
- monitoring
|
|
|
|
gotify:
|
|
image: gotify/server: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
|
|
networks:
|
|
- changemaker-lite
|
|
profiles:
|
|
- monitoring
|
|
|
|
# =============================================================================
|
|
# 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:
|