# ============================================================================ # Overlay: run PostgreSQL alongside the API. # # docker compose -f docker-compose.yml -f docker-compose.local-db.yml up -d # # FOR STAGING AND FOR TESTING THE STACK ON ONE HOST. Not for production data. # A database in a container on the same host as its application has no backup, # no point-in-time recovery, no failover, and dies with the host. The base file # assumes a managed PostgreSQL for exactly that reason. # # ── TLS ───────────────────────────────────────────────────────────────────── # # PostgreSQL is started with SSL on, using a self-signed certificate generated # on first boot into the data volume. That is enough for DATABASE_SSLMODE= # require, which encrypts the connection but does not verify who is on the # other end — appropriate for a private compose network, NOT a substitute for # verify-full against a real CA. # # It also means APP_ENV=production works here without weakening the check in # internal/config, which is the point: the guard stays honest and the local # stack meets it rather than dodging it. # # Set these in .env to match: # DATABASE_HOST=postgres # DATABASE_SSLMODE=require # DATABASE_URL=postgres://krow:@postgres:5432/krow?sslmode=require # ============================================================================ services: postgres: image: postgres:16-alpine container_name: krow-postgres environment: POSTGRES_DB: ${DATABASE_NAME:-krow} POSTGRES_USER: ${DATABASE_USER:-krow} POSTGRES_PASSWORD: ${DATABASE_PASSWORD:?DATABASE_PASSWORD is required} # scram-sha-256 rather than the image's md5 default. md5 has been # deprecated upstream for years and is trivial to crack from a captured # handshake. POSTGRES_INITDB_ARGS: "--auth-host=scram-sha-256 --auth-local=scram-sha-256" # Generate a self-signed certificate on first boot, then hand off to the # image's own entrypoint with SSL enabled. The key must be 0600 and owned # by the postgres user or the server refuses to start — that check is the # reason this is done here rather than in a bind mount, where host # ownership would leak in. command: - sh - -c - | set -e CERT=/var/lib/postgresql/data/server.crt KEY=/var/lib/postgresql/data/server.key if [ ! -f "$$CERT" ]; then mkdir -p /var/lib/postgresql/data openssl req -new -x509 -days 3650 -nodes -text \ -out "$$CERT" -keyout "$$KEY" -subj "/CN=postgres" 2>/dev/null chmod 0600 "$$KEY" chown postgres:postgres "$$CERT" "$$KEY" fi exec docker-entrypoint.sh postgres \ -c ssl=on -c ssl_cert_file="$$CERT" -c ssl_key_file="$$KEY" volumes: - postgres-data:/var/lib/postgresql/data healthcheck: # -U and -d so this reports on the application's database, not on # whatever `postgres` happens to be reachable. test: ["CMD-SHELL", "pg_isready -U ${DATABASE_USER:-krow} -d ${DATABASE_NAME:-krow}"] interval: 10s timeout: 5s retries: 10 start_period: 30s ports: # Loopback only. Publishing 5432 to the world is how a database ends up # in someone else's botnet. - "${POSTGRES_BIND:-127.0.0.1}:${POSTGRES_PORT:-5432}:5432" restart: unless-stopped stop_grace_period: 60s security_opt: - no-new-privileges:true deploy: resources: limits: memory: ${POSTGRES_MEMORY_LIMIT:-1G} logging: driver: json-file options: max-size: "10m" max-file: "5" networks: [krow] # The migration runner now has to wait for the database to accept # connections, which it does not need to do when the database is managed and # already up. migrate: depends_on: postgres: condition: service_healthy volumes: postgres-data: driver: local