services: postgres: image: postgres:16 environment: POSTGRES_USER: monlet POSTGRES_PASSWORD: monlet POSTGRES_DB: monlet POSTGRES_INITDB_ARGS: "--auth-local=scram-sha-256 --auth-host=scram-sha-256" TZ: UTC healthcheck: test: ["CMD-SHELL", "pg_isready -U monlet -d monlet"] interval: 5s timeout: 3s retries: 20 volumes: - monlet_pg:/var/lib/postgresql/data ports: - "${MONLET_POSTGRES_HOST_PORT:-5432}:5432" server: build: context: ../server depends_on: postgres: condition: service_healthy environment: MONLET_AUTH_TOKEN: ${MONLET_AUTH_TOKEN:?MONLET_AUTH_TOKEN is required} MONLET_DATABASE_URL: postgresql+asyncpg://monlet:monlet@postgres:5432/monlet MONLET_LOG_LEVEL: ${MONLET_LOG_LEVEL:-INFO} MONLET_ENABLE_DETECTOR: "true" MONLET_ENABLE_NOTIFIER_WORKER: "true" MONLET_NOTIFICATION_TIME_ZONE: ${MONLET_NOTIFICATION_TIME_ZONE:-UTC} MONLET_NOTIFIER_DEBUG_ENABLED: "true" TZ: UTC command: - sh - -c - "alembic upgrade head && uvicorn monlet_server.main:app --host 0.0.0.0 --port 8000" ports: - "${MONLET_SERVER_HOST_PORT:-8000}:8000" healthcheck: test: ["CMD-SHELL", "python -c 'import urllib.request,sys;sys.exit(0) if urllib.request.urlopen(\"http://localhost:8000/api/v1/ready\").status==200 else sys.exit(1)'"] interval: 5s timeout: 3s retries: 30 # PH-015: production hardening for runtime containers. read_only: true tmpfs: - /tmp:size=64M security_opt: - no-new-privileges:true cap_drop: - ALL stop_grace_period: 15s web: build: context: ../web depends_on: server: condition: service_healthy environment: MONLET_API_BASE_URL: http://server:8000 MONLET_API_TOKEN: ${MONLET_AUTH_TOKEN:?MONLET_AUTH_TOKEN is required} MONLET_WEB_TIME_ZONE: ${MONLET_WEB_TIME_ZONE:-UTC} MONLET_WEB_AUTH_USERNAME: ${MONLET_WEB_AUTH_USERNAME:-} MONLET_WEB_AUTH_PASSWORD: ${MONLET_WEB_AUTH_PASSWORD:-} MONLET_WEB_SESSION_SECRET: ${MONLET_WEB_SESSION_SECRET:-} MONLET_WEB_SESSION_TTL_SEC: ${MONLET_WEB_SESSION_TTL_SEC:-604800} MONLET_WEB_TRUST_PROXY_AUTH: ${MONLET_WEB_TRUST_PROXY_AUTH:-false} TZ: ${MONLET_WEB_TIME_ZONE:-UTC} ports: - "${MONLET_WEB_HOST_PORT:-3000}:3000" # PH-015: production hardening for the Next.js runtime. Next writes a small # cache under .next/cache so we keep that path writable via tmpfs. read_only: true tmpfs: - /tmp:size=64M - /app/.next/cache:size=64M security_opt: - no-new-privileges:true cap_drop: - ALL healthcheck: test: ["CMD-SHELL", "wget -q -O- http://localhost:3000/api/health >/dev/null 2>&1 || exit 1"] interval: 10s timeout: 10s retries: 10 start_period: 30s stop_grace_period: 15s prometheus: image: prom/prometheus:v2.55.0 profiles: ["observability"] volumes: - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro ports: - "9090:9090" depends_on: - server alertmanager: image: prom/alertmanager:v0.27.0 profiles: ["observability"] volumes: - ./alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro ports: - "9093:9093" grafana: image: grafana/grafana:11.3.0 profiles: ["observability"] environment: GF_SECURITY_ADMIN_PASSWORD: ${GF_ADMIN_PASSWORD:-admin} volumes: - ./grafana/provisioning:/etc/grafana/provisioning:ro - ./grafana/dashboards:/var/lib/grafana/dashboards:ro ports: - "3001:3000" volumes: monlet_pg: