############################################################################### # Changemaker Lite v2 — Docker Compose ############################################################################### services: # ========================================================================= # V2 CORE SERVICES # ========================================================================= # Unified Express.js API api: build: context: ./api target: development container_name: changemaker-v2-api restart: unless-stopped ports: - "${API_PORT:-4000}:4000" - "${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:-changemaker}@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_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:-REQUIRED_STRONG_PASSWORD_CHANGE_THIS} - 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=${NODE_TLS_REJECT_UNAUTHORIZED:-} - 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} volumes: - ./api:/app - /app/node_modules - ./assets/uploads:/app/uploads - ./mkdocs:/mkdocs:rw - ./data:/data:ro - ./configs:/app/configs:ro - /var/run/docker.sock:/var/run/docker.sock 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: development container_name: changemaker-media-api restart: unless-stopped ports: - "${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:-changemaker}@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} 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}/public:/media/public:rw depends_on: v2-postgres: condition: service_healthy networks: - changemaker-lite # React Admin GUI (Vite dev server) admin: build: context: ./admin target: development container_name: changemaker-v2-admin restart: unless-stopped ports: - "${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 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:-changemaker} 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 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" - "8881:8881" # NocoDB embed proxy (strips X-Frame-Options) - "8882:8882" # n8n embed proxy - "8883:8883" # Gitea embed proxy - "8884:8884" # MailHog embed proxy - "8885:8885" # Mini QR embed proxy - "8886:8886" # Excalidraw embed proxy - "8887:8887" # Homepage 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:latest container_name: changemaker-v2-nocodb restart: unless-stopped ports: - "${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:-changemaker}&d=nocodb_meta" NC_ADMIN_EMAIL: ${NC_ADMIN_EMAIL:-admin@cmlite.org} NC_ADMIN_PASSWORD: ${NC_ADMIN_PASSWORD:-admin123} 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 # ========================================================================= # 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: - "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:latest container_name: listmonk-app restart: unless-stopped ports: - "${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:-5432}: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 } 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 <