services: postgres: # pgvector-enabled Postgres 16 (semantic search). Drop-in for postgres:16 data; # the 'vector' extension is created by the AddEmbeddingColumn migration. image: pgvector/pgvector:pg16 environment: POSTGRES_DB: inboxintel POSTGRES_USER: inboxintel # V-03: require an explicit strong password (fail fast if POSTGRES_PASSWORD is unset) # rather than silently defaulting to a guessable one. POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in deploy/.env} volumes: - pgdata:/var/lib/postgresql/data # V-03: bind to loopback only so the database is reachable from the host for local # tooling but NOT from other machines on the network. The api container reaches it # over the internal compose network regardless of this published port. ports: - "127.0.0.1:5432:5432" healthcheck: test: ["CMD-SHELL", "pg_isready -U inboxintel"] interval: 5s timeout: 5s retries: 10 # Nightly logical backups (RECOMMENDATIONS #3 — previously there were NONE). Dumps # rotate after BACKUP_KEEP_DAYS. The ./backups host directory should live on an # encrypted disk and be included in your off-machine backup regime (see SECURITY.md). # Restore: docker compose exec -T postgres psql -U inboxintel -d inboxintel < backups/.sql backup: image: pgvector/pgvector:pg16 entrypoint: /bin/sh command: - -c - | while true; do ts=$$(date -u +%Y%m%d-%H%M%S) if pg_dump -h postgres -U inboxintel -d inboxintel > /backups/inboxintel-$$ts.sql.tmp; then mv /backups/inboxintel-$$ts.sql.tmp /backups/inboxintel-$$ts.sql echo "backup OK: inboxintel-$$ts.sql" else rm -f /backups/inboxintel-$$ts.sql.tmp echo "backup FAILED at $$ts" >&2 fi find /backups -name 'inboxintel-*.sql' -mtime +$${BACKUP_KEEP_DAYS:-7} -delete sleep 86400 done environment: PGPASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in deploy/.env} BACKUP_KEEP_DAYS: ${BACKUP_KEEP_DAYS:-7} volumes: - ./backups:/backups depends_on: postgres: condition: service_healthy api: build: context: . dockerfile: src/InboxIntel.Api/Dockerfile environment: ASPNETCORE_ENVIRONMENT: Production ASPNETCORE_URLS: http://+:8080 ConnectionStrings__Postgres: "Host=postgres;Port=5432;Database=inboxintel;Username=inboxintel;Password=${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in deploy/.env}" DataProtection__KeyPath: /keys GoogleOAuth__ClientId: ${GOOGLE_CLIENT_ID:-} GoogleOAuth__ClientSecret: ${GOOGLE_CLIENT_SECRET:-} Ai__Mode: ${AI_MODE:-Disabled} # Dev mode shows the dev banner and caps the initial sync. Set DEV_MODE=true # and MAX_MESSAGES=1000 in deploy/.env to exercise it in this Docker setup. App__DevMode: ${DEV_MODE:-false} GmailSync__MaxMessages: ${MAX_MESSAGES:-0} Cors__Origins__0: ${FRONTEND_ORIGIN:-http://localhost:8081} volumes: - keys:/keys depends_on: postgres: condition: service_healthy # V-08: bind to loopback so the API is not directly reachable from the network # (only via the frontend/nginx proxy over the internal compose network). This # prevents external clients from bypassing the proxy to spoof X-Forwarded-* headers. ports: - "127.0.0.1:8080:8080" frontend: build: context: ./frontend dockerfile: Dockerfile depends_on: - api ports: - "8081:80" # Optional reverse proxy. Enable with: docker compose --profile proxy up nginx: image: nginx:alpine profiles: ["proxy"] volumes: - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro depends_on: - api - frontend ports: - "80:80" volumes: pgdata: keys: