services: # Single-container default (HEALTHLOG_PROCESS_TYPE=all). Existing # self-host deployments stay working with `docker compose up`. # For horizontal scaling switch to the web/worker split below # (HEALTHLOG_PROCESS_TYPE=web on this service + uncomment app-worker). app: # Tracks the GHCR `:latest` multi-arch image. `pull_policy: always` # is the load-bearing line — without it, Docker keeps a stale # `:latest` digest cached on the host and skips the registry round- # trip on `compose up`, which is what kept Coolify re-deploying the # previous image after every tag push. With pull_policy=always the # compose-up always re-checks the registry digest and pulls when # GHCR has a fresher manifest. The small bandwidth cost on every # `compose up` is the price of digest-correctness. image: ghcr.io/mbombeck/healthlog:latest pull_policy: always container_name: healthlog-app # The app runs as the unprivileged `nextjs` user and never needs a # setuid/setgid escalation; blocking privilege gain caps what a # compromised process inside the container can do. security_opt: - no-new-privileges:true ports: - "3000:3000" environment: # connection_limit is set explicitly: Prisma's own default is # cpu_count*2+1, which starves the combined web+worker process # (HEALTHLOG_PROCESS_TYPE=all) under background AI/rollup load. Default # 20 suits a small host; raise DB_CONNECTION_LIMIT on a busy one (keep it # below the database's max_connections, leaving headroom for pg-boss, # migrations, and backups). pool_timeout is the seconds a query waits for # a free connection before erroring. DATABASE_URL: "postgresql://healthlog:${POSTGRES_PASSWORD}@db:5432/healthlog?schema=public&connection_limit=${DB_CONNECTION_LIMIT:-20}&pool_timeout=${DB_POOL_TIMEOUT:-20}" NODE_ENV: production ENCRYPTION_KEY: "${ENCRYPTION_KEY}" ENCRYPTION_KEYS: "${ENCRYPTION_KEYS:-}" ENCRYPTION_ACTIVE_KEY_ID: "${ENCRYPTION_ACTIVE_KEY_ID:-}" API_TOKEN_HMAC_KEY: "${API_TOKEN_HMAC_KEY}" NEXT_PUBLIC_APP_URL: "${NEXT_PUBLIC_APP_URL:-http://localhost:3000}" APP_URL: "${APP_URL:-http://localhost:3000}" # v1.5.1 session-cookie Secure-flag opt-out. Forwarded explicitly so # an operator setting SESSION_COOKIE_SECURE=false in .env actually # reaches the Node process (the compose `environment:` block is a # whitelist — vars not listed here don't propagate from .env into # the container, even though docker-compose reads .env for # `${VAR}` substitution at compose-up time). SESSION_COOKIE_SECURE: "${SESSION_COOKIE_SECURE:-}" # all = bundle web + worker in this container (default, single-host) # web = serve HTTP only; pair with a separate app-worker service HEALTHLOG_PROCESS_TYPE: "${HEALTHLOG_PROCESS_TYPE:-all}" BACKUP_ENCRYPTION_KEY: "${BACKUP_ENCRYPTION_KEY:-}" BACKUP_S3_ENDPOINT: "${BACKUP_S3_ENDPOINT:-}" BACKUP_S3_BUCKET: "${BACKUP_S3_BUCKET:-}" BACKUP_S3_ACCESS_KEY: "${BACKUP_S3_ACCESS_KEY:-}" BACKUP_S3_SECRET_KEY: "${BACKUP_S3_SECRET_KEY:-}" BACKUP_S3_REGION: "${BACKUP_S3_REGION:-auto}" BACKUP_RETENTION_DAYS: "${BACKUP_RETENTION_DAYS:-30}" # Per-export ceiling on MedicationAdministration rows in the FHIR # health-record Bundle. Bounded 1..50000, default 5000. Listed here so # an operator override in .env actually reaches the container (the # `environment:` block is a whitelist). FHIR_MAX_MEDICATION_ADMINISTRATIONS: "${FHIR_MAX_MEDICATION_ADMINISTRATIONS:-}" # WHOOP integration (optional). The app client id/secret are per-user # BYO-keys stored encrypted in the DB, so there is no # WHOOP_CLIENT_ID/SECRET env. WHOOP_WEBHOOK_SECRET is the instance-level # path-segment + HMAC-verification secret for the webhook; # WHOOP_REDIRECT_URI overrides the derived # `${NEXT_PUBLIC_APP_URL}/api/whoop/callback` when the public URL # differs. Listed here so an operator override in .env reaches the # container (the `environment:` block is a whitelist). WHOOP_WEBHOOK_SECRET: "${WHOOP_WEBHOOK_SECRET:-}" WHOOP_REDIRECT_URI: "${WHOOP_REDIRECT_URI:-}" # Deploy-result webhook (optional). DEPLOY_WEBHOOK_SECRET authenticates # POST /api/internal/deploy-webhook; DEPLOY_LOGS_URL is the # deployment-dashboard URL (e.g. the Coolify UI) linked in # deploy-failure notifications. Listed here so operator values in # .env reach the container (the `environment:` block is a whitelist). DEPLOY_WEBHOOK_SECRET: "${DEPLOY_WEBHOOK_SECRET:-}" DEPLOY_LOGS_URL: "${DEPLOY_LOGS_URL:-}" # Optional replay hardening: when "true", the deploy webhook # requires a fresh X-Deploy-Webhook-Timestamp header (±5 min). # See docs/ops/deploy.md. DEPLOY_WEBHOOK_REQUIRE_TIMESTAMP: "${DEPLOY_WEBHOOK_REQUIRE_TIMESTAMP:-}" # TLS leaf SPKI-change alarm baseline (optional). The native client # pins the served TLS leaf; this is the known-good pin set the monitor # alarms against on a rotation. Comma-separated for the dual-pin # renewal window. Listed here so an operator value in .env reaches the # container (the `environment:` block is a whitelist). See # docs/ops/tls-cert-pin.md. TLS_LEAF_SPKI_PINS: "${TLS_LEAF_SPKI_PINS:-}" depends_on: db: condition: service_healthy restart: unless-stopped healthcheck: test: [ "CMD-SHELL", "wget --no-verbose --tries=1 --spider http://127.0.0.1:3000/api/version || exit 1", ] interval: 30s timeout: 5s retries: 3 start_period: 30s # Optional dedicated worker container — uncomment + set # HEALTHLOG_PROCESS_TYPE=web on `app` to split the deployment. # See docs/self-hosting/scaling.md for the full guide. # # app-worker: # image: ghcr.io/mbombeck/healthlog:latest # build: # context: . # dockerfile: Dockerfile # container_name: healthlog-worker # environment: # DATABASE_URL: "postgresql://healthlog:${POSTGRES_PASSWORD}@db:5432/healthlog?schema=public" # NODE_ENV: production # ENCRYPTION_KEY: "${ENCRYPTION_KEY}" # ENCRYPTION_KEYS: "${ENCRYPTION_KEYS:-}" # ENCRYPTION_ACTIVE_KEY_ID: "${ENCRYPTION_ACTIVE_KEY_ID:-}" # API_TOKEN_HMAC_KEY: "${API_TOKEN_HMAC_KEY}" # APP_URL: "${APP_URL:-http://localhost:3000}" # HEALTHLOG_PROCESS_TYPE: "worker" # BACKUP_ENCRYPTION_KEY: "${BACKUP_ENCRYPTION_KEY:-}" # BACKUP_S3_ENDPOINT: "${BACKUP_S3_ENDPOINT:-}" # BACKUP_S3_BUCKET: "${BACKUP_S3_BUCKET:-}" # BACKUP_S3_ACCESS_KEY: "${BACKUP_S3_ACCESS_KEY:-}" # BACKUP_S3_SECRET_KEY: "${BACKUP_S3_SECRET_KEY:-}" # BACKUP_S3_REGION: "${BACKUP_S3_REGION:-auto}" # depends_on: # db: # condition: service_healthy # restart: unless-stopped db: # Pinned to the concrete minor instead of the floating `16-alpine` # alias so a database update is always a deliberate, reviewable # change (at pin time `16-alpine` resolved to this same digest, so # existing deployments see no image change). Minor releases share # the on-disk format — bumping this pin is safe without a dump / # restore; a major-version bump (17+) is not. image: postgres:16.14-alpine container_name: healthlog-db security_opt: - no-new-privileges:true environment: POSTGRES_USER: healthlog POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" POSTGRES_DB: healthlog volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U healthlog"] interval: 5s timeout: 5s retries: 5 restart: unless-stopped volumes: pgdata: