Files
HealthLog/docker-compose.yml
T
Marc-André Bombeck 38320360a3 fix(deploy): healthcheck uses 127.0.0.1 + image pinned to :1.4.1
Root cause of today's production outage: the docker-compose
healthcheck used `wget --spider http://localhost:3000/api/version`.
Inside Alpine containers, busybox-wget resolves `localhost` to
IPv6 `::1` first. Next.js standalone (server.js) listens on IPv4
`0.0.0.0:3000` only — `::1` is not bound, so wget gets ECONNREFUSED.
Docker marks the container unhealthy after 3 retries; Traefik takes
the upstream out of rotation; Cloudflare returns 503 "no available
server" because there's no healthy origin behind the route.

Confirmed via SSH on apps-01:
  $ docker exec <container> wget -O- http://localhost:3000/api/version
  Connecting to localhost:3000 ([::1]:3000)
  wget: can't connect to remote host: Connection refused
  $ netstat -tlnp
  tcp  0.0.0.0:3000  LISTEN  1/next-server   (IPv4 only, as expected)

Switching the URL to `http://127.0.0.1:3000/api/version` forces IPv4
and matches the Dockerfile's own HEALTHCHECK directive (which already
used 127.0.0.1). Same fix shape for self-hosters: their `wget` will
do the same thing on Alpine.

This explains why today's deploys looked broken even after rolling
back to v1.4.0 source and even after pinning to :1.3.3. The image
contents were fine; the healthcheck path was always going to fail
on this Alpine + Next.js combo. Why it worked yesterday: the
previous deploy chain happened to skip the new healthcheck override
in the compose file (the Dockerfile-level HEALTHCHECK uses 127.0.0.1
and was authoritative). Today's compose-level override flipped that.

Image pinned back to `:1.4.1` (the multi-arch GHCR build that landed
this afternoon — both `1.4.1` and `1.4.0` are healthy images, the
healthcheck is what was sick).

Co-Authored-By: Marc-André Bombeck <mbombeck@gmail.com>
2026-05-08 17:34:04 +02:00

100 lines
3.8 KiB
YAML

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:
# Pinned to GHCR :1.3.3 multi-arch image (gestern released, audit-tested).
# Production on apps-01 has been 503-ing since today's deploys; rolling back
# to the last release that's verifiably running on self-host installs to
# rule out runtime / Docker-network issues. No `build:` block on purpose:
# we want Coolify to PULL the prebuilt image, not build from source.
image: ghcr.io/mbombeck/healthlog:1.4.1
container_name: healthlog-app
ports:
- "3000:3000"
environment:
DATABASE_URL: "postgresql://healthlog:${POSTGRES_PASSWORD}@db:5432/healthlog?schema=public"
NODE_ENV: production
SESSION_SECRET: "${SESSION_SECRET}"
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}"
# 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}"
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
# SESSION_SECRET: "${SESSION_SECRET}"
# 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:
image: postgres:16-alpine
container_name: healthlog-db
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: