Skip to content

Harden Dockerfile for production: pin, freeze, drop runtime uv#98

Open
brandonros wants to merge 3 commits into
1rgs:mainfrom
brandonros:main
Open

Harden Dockerfile for production: pin, freeze, drop runtime uv#98
brandonros wants to merge 3 commits into
1rgs:mainfrom
brandonros:main

Conversation

@brandonros
Copy link
Copy Markdown

The previous Dockerfile was a dev setup masquerading as production:

  • FROM python:latest meant rebuilds drifted silently over time.
  • uv sync --locked + uv run at CMD meant every container start re-validated the lockfile against the venv, which can touch the package index (at minimum DNS) even when the venv is already built.
  • --reload is uvicorn's dev mode; it watches the filesystem and respawns workers, each of which re-invokes the CMD and compounds the above.

This rewrite:

  • Pins the base image (python:3.12-slim) and uv (0.4.30) so builds are reproducible.
  • Uses a multi-stage build so the runtime image ships neither uv nor pip caches.
  • Switches --locked -> --frozen so lockfile drift fails the build loudly instead of being silently reconciled.
  • Splits the dep-install layer from the source-copy layer so code changes don't bust the dependency cache.
  • Drops uv run from CMD and invokes uvicorn directly from /app/.venv/bin via PATH. Runtime startup now makes zero PyPI callouts and needs no DNS for package management.
  • Removes --reload.

brandonros and others added 3 commits April 21, 2026 22:43
The previous Dockerfile was a dev setup masquerading as production:

- `FROM python:latest` meant rebuilds drifted silently over time.
- `uv sync --locked` + `uv run` at CMD meant every container start
  re-validated the lockfile against the venv, which can touch the
  package index (at minimum DNS) even when the venv is already built.
- `--reload` is uvicorn's dev mode; it watches the filesystem and
  respawns workers, each of which re-invokes the CMD and compounds
  the above.

This rewrite:

- Pins the base image (python:3.12-slim) and uv (0.4.30) so builds
  are reproducible.
- Uses a multi-stage build so the runtime image ships neither uv
  nor pip caches.
- Switches `--locked` -> `--frozen` so lockfile drift fails the build
  loudly instead of being silently reconciled.
- Splits the dep-install layer from the source-copy layer so code
  changes don't bust the dependency cache.
- Drops `uv run` from CMD and invokes uvicorn directly from
  /app/.venv/bin via PATH. Runtime startup now makes zero PyPI
  callouts and needs no DNS for package management.
- Removes `--reload`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Without these env vars, `uv sync` downloads its own CPython into
/root/.local/share/uv/python/ and symlinks the venv's interpreter
to it. In a multi-stage build we only COPY /app across, so that
interpreter directory is absent from the runtime image and the
venv's python symlink dangles -- exec fails at container start with
"No such file or directory" on /app/.venv/bin/uvicorn.

Silent version drift too: uv picked 3.10 despite the 3.12-slim base.

UV_PYTHON_PREFERENCE=only-system + UV_PYTHON_DOWNLOADS=never force
uv to use /usr/local/bin/python3.12 from the base image, which is
present in both stages.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The host pins Python 3.10 via .python-version for local dev. Keeping
the container on the same version avoids subtle behavior differences
between developer machines and production (stdlib changes, dep
wheel selection, etc.) and means `uv sync` respects the existing
pin inside the build context without extra workarounds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant