# Changemaker Lite — Instance: {{name}} # Compose project: {{composeProject}} # Generated by CCP services: # ─── Core Infrastructure ─────────────────────────────────── v2-postgres: image: {{registryUrl}}/postgres:16-alpine container_name: {{containerPrefix}}-postgres restart: unless-stopped environment: POSTGRES_USER: changemaker POSTGRES_PASSWORD: "{{secrets.postgresPassword}}" POSTGRES_DB: changemaker_v2 volumes: - {{containerPrefix}}-postgres-data:/var/lib/postgresql/data - ./api/prisma/init-nocodb-db.sh:/docker-entrypoint-initdb.d/10-init-nocodb.sh:ro - ./api/prisma/init-gancio-db.sh:/docker-entrypoint-initdb.d/20-init-gancio.sh:ro - ./api/prisma/init-gitea-db.sh:/docker-entrypoint-initdb.d/30-init-gitea.sh:ro ports: - "127.0.0.1:{{ports.postgres}}:5432" networks: - {{networkName}} healthcheck: test: ["CMD-SHELL", "pg_isready -U changemaker -d changemaker_v2"] interval: 10s timeout: 5s retries: 5 redis: image: {{registryUrl}}/redis:7-alpine container_name: {{containerPrefix}}-redis restart: unless-stopped command: "redis-server --appendonly yes --maxmemory 512mb --maxmemory-policy noeviction --requirepass {{secrets.redisPassword}}" volumes: - {{containerPrefix}}-redis-data:/data networks: - {{networkName}} healthcheck: test: ["CMD", "redis-cli", "-a", "{{secrets.redisPassword}}", "ping"] interval: 10s timeout: 5s retries: 5 deploy: resources: limits: cpus: '1' memory: 512M reservations: cpus: '0.25' memory: 256M logging: driver: "json-file" options: max-size: "5m" max-file: "2" # ─── Application Services ────────────────────────────────── api: {{#if useRegistry}} image: {{registryUrl}}/changemaker-api:{{imageTag}} {{else}} build: context: ./api dockerfile: Dockerfile target: development {{/if}} container_name: {{containerPrefix}}-api restart: unless-stopped depends_on: v2-postgres: condition: service_healthy redis: condition: service_healthy env_file: .env environment: DATABASE_URL: "postgresql://changemaker:{{secrets.postgresPassword}}@{{containerPrefix}}-postgres:5432/changemaker_v2" REDIS_URL: "redis://:{{secrets.redisPassword}}@{{containerPrefix}}-redis:6379" PORT: "4000" NAR_DATA_DIR: /data LISTMONK_URL: http://{{containerPrefix}}-listmonk:9000 ADMIN_URL: https://app.{{domain}} API_URL: https://api.{{domain}} {{#if enableGancio}} GANCIO_URL: http://{{containerPrefix}}-gancio:13120 {{/if}} ENABLE_MEET: "{{#if enableMeet}}true{{else}}false{{/if}}" ENABLE_SMS: "{{#if enableSms}}true{{else}}false{{/if}}" ENABLE_SOCIAL: "{{#if enableSocial}}true{{else}}false{{/if}}" ENABLE_PEOPLE: "{{#if enablePeople}}true{{else}}false{{/if}}" {{#if enableMeet}} JITSI_APP_ID: changemaker JITSI_APP_SECRET: "{{secrets.jitsiAppSecret}}" JITSI_URL: http://{{containerPrefix}}-jitsi-web:80 {{/if}} {{#if enableChat}} ROCKETCHAT_URL: http://{{containerPrefix}}-rocketchat:3000 {{/if}} ports: - "{{ports.api}}:4000" {{#if enableListmonk}} - "9002:9002" {{/if}} volumes: - ./assets/uploads:/app/uploads - ./mkdocs:/mkdocs:rw - ./data:/data:ro - ./data/upgrade:/app/upgrade:rw - ./configs:/app/configs:ro networks: - {{networkName}} healthcheck: test: ["CMD", "wget", "-q", "--spider", "http://localhost:4000/api/health"] interval: 15s timeout: 5s retries: 3 start_period: 30s deploy: resources: limits: cpus: '2' memory: 1G reservations: cpus: '0.25' memory: 256M admin: {{#if useRegistry}} image: {{registryUrl}}/changemaker-admin:{{imageTag}} {{else}} build: context: ./admin target: development {{/if}} container_name: {{containerPrefix}}-admin restart: unless-stopped depends_on: - api environment: DOMAIN: {{domain}} NODE_ENV: production VITE_API_URL: http://{{containerPrefix}}-api:4000 VITE_MKDOCS_URL: http://{{containerPrefix}}-mkdocs:8000 VITE_DOMAIN: {{domain}} VITE_MKDOCS_SITE_PORT: "{{math ports.embed "+" 14}}" {{#if enableMedia}} VITE_MEDIA_API_URL: http://{{containerPrefix}}-media-api:4100 {{/if}} ports: - "{{ports.admin}}:3000" networks: - {{networkName}} healthcheck: test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:3000/"] interval: 30s timeout: 5s retries: 3 start_period: 20s {{#if enableMedia}} media-api: {{#if useRegistry}} image: {{registryUrl}}/changemaker-media-api:{{imageTag}} {{else}} build: context: ./api dockerfile: Dockerfile.media target: development {{/if}} container_name: {{containerPrefix}}-media-api restart: unless-stopped depends_on: v2-postgres: condition: service_healthy redis: condition: service_healthy env_file: .env environment: DATABASE_URL: "postgresql://changemaker:{{secrets.postgresPassword}}@{{containerPrefix}}-postgres:5432/changemaker_v2" REDIS_URL: "redis://:{{secrets.redisPassword}}@{{containerPrefix}}-redis:6379" MEDIA_API_PORT: "4100" CORS_ORIGINS: https://app.{{domain}},http://localhost:{{ports.admin}} ENABLE_MEDIA_FEATURES: "true" MEDIA_ROOT: /media/local MEDIA_UPLOADS: /media/uploads volumes: - ./media:/media:ro - ./media/local/inbox:/media/local/inbox:rw - ./media/local/thumbnails:/media/local/thumbnails:rw - ./media/local/photos:/media/local/photos:rw - ./media/public:/media/public:rw networks: - {{networkName}} healthcheck: test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:4100/health"] interval: 15s timeout: 5s retries: 3 start_period: 30s deploy: resources: limits: cpus: '2' memory: 1G reservations: cpus: '0.25' memory: 256M {{/if}} # ─── Reverse Proxy ───────────────────────────────────────── nginx: image: {{registryUrl}}/nginx:alpine container_name: {{containerPrefix}}-nginx restart: unless-stopped depends_on: - api - admin volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro - ./nginx/conf.d:/etc/nginx/conf.d:ro ports: - "{{ports.nginx}}:80" - "{{math ports.embed "+" 0}}:8881" # NocoDB embed proxy - "{{math ports.embed "+" 1}}:8882" # n8n embed proxy - "{{math ports.embed "+" 2}}:8883" # Gitea embed proxy - "{{math ports.embed "+" 3}}:8884" # MailHog embed proxy - "{{math ports.embed "+" 4}}:8885" # Mini QR embed proxy - "{{math ports.embed "+" 5}}:8886" # Excalidraw embed proxy - "{{math ports.embed "+" 6}}:8887" # Homepage embed proxy - "{{math ports.embed "+" 7}}:8888" # Code Server embed proxy - "{{math ports.embed "+" 8}}:8889" # MkDocs embed proxy - "{{math ports.embed "+" 9}}:8890" # Vaultwarden embed proxy - "{{math ports.embed "+" 10}}:8891" # Rocket.Chat embed proxy - "{{math ports.embed "+" 11}}:8892" # Gancio embed proxy - "{{math ports.embed "+" 12}}:8893" # Grafana embed proxy - "{{math ports.embed "+" 13}}:8894" # Listmonk embed proxy - "{{math ports.embed "+" 14}}:8895" # MkDocs site embed proxy - "{{math ports.embed "+" 15}}:8896" # Jitsi Meet embed proxy networks: - {{networkName}} healthcheck: test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:80/"] interval: 30s timeout: 5s retries: 3 # ─── Supporting Services ─────────────────────────────────── nocodb-v2: image: {{registryUrl}}/nocodb:0.301.3 container_name: {{containerPrefix}}-nocodb restart: unless-stopped depends_on: v2-postgres: condition: service_healthy environment: NC_DB: "pg://{{containerPrefix}}-postgres:5432?u=changemaker&p={{secrets.postgresPassword}}&d=nocodb_meta" NC_ADMIN_EMAIL: "{{secrets.adminEmail}}" NC_ADMIN_PASSWORD: "{{secrets.nocodbAdminPassword}}" volumes: - {{containerPrefix}}-nocodb-data:/usr/app/data networks: - {{networkName}} mailhog: image: {{registryUrl}}/mailhog:v1.0.1 container_name: {{containerPrefix}}-mailhog restart: unless-stopped networks: - {{networkName}} logging: driver: "json-file" options: max-size: "5m" max-file: "2" mkdocs: image: {{registryUrl}}/mkdocs-material:latest container_name: {{containerPrefix}}-mkdocs restart: unless-stopped volumes: - ./mkdocs:/docs:rw - ./assets/images:/docs/assets/images:rw user: "1000:1000" environment: SITE_URL: https://{{domain}} ADMIN_PORT: "{{ports.admin}}" ADMIN_URL: https://app.{{domain}} BASE_DOMAIN: https://{{domain}} API_URL: https://api.{{domain}} API_PORT: "{{ports.api}}" {{#if enableMedia}} MEDIA_API_PUBLIC_URL: https://media.{{domain}} MEDIA_API_PORT: "4100" {{/if}} {{#if enableGancio}} GANCIO_URL: http://{{containerPrefix}}-gancio:13120 GANCIO_PORT: "8092" {{/if}} command: serve --dev-addr=0.0.0.0:8000 --watch-theme --livereload networks: - {{networkName}} {{#if enableListmonk}} listmonk-db: image: {{registryUrl}}/postgres:17-alpine container_name: {{containerPrefix}}-listmonk-db restart: unless-stopped environment: POSTGRES_USER: listmonk POSTGRES_PASSWORD: "{{secrets.listmonkAdminPassword}}" POSTGRES_DB: listmonk volumes: - {{containerPrefix}}-listmonk-data:/var/lib/postgresql/data networks: - {{networkName}} healthcheck: test: ["CMD-SHELL", "pg_isready -U listmonk"] interval: 10s timeout: 5s retries: 6 listmonk-app: image: {{registryUrl}}/listmonk:v6.0.0 container_name: {{containerPrefix}}-listmonk restart: unless-stopped depends_on: listmonk-db: condition: service_healthy command: [sh, -c, "./listmonk --install --idempotent --yes --config '' && ./listmonk --upgrade --yes --config '' && ./listmonk --config ''"] environment: LISTMONK_app__address: "0.0.0.0:9000" LISTMONK_db__host: {{containerPrefix}}-listmonk-db LISTMONK_db__port: "5432" LISTMONK_db__user: listmonk LISTMONK_db__password: "{{secrets.listmonkAdminPassword}}" LISTMONK_db__database: listmonk LISTMONK_db__ssl_mode: disable TZ: Etc/UTC LISTMONK_ADMIN_USER: admin LISTMONK_ADMIN_PASSWORD: "{{secrets.listmonkAdminPassword}}" volumes: - ./assets/uploads:/listmonk/uploads:rw networks: - {{networkName}} healthcheck: test: ["CMD", "wget", "-q", "--spider", "http://localhost:9000/"] interval: 30s timeout: 5s retries: 3 start_period: 30s listmonk-init: image: {{registryUrl}}/postgres:17-alpine container_name: {{containerPrefix}}-listmonk-init depends_on: listmonk-app: condition: service_started restart: "no" environment: PGPASSWORD: "{{secrets.listmonkAdminPassword}}" LISTMONK_API_USER: v2-api LISTMONK_API_TOKEN: "{{secrets.listmonkApiToken}}" LISTMONK_SMTP_HOST: {{containerPrefix}}-mailhog LISTMONK_SMTP_PORT: "1025" entrypoint: ["/bin/sh", "-c"] command: - | echo "[listmonk-init] Waiting for Listmonk tables..." for i in $$(seq 1 30); do if psql -h {{containerPrefix}}-listmonk-db -U listmonk -d listmonk -c "SELECT 1 FROM users LIMIT 1" >/dev/null 2>&1; then break fi sleep 2 done if [ -n "$$LISTMONK_API_TOKEN" ]; then echo "[listmonk-init] Upserting API user '$$LISTMONK_API_USER'..." psql -h {{containerPrefix}}-listmonk-db -U listmonk -d listmonk -q < /gancio-data/config.json <<'EOF' { "baseurl": "https://events.{{domain}}", "title": "Events", "description": "Community Events", "server": { "host": "0.0.0.0", "port": 13120 }, "db": { "dialect": "postgres", "host": "{{containerPrefix}}-postgres", "port": 5432, "database": "gancio", "username": "changemaker", "password": "{{secrets.postgresPassword}}" } } EOF echo "[gancio-config] config.json created" else echo "[gancio-config] config.json already exists, skipping" fi restart: "no" networks: - {{networkName}} gancio: image: {{registryUrl}}/gancio:1.28.2 container_name: {{containerPrefix}}-gancio restart: unless-stopped depends_on: v2-postgres: condition: service_healthy gancio-config: condition: service_completed_successfully environment: GANCIO_DATA: /home/node/data NODE_ENV: production GANCIO_DB_DIALECT: postgres GANCIO_DB_HOST: {{containerPrefix}}-postgres GANCIO_DB_PORT: "5432" GANCIO_DB_DATABASE: gancio GANCIO_DB_USERNAME: changemaker GANCIO_DB_PASSWORD: "{{secrets.postgresPassword}}" server__baseurl: https://events.{{domain}} volumes: - {{containerPrefix}}-gancio-data:/home/node/data networks: - {{networkName}} healthcheck: test: ["CMD", "node", "-e", "require('http').get('http://localhost:13120/', r => process.exit(r.statusCode < 400 ? 0 : 1)).on('error', () => process.exit(1))"] interval: 30s timeout: 10s retries: 5 start_period: 60s # Gancio post-start seeder — seeds theme settings after Gancio creates its tables gancio-init: image: {{registryUrl}}/postgres:16-alpine container_name: {{containerPrefix}}-gancio-init depends_on: gancio: condition: service_healthy environment: PGHOST: {{containerPrefix}}-postgres PGUSER: changemaker PGPASSWORD: "{{secrets.postgresPassword}}" PGDATABASE: gancio entrypoint: ["/bin/sh", "-c"] command: - | echo "[gancio-init] Seeding Gancio default theme settings..." psql -c "INSERT INTO settings (key, value, is_secret, \"createdAt\", \"updatedAt\") VALUES ('dark_colors', '{\"primary\": \"#FF6E40\", \"error\": \"#FF5252\", \"info\": \"#2196F3\", \"success\": \"#4CAF50\", \"warning\": \"#FB8C00\"}', false, NOW(), NOW()), ('light_colors', '{\"primary\": \"#FF4500\", \"error\": \"#FF5252\", \"info\": \"#2196F3\", \"success\": \"#4CAF50\", \"warning\": \"#FB8C00\"}', false, NOW(), NOW()) ON CONFLICT (key) DO NOTHING;" echo "[gancio-init] Theme settings seeded" echo "[gancio-init] Done" restart: "no" networks: - {{networkName}} {{/if}} {{#if enableChat}} nats-rocketchat: image: {{registryUrl}}/nats:2.11-alpine container_name: {{containerPrefix}}-nats restart: unless-stopped command: --http_port 8222 networks: - {{networkName}} mongodb-rocketchat: image: {{registryUrl}}/mongo:6.0 container_name: {{containerPrefix}}-mongodb restart: unless-stopped entrypoint: ["/bin/bash", "-c", "if [ ! -f /data/replica.key ]; then openssl rand -base64 756 > /data/replica.key; fi && chmod 400 /data/replica.key && chown 999:999 /data/replica.key && exec mongod --replSet rs0 --bind_ip_all --auth --keyFile /data/replica.key"] environment: MONGO_INITDB_ROOT_USERNAME: "${MONGO_ROOT_USER:-rocketchat}" MONGO_INITDB_ROOT_PASSWORD: "${MONGO_ROOT_PASSWORD}" volumes: - {{containerPrefix}}-mongodb-data:/data/db networks: - {{networkName}} healthcheck: test: ["CMD", "mongosh", "-u", "${MONGO_ROOT_USER:-rocketchat}", "-p", "${MONGO_ROOT_PASSWORD}", "--authenticationDatabase", "admin", "--quiet", "--eval", "try { rs.status().ok } catch(e) { rs.initiate({_id:'rs0',members:[{_id:0,host:'{{containerPrefix}}-mongodb:27017'}]}).ok }"] interval: 10s timeout: 10s retries: 10 start_period: 30s rocketchat: image: {{registryUrl}}/rocketchat:7.9.7 container_name: {{containerPrefix}}-rocketchat restart: unless-stopped depends_on: mongodb-rocketchat: condition: service_healthy nats-rocketchat: condition: service_started environment: ROOT_URL: http://chat.{{domain}} MONGO_URL: mongodb://${MONGO_ROOT_USER:-rocketchat}:${MONGO_ROOT_PASSWORD}@{{containerPrefix}}-mongodb:27017/rocketchat?replicaSet=rs0&authSource=admin MONGO_OPLOG_URL: mongodb://${MONGO_ROOT_USER:-rocketchat}:${MONGO_ROOT_PASSWORD}@{{containerPrefix}}-mongodb:27017/local?replicaSet=rs0&authSource=admin TRANSPORTER: monolith+nats://{{containerPrefix}}-nats:4222 PORT: "3000" ADMIN_USERNAME: rcadmin ADMIN_NAME: Admin ADMIN_EMAIL: "{{secrets.adminEmail}}" ADMIN_PASS: "{{secrets.rocketchatAdminPassword}}" CREATE_TOKENS_FOR_USERS: "true" OVERWRITE_SETTING_Iframe_Integration_send_enable: "true" OVERWRITE_SETTING_Iframe_Integration_receive_enable: "true" OVERWRITE_SETTING_Iframe_Integration_receive_origin: http://app.{{domain}},https://app.{{domain}} {{#if enableMeet}} OVERWRITE_SETTING_Jitsi_Enabled: "true" OVERWRITE_SETTING_Jitsi_Domain: meet.{{domain}} OVERWRITE_SETTING_Jitsi_URL_Room_Prefix: RocketChat OVERWRITE_SETTING_Jitsi_Enable_Channels: "true" OVERWRITE_SETTING_Jitsi_Open_New_Window: "false" OVERWRITE_SETTING_Jitsi_Enabled_TokenAuth: "true" OVERWRITE_SETTING_Jitsi_Application_ID: changemaker OVERWRITE_SETTING_Jitsi_Application_Secret: "{{secrets.jitsiAppSecret}}" OVERWRITE_SETTING_VideoConf_Default_Provider: jitsi {{/if}} volumes: - {{containerPrefix}}-rocketchat-uploads:/app/uploads networks: - {{networkName}} healthcheck: test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:3000/api/info"] interval: 30s timeout: 10s retries: 10 start_period: 90s {{/if}} {{#if enablePangolin}} # ─── Pangolin Tunnel ─────────────────────────────────────── newt: image: {{registryUrl}}/newt:latest container_name: {{containerPrefix}}-newt restart: unless-stopped depends_on: - nginx environment: PANGOLIN_ENDPOINT: "{{pangolin.endpoint}}" NEWT_ID: "{{pangolin.newtId}}" NEWT_SECRET: "{{pangolin.newtSecret}}" networks: - {{networkName}} {{/if}} {{#if enableMeet}} # ─── Jitsi Meet (Video Conferencing) ──────────────────── jitsi-web: image: {{registryUrl}}/jitsi-web:stable-9823 container_name: {{containerPrefix}}-jitsi-web restart: unless-stopped depends_on: - jitsi-prosody environment: XMPP_SERVER: {{containerPrefix}}-jitsi-prosody XMPP_DOMAIN: meet.jitsi XMPP_AUTH_DOMAIN: auth.meet.jitsi XMPP_BOSH_URL_BASE: http://{{containerPrefix}}-jitsi-prosody:5280 XMPP_MUC_DOMAIN: muc.meet.jitsi PUBLIC_URL: https://meet.{{domain}} TZ: America/Edmonton ENABLE_AUTH: "1" AUTH_TYPE: jwt JWT_APP_ID: changemaker JWT_APP_SECRET: "{{secrets.jitsiAppSecret}}" JWT_ACCEPTED_ISSUERS: changemaker JWT_ACCEPTED_AUDIENCES: changemaker volumes: - {{containerPrefix}}-jitsi-web-config:/config networks: - {{networkName}} jitsi-prosody: image: {{registryUrl}}/jitsi-prosody:stable-9823 container_name: {{containerPrefix}}-jitsi-prosody restart: unless-stopped environment: XMPP_DOMAIN: meet.jitsi XMPP_AUTH_DOMAIN: auth.meet.jitsi XMPP_MUC_DOMAIN: muc.meet.jitsi XMPP_INTERNAL_MUC_DOMAIN: internal-muc.meet.jitsi XMPP_RECORDER_DOMAIN: recorder.meet.jitsi XMPP_CROSS_DOMAIN: "true" JICOFO_AUTH_USER: focus JICOFO_AUTH_PASSWORD: "{{secrets.jitsiJicofoAuthPassword}}" JVB_AUTH_USER: jvb JVB_AUTH_PASSWORD: "{{secrets.jitsiJvbAuthPassword}}" TZ: America/Edmonton ENABLE_AUTH: "1" AUTH_TYPE: jwt JWT_APP_ID: changemaker JWT_APP_SECRET: "{{secrets.jitsiAppSecret}}" JWT_ACCEPTED_ISSUERS: changemaker JWT_ACCEPTED_AUDIENCES: changemaker JWT_ALLOW_EMPTY: "0" volumes: - {{containerPrefix}}-jitsi-prosody-config:/config - {{containerPrefix}}-jitsi-prosody-plugins:/prosody-plugins-custom networks: - {{networkName}} jitsi-jicofo: image: {{registryUrl}}/jitsi-jicofo:stable-9823 container_name: {{containerPrefix}}-jitsi-jicofo restart: unless-stopped depends_on: - jitsi-prosody environment: XMPP_SERVER: {{containerPrefix}}-jitsi-prosody XMPP_DOMAIN: meet.jitsi XMPP_AUTH_DOMAIN: auth.meet.jitsi XMPP_INTERNAL_MUC_DOMAIN: internal-muc.meet.jitsi XMPP_MUC_DOMAIN: muc.meet.jitsi JICOFO_AUTH_USER: focus JICOFO_AUTH_PASSWORD: "{{secrets.jitsiJicofoAuthPassword}}" TZ: America/Edmonton volumes: - {{containerPrefix}}-jitsi-jicofo-config:/config networks: - {{networkName}} jitsi-jvb: image: {{registryUrl}}/jitsi-jvb:stable-9823 container_name: {{containerPrefix}}-jitsi-jvb restart: unless-stopped depends_on: - jitsi-prosody environment: XMPP_SERVER: {{containerPrefix}}-jitsi-prosody XMPP_DOMAIN: meet.jitsi XMPP_AUTH_DOMAIN: auth.meet.jitsi XMPP_INTERNAL_MUC_DOMAIN: internal-muc.meet.jitsi JVB_AUTH_USER: jvb JVB_AUTH_PASSWORD: "{{secrets.jitsiJvbAuthPassword}}" JVB_STUN_SERVERS: meet-jit-si-turnrelay.jitsi.net:443 JVB_PORT: "10000" JVB_ADVERTISE_IPS: "{{jvbAdvertiseIp}}" TZ: America/Edmonton ports: - "10000:10000/udp" volumes: - {{containerPrefix}}-jitsi-jvb-config:/config networks: - {{networkName}} {{/if}} # ─── Always-On Utilities ────────────────────────────────── mini-qr: image: {{registryUrl}}/mini-qr:v0.26.0 container_name: {{containerPrefix}}-mini-qr restart: unless-stopped networks: - {{networkName}} mkdocs-site-server: image: {{registryUrl}}/nginx:alpine container_name: {{containerPrefix}}-mkdocs-site restart: unless-stopped volumes: - ./mkdocs/site:/usr/share/nginx/html:ro networks: - {{networkName}} {{#if enableDevTools}} # ─── Dev Tools ──────────────────────────────────────────── code-server: image: {{registryUrl}}/code-server:latest container_name: {{containerPrefix}}-code-server restart: unless-stopped environment: PASSWORD: "{{secrets.nocodbAdminPassword}}" SUDO_PASSWORD: "{{secrets.nocodbAdminPassword}}" volumes: - .:/config/workspace:rw networks: - {{networkName}} gitea: image: {{registryUrl}}/gitea:1.23.7 container_name: {{containerPrefix}}-gitea restart: unless-stopped depends_on: v2-postgres: condition: service_healthy environment: GITEA__database__DB_TYPE: postgres GITEA__database__HOST: {{containerPrefix}}-postgres:5432 GITEA__database__NAME: gitea GITEA__database__USER: changemaker GITEA__database__PASSWD: "{{secrets.postgresPassword}}" GITEA__server__ROOT_URL: https://git.{{domain}} GITEA__server__DOMAIN: git.{{domain}} GITEA__server__HTTP_PORT: "3000" GITEA__server__PROTOCOL: http GITEA__server__ENABLE_GZIP: "true" GITEA__server__X_FRAME_OPTIONS: "" GITEA__security__INSTALL_LOCK: "true" GITEA__attachment__MAX_SIZE: "1024" GITEA__repository__MAX_CREATION_LIMIT: "-1" GITEA__server__LFS_START_SERVER: "true" volumes: - {{containerPrefix}}-gitea-data:/data networks: - {{networkName}} healthcheck: test: ["CMD", "curl", "-fsSL", "http://localhost:3000/api/healthz"] interval: 30s timeout: 10s retries: 5 start_period: 30s # Gitea init — creates admin user after Gitea is healthy (idempotent) # Must run as git user (UID 1000) — Gitea refuses to run as root gitea-init: image: {{registryUrl}}/gitea:1.23.7 container_name: {{containerPrefix}}-gitea-init user: "1000:1000" depends_on: gitea: condition: service_healthy environment: GITEA__database__DB_TYPE: postgres GITEA__database__HOST: {{containerPrefix}}-postgres:5432 GITEA__database__NAME: gitea GITEA__database__USER: changemaker GITEA__database__PASSWD: "{{secrets.postgresPassword}}" GITEA__security__INSTALL_LOCK: "true" volumes: - {{containerPrefix}}-gitea-data:/data entrypoint: ["/bin/sh", "-c"] command: - | echo "[gitea-init] Running migrations (idempotent)..." gitea migrate 2>/dev/null || true if gitea admin user list --admin 2>/dev/null | grep -q "admin"; then echo "[gitea-init] Admin user already exists, skipping" else echo "[gitea-init] Creating admin user..." gitea admin user create \ --admin \ --username admin \ --password "{{secrets.giteaAdminPassword}}" \ --email "{{secrets.adminEmail}}" \ --must-change-password=false echo "[gitea-init] Admin user created" fi echo "[gitea-init] Done" restart: "no" networks: - {{networkName}} n8n: image: {{registryUrl}}/n8n:latest container_name: {{containerPrefix}}-n8n restart: unless-stopped environment: N8N_ENCRYPTION_KEY: "{{secrets.n8nEncryptionKey}}" WEBHOOK_URL: https://n8n.{{domain}} N8N_HOST: n8n.{{domain}} N8N_PROTOCOL: https N8N_SECURE_COOKIE: "false" volumes: - {{containerPrefix}}-n8n-data:/home/node/.n8n networks: - {{networkName}} healthcheck: test: ["CMD", "wget", "-q", "--spider", "http://localhost:5678/healthz"] interval: 30s timeout: 5s retries: 3 docker-socket-proxy: image: {{registryUrl}}/docker-socket-proxy:v0.4.2 container_name: {{containerPrefix}}-docker-socket-proxy restart: unless-stopped environment: CONTAINERS: 1 IMAGES: 1 INFO: 1 NETWORKS: 0 VOLUMES: 0 POST: 0 volumes: - /var/run/docker.sock:/var/run/docker.sock:ro networks: - {{networkName}} homepage: image: {{registryUrl}}/homepage:v0.7.2 container_name: {{containerPrefix}}-homepage restart: unless-stopped environment: DOCKER_HOST: tcp://{{containerPrefix}}-docker-socket-proxy:2375 volumes: - {{containerPrefix}}-homepage-data:/app/config depends_on: - docker-socket-proxy networks: - {{networkName}} excalidraw: image: {{registryUrl}}/excalidraw:latest container_name: {{containerPrefix}}-excalidraw restart: unless-stopped networks: - {{networkName}} # Vaultwarden — Password manager (Bitwarden-compatible) vaultwarden: image: {{registryUrl}}/vaultwarden:1.35.4 container_name: {{containerPrefix}}-vaultwarden restart: unless-stopped environment: ADMIN_TOKEN: "{{secrets.vaultwardenAdminToken}}" DOMAIN: https://vault.{{domain}} SIGNUPS_ALLOWED: "false" WEBSOCKET_ENABLED: "true" ROCKET_PORT: "80" LOG_LEVEL: info SMTP_HOST: {{containerPrefix}}-mailhog SMTP_PORT: "1025" SMTP_FROM: "noreply@{{domain}}" SMTP_FROM_NAME: Vaultwarden SMTP_SECURITY: "off" SMTP_USERNAME: "" SMTP_PASSWORD: "" volumes: - {{containerPrefix}}-vaultwarden-data:/data networks: - {{networkName}} healthcheck: test: ["CMD", "curl", "-sf", "http://localhost:80/alive"] interval: 30s timeout: 5s retries: 3 start_period: 15s # Vaultwarden init — invites the initial admin user (safe to re-run) vaultwarden-init: image: {{registryUrl}}/alpine-curl:8.11.1 container_name: {{containerPrefix}}-vaultwarden-init depends_on: vaultwarden: condition: service_healthy restart: "no" entrypoint: ["/bin/sh", "-c"] command: - | echo "[vaultwarden-init] Waiting for Vaultwarden..." for i in $$(seq 1 20); do if curl -sf http://{{containerPrefix}}-vaultwarden:80/alive >/dev/null 2>&1; then break fi sleep 2 done VAULTWARDEN_ADMIN_TOKEN="{{secrets.vaultwardenAdminToken}}" VAULTWARDEN_URL="http://{{containerPrefix}}-vaultwarden:80" INVITE_EMAIL="{{secrets.adminEmail}}" if [ -z "$$VAULTWARDEN_ADMIN_TOKEN" ]; then echo "[vaultwarden-init] VAULTWARDEN_ADMIN_TOKEN not set, skipping invite" exit 0 fi echo "[vaultwarden-init] Authenticating with admin panel..." SESSION_COOKIE=$$(mktemp) HTTP_CODE=$$(curl -s -o /dev/null -w "%{http_code}" \ -c "$$SESSION_COOKIE" \ -X POST "$$VAULTWARDEN_URL/admin" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "token=$$VAULTWARDEN_ADMIN_TOKEN") if [ "$$HTTP_CODE" != "200" ] && [ "$$HTTP_CODE" != "302" ]; then echo "[vaultwarden-init] Admin auth failed (HTTP $$HTTP_CODE)" rm -f "$$SESSION_COOKIE" exit 1 fi echo "[vaultwarden-init] Authenticated" echo "[vaultwarden-init] Inviting $$INVITE_EMAIL..." INVITE_CODE=$$(curl -s -w "\n%{http_code}" \ -b "$$SESSION_COOKIE" \ -X POST "$$VAULTWARDEN_URL/admin/invite" \ -H "Content-Type: application/json" \ -d "{\"email\":\"$$INVITE_EMAIL\"}") INVITE_HTTP=$$(echo "$$INVITE_CODE" | tail -1) INVITE_BODY=$$(echo "$$INVITE_CODE" | head -n -1) if [ "$$INVITE_HTTP" = "200" ] || [ "$$INVITE_HTTP" = "422" ]; then echo "[vaultwarden-init] Invite sent (or user already exists)" else echo "[vaultwarden-init] Invite failed (HTTP $$INVITE_HTTP): $$INVITE_BODY" fi rm -f "$$SESSION_COOKIE" echo "[vaultwarden-init] Done" networks: - {{networkName}} {{/if}} {{#if enableMonitoring}} # ─── Monitoring Stack ────────────────────────────────────── prometheus: image: {{registryUrl}}/prometheus:v3.10.0 container_name: {{containerPrefix}}-prometheus restart: unless-stopped command: - '--config.file=/etc/prometheus/prometheus.yml' - '--storage.tsdb.path=/prometheus' - '--storage.tsdb.retention.time=30d' volumes: - ./configs/prometheus:/etc/prometheus:ro - {{containerPrefix}}-prometheus-data:/prometheus networks: - {{networkName}} grafana: image: {{registryUrl}}/grafana:12.3.0 container_name: {{containerPrefix}}-grafana restart: unless-stopped environment: GF_SECURITY_ADMIN_PASSWORD: "{{secrets.grafanaAdminPassword}}" GF_USERS_ALLOW_SIGN_UP: "false" GF_SERVER_ROOT_URL: https://grafana.{{domain}} GF_SECURITY_ALLOW_EMBEDDING: "true" volumes: - {{containerPrefix}}-grafana-data:/var/lib/grafana - ./configs/grafana:/etc/grafana/provisioning depends_on: - prometheus networks: - {{networkName}} alertmanager: image: {{registryUrl}}/alertmanager:v0.31.1 container_name: {{containerPrefix}}-alertmanager restart: unless-stopped command: - '--config.file=/etc/alertmanager/alertmanager.yml' - '--storage.path=/alertmanager' volumes: - ./configs/alertmanager:/etc/alertmanager:ro - {{containerPrefix}}-alertmanager-data:/alertmanager networks: - {{networkName}} cadvisor: image: {{registryUrl}}/cadvisor:v0.55.1 container_name: {{containerPrefix}}-cadvisor restart: unless-stopped volumes: - /:/rootfs:ro - /var/run:/var/run:ro - /sys:/sys:ro - /var/lib/docker/:/var/lib/docker:ro networks: - {{networkName}} node-exporter: image: {{registryUrl}}/node-exporter:v1.10.2 container_name: {{containerPrefix}}-node-exporter restart: unless-stopped command: - '--path.rootfs=/host' volumes: - /:/host:ro,rslave networks: - {{networkName}} redis-exporter: image: {{registryUrl}}/redis_exporter:v1.81.0 container_name: {{containerPrefix}}-redis-exporter restart: unless-stopped environment: REDIS_ADDR: redis://{{containerPrefix}}-redis:6379 REDIS_PASSWORD: "{{secrets.redisPassword}}" depends_on: - redis networks: - {{networkName}} gotify: image: {{registryUrl}}/gotify:v2.9.0 container_name: {{containerPrefix}}-gotify restart: unless-stopped volumes: - {{containerPrefix}}-gotify-data:/app/data networks: - {{networkName}} {{/if}} # ─── Volumes ────────────────────────────────────────────── volumes: {{containerPrefix}}-postgres-data: {{containerPrefix}}-redis-data: {{containerPrefix}}-nocodb-data: {{#if enableListmonk}} {{containerPrefix}}-listmonk-data: {{/if}} {{#if enableGancio}} {{containerPrefix}}-gancio-data: {{/if}} {{#if enableChat}} {{containerPrefix}}-mongodb-data: {{containerPrefix}}-rocketchat-uploads: {{/if}} {{#if enableDevTools}} {{containerPrefix}}-gitea-data: {{containerPrefix}}-n8n-data: {{containerPrefix}}-homepage-data: {{containerPrefix}}-vaultwarden-data: {{/if}} {{#if enableMonitoring}} {{containerPrefix}}-prometheus-data: {{containerPrefix}}-grafana-data: {{containerPrefix}}-alertmanager-data: {{containerPrefix}}-gotify-data: {{/if}} {{#if enableMeet}} {{containerPrefix}}-jitsi-web-config: {{containerPrefix}}-jitsi-prosody-config: {{containerPrefix}}-jitsi-prosody-plugins: {{containerPrefix}}-jitsi-jicofo-config: {{containerPrefix}}-jitsi-jvb-config: {{/if}} # ─── Networks ───────────────────────────────────────────── networks: {{networkName}}: driver: bridge