# Base stack — all server-side services. Self-contained: `docker compose up` # builds and boots everything. No host ports are published here (only the dev # override and prod files expose ports); services talk over the private network. name: altricade services: postgres: image: postgres:16-alpine environment: POSTGRES_DB: ${POSTGRES_DB} POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} volumes: - postgres-data:/var/lib/postgresql/data - ./infra/postgres/init:/docker-entrypoint-initdb.d:ro healthcheck: test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}'] interval: 10s timeout: 5s retries: 5 networks: [altricade] redis: image: redis:7-alpine command: ['redis-server', '--appendonly', 'yes'] volumes: - redis-data:/data healthcheck: test: ['CMD', 'redis-cli', 'ping'] interval: 10s timeout: 5s retries: 5 networks: [altricade] minio: image: minio/minio:RELEASE.2025-04-22T22-12-26Z command: ['server', '/data', '--console-address', ':9001'] environment: MINIO_ROOT_USER: ${MINIO_ROOT_USER} MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} # Allow browser (cross-origin) presigned PUT/GET uploads. MINIO_API_CORS_ALLOW_ORIGIN: '*' volumes: - minio-data:/data healthcheck: test: ['CMD-SHELL', 'curl -sf http://localhost:9000/minio/health/live || exit 1'] interval: 10s timeout: 5s retries: 5 networks: [altricade] # One-shot: create the media + avatars buckets, then exit. minio-setup: image: minio/mc:RELEASE.2025-04-16T18-13-26Z depends_on: minio: condition: service_healthy env_file: .env entrypoint: - /bin/sh - -c - | mc alias set local http://minio:9000 "$${MINIO_ROOT_USER}" "$${MINIO_ROOT_PASSWORD}" && mc mb --ignore-existing "local/$${MINIO_BUCKET_MEDIA}" && mc mb --ignore-existing "local/$${MINIO_BUCKET_AVATARS}" && mc anonymous set download "local/$${MINIO_BUCKET_AVATARS}" && echo "minio buckets ready (avatars public)" restart: 'no' networks: [altricade] centrifugo: image: centrifugo/centrifugo:v6 command: ['centrifugo', '-c', '/centrifugo/config.json'] environment: CENTRIFUGO_CLIENT_TOKEN_HMAC_SECRET_KEY: ${CENTRIFUGO_TOKEN_HMAC_SECRET} CENTRIFUGO_HTTP_API_KEY: ${CENTRIFUGO_API_KEY} volumes: - ./infra/centrifugo/config.json:/centrifugo/config.json:ro depends_on: redis: condition: service_healthy ulimits: nofile: soft: 65536 hard: 65536 networks: [altricade] # One-shot: run DB migrations, then exit. Backend waits for this to complete. migrate: build: context: . dockerfile: packages/backend/Dockerfile command: ['node_modules/.bin/node-pg-migrate', 'up'] env_file: .env depends_on: postgres: condition: service_healthy restart: 'no' networks: [altricade] backend: build: context: . dockerfile: packages/backend/Dockerfile env_file: .env depends_on: postgres: condition: service_healthy redis: condition: service_healthy minio: condition: service_healthy migrate: condition: service_completed_successfully networks: [altricade] # Notifications worker — drains the BullMQ queue and sends web/FCM pushes. # Reuses the backend image (tsup emits dist/worker.js). The infra/secrets dir # is mounted read-only so a Firebase service-account JSON can be provided # without rebuilding; FCM stays disabled until FCM_SERVICE_ACCOUNT_FILE is set. notifications: build: context: . dockerfile: packages/backend/Dockerfile command: ['node', 'dist/worker.js'] env_file: .env volumes: - ./infra/secrets:/app/secrets:ro depends_on: postgres: condition: service_healthy redis: condition: service_healthy migrate: condition: service_completed_successfully networks: [altricade] nginx: image: nginx:1.27-alpine # Restart on failure: a concurrent stack restart can start nginx before the # backend's DNS entry exists ("host not found in upstream"), which is fatal # at config load — retrying once dependencies are up self-heals the gateway. restart: unless-stopped volumes: - ./infra/nginx/nginx.conf:/etc/nginx/nginx.conf:ro depends_on: - backend - centrifugo networks: [altricade] volumes: postgres-data: redis-data: minio-data: networks: altricade: driver: bridge