Verified live via WSL Docker (docker compose up --wait): every other F0 service came up healthy, but web stayed unhealthy. Root cause was two compounding issues, both invisible to static review: 1. Docker auto-sets HOSTNAME to the container ID; Next's standalone server.js honors that env var and binds only to it instead of all interfaces. Fixed by explicitly setting ENV HOSTNAME=0.0.0.0 in the Dockerfile, after Docker's automatic assignment. 2. Even bound to 0.0.0.0 (IPv4), the healthcheck's `wget http://localhost` resolved localhost to ::1 (IPv6) first inside the container, where nothing was listening. Fixed by pointing the healthcheck at 127.0.0.1 explicitly instead of localhost. Full stack (db, redis, migrate, auth, storage, protocols, web) now reaches healthy end-to-end under docker compose up --wait.
130 lines
4.0 KiB
YAML
130 lines
4.0 KiB
YAML
x-logging: &default-logging
|
|
driver: json-file
|
|
options: { max-size: "10m", max-file: "3" }
|
|
|
|
services:
|
|
db:
|
|
image: postgres:16-alpine
|
|
restart: unless-stopped
|
|
logging: *default-logging
|
|
environment:
|
|
POSTGRES_DB: ${POSTGRES_DB:?required}
|
|
POSTGRES_USER: ${POSTGRES_USER:?required}
|
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?required}
|
|
ports: ["5432:5432"]
|
|
volumes:
|
|
- postgres_data:/var/lib/postgresql/data
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
|
|
interval: 5s
|
|
timeout: 5s
|
|
retries: 10
|
|
|
|
redis:
|
|
image: redis:7-alpine
|
|
restart: unless-stopped
|
|
logging: *default-logging
|
|
command: >
|
|
redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:?required}
|
|
environment:
|
|
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
|
volumes: [redis_data:/data]
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "redis-cli --no-auth-warning -a $${REDIS_PASSWORD} ping | grep -q PONG"]
|
|
interval: 5s
|
|
timeout: 5s
|
|
retries: 10
|
|
|
|
migrate:
|
|
image: migrate/migrate:v4.17.1
|
|
restart: "no"
|
|
volumes: ["./migrations:/migrations"]
|
|
depends_on:
|
|
db: { condition: service_healthy }
|
|
entrypoint:
|
|
- migrate
|
|
- -path=/migrations
|
|
- -database
|
|
- postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}?sslmode=disable
|
|
- up
|
|
|
|
auth:
|
|
build: { context: ./services/auth }
|
|
restart: unless-stopped
|
|
logging: *default-logging
|
|
environment:
|
|
PORT: 3001
|
|
DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}?sslmode=disable
|
|
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379
|
|
JWT_SECRET: ${JWT_SECRET:?required}
|
|
LAMBDAMAIL_MASTER_KEY: ${LAMBDAMAIL_MASTER_KEY:?required}
|
|
depends_on:
|
|
db: { condition: service_healthy }
|
|
redis: { condition: service_healthy }
|
|
migrate: { condition: service_completed_successfully }
|
|
healthcheck:
|
|
test: ["CMD", "node", "-e", "fetch('http://localhost:3001/health/ready').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 5
|
|
start_period: 15s
|
|
|
|
storage:
|
|
build: { context: ./services/storage }
|
|
restart: unless-stopped
|
|
logging: *default-logging
|
|
environment:
|
|
PORT: 3002
|
|
DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}?sslmode=disable
|
|
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379
|
|
depends_on:
|
|
db: { condition: service_healthy }
|
|
redis: { condition: service_healthy }
|
|
migrate: { condition: service_completed_successfully }
|
|
healthcheck:
|
|
test: ["CMD", "node", "-e", "fetch('http://localhost:3002/health/ready').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 5
|
|
start_period: 15s
|
|
|
|
protocols:
|
|
build: { context: ./services/protocols }
|
|
restart: unless-stopped
|
|
logging: *default-logging
|
|
ports: ["8080:8080"]
|
|
environment:
|
|
DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}?sslmode=disable
|
|
depends_on:
|
|
db: { condition: service_healthy }
|
|
migrate: { condition: service_completed_successfully }
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/health/ready || exit 1"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 5
|
|
start_period: 15s
|
|
|
|
web:
|
|
build: { context: ./services/webmail }
|
|
restart: unless-stopped
|
|
logging: *default-logging
|
|
ports: ["3000:3000"]
|
|
environment:
|
|
PORT: 3000
|
|
AUTH_SERVICE_URL: http://auth:3001
|
|
STORAGE_SERVICE_URL: http://storage:3002
|
|
depends_on:
|
|
auth: { condition: service_healthy }
|
|
storage: { condition: service_healthy }
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3000/health || exit 1"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 5
|
|
start_period: 15s
|
|
|
|
volumes:
|
|
postgres_data:
|
|
redis_data:
|