services: postgres: image: postgres:16-alpine container_name: corrosion-db environment: POSTGRES_DB: corrosion POSTGRES_USER: corrosion POSTGRES_PASSWORD: ${DB_PASSWORD:-corrosion_dev} volumes: - pg_data:/var/lib/postgresql/data # Auto-build the schema on a FRESH database. Postgres runs these ONLY when # the data dir is empty (first boot or after a volume reset), so it never # touches an existing volume — it just makes a fresh DB self-heal: the full # schema is applied in order from the sqlx migrations (001..NNN), then the # API's bootstrap seeds the admin. Rebuilds (with the volume kept) are a # no-op here; the data persists. Only `down -v` / volume prune loses data. - ../backend/migrations:/docker-entrypoint-initdb.d:ro ports: - "8101:5432" healthcheck: test: ["CMD-SHELL", "pg_isready -U corrosion"] interval: 5s timeout: 5s retries: 5 nats: image: nats:2.10-alpine container_name: corrosion-nats command: - "--config=/etc/nats/nats.conf" volumes: - nats_data:/data - ./nats.conf:/etc/nats/nats.conf:ro # Per-license authorization (generated on the host; carries secrets, not # committed with real users — see scripts/generate-nats-auth.mjs). - ./nats-auth.conf:/etc/nats/nats-auth.conf:ro ports: - "8089:4222" # Client connections api: build: context: ../backend-nest dockerfile: ../docker/Dockerfile.api.nestjs container_name: corrosion-api environment: DATABASE_URL: postgres://corrosion:${DB_PASSWORD:-corrosion_dev}@postgres:5432/corrosion DATABASE_MAX_CONNECTIONS: "20" NATS_URL: nats://nats:4222 # Privileged internal NATS user (full corrosion.> access). Empty = anonymous. NATS_INTERNAL_USER: ${NATS_INTERNAL_USER:-} NATS_INTERNAL_PASSWORD: ${NATS_INTERNAL_PASSWORD:-} # Secret for deriving per-license agent passwords (shared with the # nats-auth generator). HMAC-SHA256(license_id, secret). NATS_TOKEN_SECRET: ${NATS_TOKEN_SECRET:-} JWT_SECRET: ${JWT_SECRET} JWT_ACCESS_EXPIRY_SECONDS: "14400" JWT_REFRESH_EXPIRY_SECONDS: "604800" ENCRYPTION_KEY: ${ENCRYPTION_KEY} CLOUDFLARE_API_TOKEN: ${CLOUDFLARE_API_TOKEN} CLOUDFLARE_ZONE_ID: ${CLOUDFLARE_ZONE_ID} BASE_DOMAIN: ${BASE_DOMAIN:-corrosionmgmt.com} STEAM_API_KEY: ${STEAM_API_KEY} SMTP_HOST: ${SMTP_HOST:-localhost} SMTP_PORT: ${SMTP_PORT:-587} SMTP_USERNAME: ${SMTP_USERNAME} SMTP_PASSWORD: ${SMTP_PASSWORD} SMTP_FROM: ${SMTP_FROM:-noreply@corrosionmgmt.com} FRONTEND_URL: ${FRONTEND_URL:-https://panel.corrosionmgmt.com} ADMIN_EMAIL: ${ADMIN_EMAIL} ADMIN_PASSWORD: ${ADMIN_PASSWORD} ADMIN_USERNAME: ${ADMIN_USERNAME:-Commander} ADMIN_LICENSE_KEY: ${ADMIN_LICENSE_KEY:-} API_PORT: "3000" volumes: - map_data:/data/maps - backup_data:/data/backups depends_on: postgres: condition: service_healthy nats: condition: service_started ports: - "8088:3000" nginx: build: context: ../frontend dockerfile: ../docker/Dockerfile.nginx container_name: corrosion-nginx ports: - "8087:80" volumes: - ./nginx.conf:/etc/nginx/nginx.conf:ro - map_data:/data/maps:ro depends_on: api: condition: service_started healthcheck: # 127.0.0.1, not localhost: nginx listens IPv4-only (0.0.0.0:80) but # `localhost` resolves to ::1 first inside the container → the probe hit # nothing and reported unhealthy while the panel served fine on IPv4. test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1:80/ || exit 1"] interval: 10s timeout: 5s retries: 3 start_period: 10s volumes: pg_data: nats_data: map_data: backup_data: