One image, multiple roles
docker/api.Dockerfile builds a single image used for both the API
process and every Celery worker queue — only the container command
differs:
worker.Dockerfile to keep in sync — one build,
five deployable roles.
Two-stage build
The builder stage installs build tools (build-essential, libpq-dev)
and resolves dependencies with uv sync --locked — a stale lockfile fails
the build rather than silently re-resolving. The runtime stage copies only
the built virtualenv and application source; build tools never reach the
shipped image. The final image runs as a non-root user (fixed uid
10001), and the base image’s own bundled pip is uninstalled — it isn’t
needed at runtime and can’t carry a CVE if it isn’t there.
Secrets: env vars either way
The entrypoint (docker/entrypoint-api.sh) sources
/vault/secrets/config if present — a file the Vault Agent Injector
renders on Kubernetes — before execing the real process. If that file
doesn’t exist (any non-Kubernetes deployment, including Docker Compose),
the entrypoint is a no-op and the process simply reads whatever
environment variables were already set — by docker compose’s
environment:/env_file:, by your Vault AppRole integration, or by a
plain .env. The same image works both ways with no build-time branching.
Health checks
/health endpoint (apps/api/routers/health.py) — not
an illustrative placeholder path.
Building locally
docker/postgres.Dockerfile) is the upstream
pgvector/pgvector:pg16 image plus a Vault-aware entrypoint wrapper —
behavior is identical to the plain pgvector image local dev already uses,
so nothing changes except in-cluster secret sourcing.