services: postgres: image: postgres:16-alpine environment: POSTGRES_DB: service_center POSTGRES_USER: service_center # No fallback default — a missing .env value must stop the stack, not # silently start Postgres with a well-known password. POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set in .env} volumes: - pg_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U service_center"] interval: 5s timeout: 5s retries: 5 restart: unless-stopped ports: # Bound to this host's Tailscale IP only (never 0.0.0.0) — lets a # standby replica's pg_basebackup/streaming replication reach this # primary over the private tailnet, never the public internet. Adjust # or drop this binding entirely if you don't run a failover replica. - "${TAILSCALE_IP:-127.0.0.1}:5432:5432" minio: image: minio/minio command: server /data --console-address ":9001" environment: # No fallback defaults — see POSTGRES_PASSWORD above for why. MINIO_ROOT_USER: ${MINIO_ACCESS_KEY:?MINIO_ACCESS_KEY must be set in .env} MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY:?MINIO_SECRET_KEY must be set in .env} volumes: - minio_data:/data healthcheck: test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] interval: 10s timeout: 5s retries: 5 restart: unless-stopped ports: # Same Tailscale-only binding as postgres above — lets a standby # replica reach this bucket for MinIO bucket replication. - "${TAILSCALE_IP:-127.0.0.1}:9000:9000" backend: build: context: ./backend dockerfile: Dockerfile env_file: .env ports: # loopback-only — the host's system Caddy (/etc/caddy/Caddyfile) proxies # to this port once a domain is chosen. Handy if this host runs other # projects too, each on its own loopback port. - "127.0.0.1:18090:3000" networks: default: {} platform: aliases: - core depends_on: postgres: condition: service_healthy minio: condition: service_healthy restart: unless-stopped # web (React+Vite frontend, UI ported from Glass CRM) joins here in Phase 1 volumes: pg_data: minio_data: networks: # Shared with module repos (e.g. production/) so they can reach core by # name for heartbeat calls — core itself never dials out to modules. platform: name: platform_net external: true