services: postgres: restart: unless-stopped # 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: restart: unless-stopped 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 # One-shot: ensure the DataProtection 'keys' volume is owned by the API's non-root # 'app' user (uid 1654). A volume created by an older root-running image is root-owned, # which makes the app fail to read its key ring and 500s on login. Runs as root, chowns, # exits; the api waits for it. Idempotent and cheap. init-keys: image: busybox command: ["sh", "-c", "chown -R 1654:1654 /keys"] volumes: - keys:/keys api: restart: unless-stopped 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} # Points at the compose 'ollama' service when the ai profile is up; harmless otherwise. Ai__OllamaBaseUrl: ${OLLAMA_BASE_URL:-http://ollama:11434} # OTLP export activates only when set (e.g. http://lgtm:4317 with the observability profile). OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_ENDPOINT:-} # 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 init-keys: condition: service_completed_successfully # 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: restart: unless-stopped build: context: ./frontend dockerfile: Dockerfile depends_on: - api ports: - "8081:80" # Local AI (semantic search + assistants). Enable with: # docker compose --profile ai up -d && set AI_MODE=LocalOllama in deploy/.env # First run: docker compose exec ollama ollama pull nomic-embed-text # GPU (RTX 3080): uncomment the deploy block to pass the GPU through. ollama: image: ollama/ollama profiles: ["ai"] volumes: - ollama:/root/.ollama # deploy: # resources: # reservations: # devices: # - driver: nvidia # count: all # capabilities: [gpu] # Observability (RECOMMENDATIONS #5): all-in-one Grafana+Tempo+Prometheus+Loki. # Enable with: docker compose --profile observability up -d # then set OTEL_ENDPOINT=http://lgtm:4317 in deploy/.env and restart the api. # Grafana UI: http://localhost:3000 (admin/admin on first run). lgtm: image: grafana/otel-lgtm profiles: ["observability"] ports: - "127.0.0.1:3000:3000" # 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: ollama: