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:
There is no separate 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

Targets the real /health endpoint (apps/api/routers/health.py) — not an illustrative placeholder path.

Building locally

The Postgres image (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.

VPS deployment

Kubernetes deployment