The overlay had never been run against a fresh volume. Two faults, the first
hiding the second:
- postgres:16-alpine ships libssl but not the openssl CLI, so the first-boot
certificate generation exited 127 in a restart loop. It failed invisibly:
the 2>/dev/null on the openssl line swallowed sh's "not found" as well, so
`docker logs` was completely empty. openssl is now installed on the boot
that generates the certificate, inside the same guard, so a restart still
needs no network.
- the certificate was written into /var/lib/postgresql/data BEFORE initdb
ran, and initdb refuses to initialise a directory that is not empty. That
made a fresh volume unstartable regardless of the first fault. The
certificate now lives in its own volume, which keeps it persistent — the
reason it was put in the data directory — without touching the cluster's.
Separately, docker-compose.yml did not pass ANTHROPIC_API_KEY to the api
container, so a compose deployment could never register the agent run routes:
POST /agents/{id}/runs answered 404 and /version reported two endpoints fewer.
The model and embedder variables are now passed through, all defaulting to
empty so a deployment without them behaves exactly as it did.
Verified on a fresh volume: 56/56 verify-deploy checks against the resulting
stack, including a live agent run and 34 chunks embedded through Ollama.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJvibeSc1JYXjatankqM1g
114 lines
4.7 KiB
YAML
114 lines
4.7 KiB
YAML
# ============================================================================
|
|
# 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:<url-encoded-password>@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
|
|
# NOT in /var/lib/postgresql/data: initdb refuses to initialise a
|
|
# directory that is not empty, so writing the certificate there first
|
|
# means a fresh volume can never come up at all. Its own volume keeps
|
|
# the certificate persistent without touching the cluster's.
|
|
CERT=/var/lib/postgresql/certs/server.crt
|
|
KEY=/var/lib/postgresql/certs/server.key
|
|
if [ ! -f "$$CERT" ]; then
|
|
mkdir -p /var/lib/postgresql/certs
|
|
# postgres:16-alpine ships libssl but not the openssl CLI, so this
|
|
# has to be installed before it can be called. Inside the if, so it
|
|
# only happens on the boot that actually generates the certificate
|
|
# and a restart does not need the network.
|
|
command -v openssl >/dev/null || apk add --no-cache openssl
|
|
openssl req -new -x509 -days 3650 -nodes -text \
|
|
-out "$$CERT" -keyout "$$KEY" -subj "/CN=postgres"
|
|
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
|
|
- postgres-certs:/var/lib/postgresql/certs
|
|
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
|
|
postgres-certs:
|
|
driver: local
|