services: webserver: build: context: . dockerfile: nginx/Dockerfile container_name: nginx_webserver volumes: - nginx_logs:/var/log/nginx restart: unless-stopped depends_on: api: condition: service_started networks: - proxy # external — reachable by your reverse proxy / traefik - internal # needs a path to the api container api: build: . container_name: wp_api environment: # Preferred: the API builds its own connection string from these and # encodes the password automatically (no manual URL-encoding needed). POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: ${POSTGRES_DB} POSTGRES_HOST: db # Optional full-URL override (must be URL-encoded if used). DATABASE_URL: ${DATABASE_URL:-} # Signs login session cookies. REQUIRED — compose fails fast if it's unset, # and the API refuses to start in production without it (see server/auth.py). AUTH_SECRET_KEY: ${AUTH_SECRET_KEY:?set AUTH_SECRET_KEY in .env (see server/.env.example)} AUTH_SESSION_HOURS: ${AUTH_SESSION_HOURS:-12} # Optional — SMTP password for WP-assignment emails. Email is off by # default and enabled from the Admin console; this is the only email # secret and it is never stored in the DB. Leave unset until configured. SMTP_PASSWORD: ${SMTP_PASSWORD:-} # D13 — domain authentication. REQUIRED: the suite stores no passwords and # has no local fallback, so a wrong value here means nobody can sign in. # Connect to the DOMAIN NAME, never a DC or an IP (SAN + DNS round-robin # across six DCs). See server/ldap_auth.py and DEPLOYMENT.md. LDAP_DOMAIN: ${LDAP_DOMAIN:-prime.local} LDAP_HOST: ${LDAP_HOST:-prime.local} # Trust anchor for the DC certificate — public CA certs, baked into the image # at server/certs/. Override only to point at a mounted bundle. LDAP_CA_FILE: ${LDAP_CA_FILE:-/app/server/certs/prime-ca-chain.pem} # AD group required to sign in. Empty = any domain account. This is the # initial value; the live one is set in the Admin console (T10.5). LDAP_REQUIRED_GROUP: ${LDAP_REQUIRED_GROUP:-} # Optional — read-only SQL Server connection to the Micron asset catalog, # which backs the asset picker in the work package creator. Leave unset and # the picker cleanly falls back to manual entry (see server/assets_db.py). # Use a db_datareader login: the app only ever SELECTs. MICRON_DB_URL: ${MICRON_DB_URL:-} restart: unless-stopped depends_on: db: condition: service_healthy # waits for postgres to accept connections networks: - internal # Reaching the Micron database — and, since D13, the domain controllers — # means leaving this compose project, and `internal` is deliberately # egress-free. `outbound` is attached to the api container ONLY; the database # and backup containers stay sealed. # # DO NOT DETACH THIS. It used to be optional ("detach it if you are not using # the Micron asset picker"), but authentication now needs a route to # prime.local:636. Without it every sign-in fails and there is no local # password fallback to fall back to. - outbound db: image: postgres:16-alpine container_name: wp_db environment: POSTGRES_DB: ${POSTGRES_DB} POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} volumes: - pgdata:/var/lib/postgresql/data restart: unless-stopped healthcheck: test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] interval: 10s timeout: 5s retries: 5 networks: - internal # Scheduled pg_dump backups. Writes gzipped, timestamped dumps to ./backups on # the host (sync that folder offsite from the host — this container has no # internet egress). See scripts/db-backup.sh and DEPLOYMENT.md § Backups. backup: build: context: . dockerfile: scripts/backup.Dockerfile # postgres client + openssl container_name: wp_db_backup environment: POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: ${POSTGRES_DB} PGHOST: db BACKUP_DIR: /backups BACKUP_KEEP: ${BACKUP_KEEP:-14} # keep the newest N dumps BACKUP_INTERVAL_SECONDS: ${BACKUP_INTERVAL_SECONDS:-86400} # 86400 = daily # Set BACKUP_ENC_PASSPHRASE in .env to encrypt dumps at rest (AES-256). # Required once the DB holds customer IP. Keep the passphrase off this host. BACKUP_ENC_PASSPHRASE: ${BACKUP_ENC_PASSPHRASE:-} volumes: - ./scripts:/scripts:ro - ./backups:/backups restart: unless-stopped depends_on: db: condition: service_healthy networks: - internal volumes: pgdata: nginx_logs: networks: proxy: name: proxy external: true internal: internal: true # no route off the host for anything on this network alone outbound: # An ordinary bridge network, i.e. one that HAS a default gateway. `internal` # above removes the gateway entirely, which blocks not just the internet but # the LAN and the VPN too — so the api container needs this second network to # reach the domain controllers (LDAPS, D13) and the Micron asset database. # Attached to `api` alone: `db` and `backup` remain on `internal` only and # still have no way off the host. Required — see the note on the api service. driver: bridge