services: # ── Application Services ────────────────────────────────────────────────────── api: build: context: . dockerfile: apps/api/Dockerfile target: production image: ${REGISTRY_URL:-ghcr.io/goodgo}/goodgo-api:${IMAGE_TAG:-latest} container_name: goodgo-api restart: unless-stopped ports: - '127.0.0.1:${API_PORT:-3001}:3001' environment: NODE_ENV: production DATABASE_URL: postgresql://${DB_USER}:${DB_PASSWORD}@pgbouncer:6432/${DB_NAME} # Direct connection for migrations (bypasses PgBouncer — required for DDL) DATABASE_URL_DIRECT: postgresql://${DB_USER}:${DB_PASSWORD}@postgres:5432/${DB_NAME} REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379 TYPESENSE_HOST: typesense TYPESENSE_PORT: 8108 TYPESENSE_API_KEY: ${TYPESENSE_API_KEY} JWT_SECRET: ${JWT_SECRET} JWT_REFRESH_SECRET: ${JWT_REFRESH_SECRET} MINIO_ENDPOINT: minio MINIO_PORT: 9000 MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY} MINIO_SECRET_KEY: ${MINIO_SECRET_KEY} AI_SERVICES_URL: http://ai-services:8000 AI_SERVICES_API_KEY: ${AI_API_KEY} RUN_MIGRATIONS: ${RUN_MIGRATIONS:-false} depends_on: pgbouncer: condition: service_healthy redis: condition: service_healthy typesense: condition: service_healthy healthcheck: test: ['CMD', 'node', '-e', "fetch('http://localhost:3001/health').then(r => { if (!r.ok) throw 1 }).catch(() => process.exit(1))"] interval: 30s timeout: 5s retries: 5 start_period: 30s deploy: resources: limits: memory: 1g cpus: '1.0' reservations: memory: 512m security_opt: - no-new-privileges:true read_only: true tmpfs: - /tmp logging: driver: json-file options: max-size: '10m' max-file: '5' networks: - goodgo-net web: image: ${REGISTRY_URL:-ghcr.io/goodgo}/goodgo-web:${IMAGE_TAG:-latest} container_name: goodgo-web restart: unless-stopped ports: - '127.0.0.1:${WEB_PORT:-3000}:3000' environment: NODE_ENV: production NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://api:3001} depends_on: api: condition: service_healthy healthcheck: test: ['CMD', 'node', '-e', "fetch('http://localhost:3000').then(r => { if (!r.ok) throw 1 }).catch(() => process.exit(1))"] interval: 30s timeout: 5s retries: 3 start_period: 15s deploy: resources: limits: memory: 512m cpus: '0.5' reservations: memory: 256m security_opt: - no-new-privileges:true read_only: true tmpfs: - /tmp logging: driver: json-file options: max-size: '10m' max-file: '5' networks: - goodgo-net ai-services: image: ${REGISTRY_URL:-ghcr.io/goodgo}/goodgo-ai-services:${IMAGE_TAG:-latest} container_name: goodgo-ai-services restart: unless-stopped environment: AI_DEBUG: 'false' AI_LOG_LEVEL: info AI_API_KEY: ${AI_API_KEY} AI_RATE_LIMIT: ${AI_RATE_LIMIT:-60/minute} healthcheck: test: ['CMD', 'python', '-c', 'import httpx; httpx.get("http://localhost:8000/health").raise_for_status()'] interval: 30s timeout: 5s retries: 5 start_period: 30s deploy: resources: limits: memory: 1g cpus: '1.0' reservations: memory: 512m security_opt: - no-new-privileges:true read_only: true tmpfs: - /tmp logging: driver: json-file options: max-size: '10m' max-file: '5' networks: - goodgo-net # ── Data Services ───────────────────────────────────────────────────────────── postgres: image: postgis/postgis:16-3.4 container_name: goodgo-postgres restart: unless-stopped environment: POSTGRES_DB: ${DB_NAME} POSTGRES_USER: ${DB_USER} POSTGRES_PASSWORD: ${DB_PASSWORD} volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ['CMD-SHELL', 'pg_isready -U ${DB_USER} -d ${DB_NAME}'] interval: 10s timeout: 5s retries: 5 start_period: 30s deploy: resources: limits: memory: 2g cpus: '2.0' reservations: memory: 1g shm_size: 256m logging: driver: json-file options: max-size: '10m' max-file: '5' networks: - goodgo-net # ── Connection Pooling ───────────────────────────────────────────────────── pgbouncer: image: edoburu/pgbouncer:1.23.1-p2 container_name: goodgo-pgbouncer restart: unless-stopped entrypoint: ['/bin/sh', '/etc/pgbouncer/entrypoint.sh'] environment: DB_USER: ${DB_USER} DB_PASSWORD: ${DB_PASSWORD} PGBOUNCER_POOL_SIZE: ${PGBOUNCER_POOL_SIZE:-20} PGBOUNCER_MAX_CLIENT_CONN: ${PGBOUNCER_MAX_CLIENT_CONN:-200} PGBOUNCER_ADMIN_PASSWORD: ${PGBOUNCER_ADMIN_PASSWORD:-pgbouncer_admin_secret} PGBOUNCER_STATS_PASSWORD: ${PGBOUNCER_STATS_PASSWORD:-pgbouncer_stats_secret} volumes: - ./infra/pgbouncer/pgbouncer.ini:/etc/pgbouncer/pgbouncer.ini:ro - ./infra/pgbouncer/userlist.txt.template:/etc/pgbouncer/userlist.txt.template:ro - ./infra/pgbouncer/entrypoint.sh:/etc/pgbouncer/entrypoint.sh:ro depends_on: postgres: condition: service_healthy healthcheck: test: ['CMD-SHELL', 'pg_isready -h 127.0.0.1 -p 6432 -U ${DB_USER}'] interval: 10s timeout: 5s retries: 5 start_period: 10s deploy: resources: limits: memory: 256m cpus: '0.5' reservations: memory: 64m security_opt: - no-new-privileges:true logging: driver: json-file options: max-size: '5m' max-file: '3' networks: - goodgo-net redis: image: redis:7-alpine container_name: goodgo-redis restart: unless-stopped command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD} --maxmemory 512mb --maxmemory-policy allkeys-lru volumes: - redis_data:/data healthcheck: test: ['CMD', 'redis-cli', '-a', '${REDIS_PASSWORD}', 'ping'] interval: 10s timeout: 5s retries: 5 start_period: 10s deploy: resources: limits: memory: 768m cpus: '0.5' reservations: memory: 256m security_opt: - no-new-privileges:true read_only: true tmpfs: - /tmp logging: driver: json-file options: max-size: '10m' max-file: '3' networks: - goodgo-net typesense: image: typesense/typesense:27.1 container_name: goodgo-typesense restart: unless-stopped environment: TYPESENSE_API_KEY: ${TYPESENSE_API_KEY} TYPESENSE_DATA_DIR: /data volumes: - typesense_data:/data healthcheck: test: ['CMD', 'curl', '-sf', 'http://localhost:8108/health'] interval: 10s timeout: 5s retries: 5 start_period: 15s deploy: resources: limits: memory: 1g cpus: '1.0' reservations: memory: 512m logging: driver: json-file options: max-size: '10m' max-file: '3' networks: - goodgo-net minio: image: minio/minio:latest container_name: goodgo-minio restart: unless-stopped command: server /data --console-address ":9001" environment: MINIO_ROOT_USER: ${MINIO_ACCESS_KEY} MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY} volumes: - minio_data:/data healthcheck: test: ['CMD', 'curl', '-sf', 'http://localhost:9000/minio/health/live'] interval: 10s timeout: 5s retries: 5 start_period: 15s deploy: resources: limits: memory: 1g cpus: '0.5' reservations: memory: 256m logging: driver: json-file options: max-size: '10m' max-file: '3' networks: - goodgo-net # ── Database Backup ─────────────────────────────────────────────────────────── pg-backup: image: postgis/postgis:16-3.4 container_name: goodgo-pg-backup restart: unless-stopped entrypoint: /bin/bash command: - -c - | apt-get update -qq && apt-get install -y -qq cron > /dev/null 2>&1 (echo "0 2 * * * PGHOST=postgres PGPORT=5432 PGUSER=${DB_USER} PGDATABASE=${DB_NAME} PGPASSWORD=${DB_PASSWORD} BACKUP_DIR=/backups RETENTION_DAYS=${BACKUP_RETENTION_DAYS:-7} /scripts/pg-backup.sh >> /var/log/pg-backup.log 2>&1" echo "0 4 * * * PGHOST=postgres PGPORT=5432 PGUSER=${DB_USER} PGDATABASE=${DB_NAME} PGPASSWORD=${DB_PASSWORD} BACKUP_DIR=/backups REPORT_FILE=/backups/verify-latest.json /scripts/pg-verify-backup.sh >> /var/log/pg-verify.log 2>&1") | crontab - /scripts/pg-backup.sh cron -f environment: PGHOST: postgres PGPORT: '5432' PGUSER: ${DB_USER} PGDATABASE: ${DB_NAME} PGPASSWORD: ${DB_PASSWORD} BACKUP_DIR: /backups RETENTION_DAYS: ${BACKUP_RETENTION_DAYS:-7} volumes: - ./scripts/backup:/scripts:ro - pg_backups:/backups depends_on: postgres: condition: service_healthy deploy: resources: limits: memory: 512m cpus: '0.5' logging: driver: json-file options: max-size: '5m' max-file: '3' networks: - goodgo-net # ── Monitoring & Logging ────────────────────────────────────────────────────── loki: image: grafana/loki:3.0.0 container_name: goodgo-loki restart: unless-stopped command: -config.file=/etc/loki/loki-config.yml volumes: - ./monitoring/loki/loki-config.yml:/etc/loki/loki-config.yml:ro - loki_data:/loki healthcheck: test: ['CMD', 'wget', '--spider', '-q', 'http://localhost:3100/ready'] interval: 15s timeout: 5s retries: 5 start_period: 20s deploy: resources: limits: memory: 512m cpus: '0.5' reservations: memory: 256m logging: driver: json-file options: max-size: '10m' max-file: '3' networks: - goodgo-net promtail: image: grafana/promtail:3.0.0 container_name: goodgo-promtail restart: unless-stopped command: -config.file=/etc/promtail/promtail-config.yml volumes: - ./monitoring/promtail/promtail-config.yml:/etc/promtail/promtail-config.yml:ro - /var/run/docker.sock:/var/run/docker.sock:ro depends_on: loki: condition: service_healthy deploy: resources: limits: memory: 256m cpus: '0.25' reservations: memory: 128m logging: driver: json-file options: max-size: '5m' max-file: '3' networks: - goodgo-net prometheus: image: prom/prometheus:v2.51.0 container_name: goodgo-prometheus restart: unless-stopped command: - '--config.file=/etc/prometheus/prometheus.yml' - '--storage.tsdb.retention.time=30d' - '--web.enable-lifecycle' volumes: - ./monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro - ./monitoring/prometheus/alert-rules.yml:/etc/prometheus/alert-rules.yml:ro - prometheus_data:/prometheus depends_on: alertmanager: condition: service_healthy healthcheck: test: ['CMD', 'wget', '--spider', '-q', 'http://localhost:9090/-/healthy'] interval: 15s timeout: 5s retries: 3 start_period: 10s deploy: resources: limits: memory: 1g cpus: '0.5' reservations: memory: 512m security_opt: - no-new-privileges:true logging: driver: json-file options: max-size: '10m' max-file: '3' networks: - goodgo-net alertmanager: image: prom/alertmanager:v0.27.0 container_name: goodgo-alertmanager restart: unless-stopped command: - '--config.file=/etc/alertmanager/alertmanager.yml' - '--storage.path=/alertmanager' - '--data.retention=120h' environment: SLACK_WEBHOOK_URL: ${SLACK_WEBHOOK_URL:-} volumes: - ./monitoring/alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro healthcheck: test: ['CMD', 'wget', '--spider', '-q', 'http://localhost:9093/-/healthy'] interval: 15s timeout: 5s retries: 3 start_period: 10s deploy: resources: limits: memory: 256m cpus: '0.25' reservations: memory: 64m security_opt: - no-new-privileges:true logging: driver: json-file options: max-size: '5m' max-file: '3' networks: - goodgo-net grafana: image: grafana/grafana:10.4.1 container_name: goodgo-grafana restart: unless-stopped ports: - '127.0.0.1:${GRAFANA_PORT:-3002}:3000' environment: GF_SECURITY_ADMIN_USER__FILE: /run/secrets/grafana_admin_user GF_SECURITY_ADMIN_PASSWORD__FILE: /run/secrets/grafana_admin_password GF_USERS_ALLOW_SIGN_UP: 'false' GF_SERVER_ROOT_URL: ${GRAFANA_ROOT_URL:-http://localhost:3002} secrets: - grafana_admin_user - grafana_admin_password volumes: - ./monitoring/grafana/provisioning:/etc/grafana/provisioning:ro - ./monitoring/grafana/dashboards:/var/lib/grafana/dashboards:ro - grafana_data:/var/lib/grafana depends_on: prometheus: condition: service_healthy loki: condition: service_healthy alertmanager: condition: service_healthy healthcheck: test: ['CMD', 'wget', '--spider', '-q', 'http://localhost:3000/api/health'] interval: 15s timeout: 5s retries: 3 start_period: 15s deploy: resources: limits: memory: 512m cpus: '0.5' reservations: memory: 256m security_opt: - no-new-privileges:true logging: driver: json-file options: max-size: '10m' max-file: '3' networks: - goodgo-net volumes: pgdata: driver: local redis_data: driver: local typesense_data: driver: local minio_data: driver: local pg_backups: driver: local loki_data: driver: local prometheus_data: driver: local grafana_data: driver: local secrets: grafana_admin_user: environment: GRAFANA_ADMIN_USER grafana_admin_password: environment: GRAFANA_ADMIN_PASSWORD networks: goodgo-net: driver: bridge name: goodgo-net