services: app: build: context: . dockerfile: Dockerfile image: ${APP_NAME:-ai-coding-starter}:latest container_name: ${APP_NAME:-ai-coding-starter} restart: unless-stopped env_file: - .env environment: NODE_ENV: production PORT: 3000 ports: # Bound to loopback: the reverse proxy on the host forwards to this. # Set APP_BIND=0.0.0.0 in .env only if the port must be publicly reachable. - "${APP_BIND:-127.0.0.1}:${APP_PORT:-3000}:3000" healthcheck: test: - CMD - node - -e - "fetch('http://127.0.0.1:3000/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" interval: 30s timeout: 5s start_period: 20s retries: 3 logging: driver: json-file options: max-size: "10m" max-file: "3" # A database is deliberately not part of this template — /architecture picks one # per project. # # RULE FOR EVERY SERVICE ADDED BELOW: only `app` gets a `ports:` entry. Databases, # caches and queues get none — the app reaches them by service name over this # compose network (`db:5432`). Publishing 5432 puts the database on the public # internet, and Docker's iptables rules bypass ufw, so the server only *looks* # firewalled. See .claude/rules/security.md. # # db: # image: postgres:17-alpine # restart: unless-stopped # environment: # POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env} # POSTGRES_DB: ${POSTGRES_DB:-app} # expose: # - "5432" # documentation only — NOT reachable from the host # volumes: # - db-data:/var/lib/postgresql/data # healthcheck: # test: ["CMD-SHELL", "pg_isready -U postgres"] # interval: 10s # timeout: 5s # retries: 5 # # Then make the app wait for it: # app: # depends_on: # db: # condition: service_healthy # # volumes: # db-data: