Fix the image build: fetch the git dependency in its own stage
The build died at `uv sync`: django-lucide is our fork, declared as a git source and pinned by the lock to a commit, so uv shells out to `git` to fetch it — and python:3.14-slim has no git. Installing git in the runtime image would have fixed it and left a build tool, plus its dependency tree, in production for the sake of one package that is already vendored into the venv by then. So the virtualenv is now built in a stage that has git, and the finished .venv is copied into a runtime stage that does not. Same base image, so the compiled wheels inside it stay ABI compatible. Also drops the second `uv sync`, which installed the project itself: there is no [build-system] and rosterchief is not a package — gunicorn imports it from the working directory, exactly as it does locally. Unverified end to end: still no container runtime on this machine. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -67,6 +67,22 @@ docker compose run --rm web python manage.py check --deploy
|
||||
**off** in code, because defaulting them to `not DEBUG` would redirect every test request to
|
||||
https and break the suite anywhere `DEBUG` is unset.
|
||||
|
||||
### One dependency comes from git
|
||||
|
||||
`django-lucide` is our fork (`[tool.uv.sources]` in `pyproject.toml`, pinned by `uv.lock` to a
|
||||
commit), so **uv shells out to `git`** to fetch it. `python:*-slim` has no git, which is why
|
||||
the image builds the virtualenv in a **separate stage** that installs git, and copies the
|
||||
finished `.venv` into a runtime stage that does not have it — a build tool has no business in
|
||||
a production image.
|
||||
|
||||
Two consequences worth knowing:
|
||||
|
||||
- The build needs **network access to GitHub**, and the fork must stay reachable. If that ever
|
||||
becomes awkward (a private runner, an air-gapped build), publish the fork to a private index
|
||||
or vendor the wheel, and the git stage disappears.
|
||||
- `uv.lock` pins the exact commit, so the build is reproducible even though the source is a
|
||||
branch. Don't build with `--no-frozen`.
|
||||
|
||||
The first `docker compose up` will take a minute or two: Caddy is provisioning the wildcard
|
||||
certificate over DNS-01, and DNS propagation is not instant. Watch it with
|
||||
`docker compose logs -f caddy`.
|
||||
|
||||
39
Dockerfile
39
Dockerfile
@@ -15,7 +15,31 @@ COPY billing ./billing
|
||||
RUN npm run build
|
||||
|
||||
|
||||
# --- 2. the runtime ----------------------------------------------------------
|
||||
# --- 2. the virtualenv -------------------------------------------------------
|
||||
# Separate from the runtime for one reason: django-lucide is a *git* dependency (our lucide
|
||||
# fork), so uv shells out to git to fetch it. python:*-slim has no git, and installing it in
|
||||
# the runtime image would leave a build-time tool — plus its dependency tree — in production
|
||||
# for the sake of one package that is already vendored into the venv by then.
|
||||
FROM python:3.14-slim AS venv
|
||||
|
||||
RUN apt-get update && apt-get install --no-install-recommends -y git ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
||||
|
||||
ENV UV_COMPILE_BYTECODE=1 \
|
||||
UV_LINK_MODE=copy \
|
||||
UV_PYTHON_DOWNLOADS=never
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Dependencies first: they change far less often than the code, so this layer caches.
|
||||
COPY pyproject.toml uv.lock ./
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv sync --frozen --no-dev --no-install-project
|
||||
|
||||
|
||||
# --- 3. the runtime ----------------------------------------------------------
|
||||
FROM python:3.14-slim AS app
|
||||
|
||||
# WeasyPrint binds to these at import: no pango, no invoices. This is also why building the
|
||||
@@ -31,26 +55,19 @@ RUN apt-get update && apt-get install --no-install-recommends -y \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
||||
|
||||
ENV PYTHONUNBUFFERED=1 \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
UV_COMPILE_BYTECODE=1 \
|
||||
UV_LINK_MODE=copy \
|
||||
PATH="/app/.venv/bin:$PATH"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Dependencies first: they change far less often than the code, so this layer caches.
|
||||
COPY pyproject.toml uv.lock ./
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv sync --frozen --no-dev --no-install-project
|
||||
# The venv arrives fully built. Same base image, so the compiled wheels inside it are ABI
|
||||
# compatible; nothing is re-resolved here, and no git is needed to run what git fetched.
|
||||
COPY --from=venv /app/.venv ./.venv
|
||||
|
||||
COPY . .
|
||||
COPY --from=css /build/static/css/app.css ./static/css/app.css
|
||||
|
||||
RUN --mount=type=cache,target=/root/.cache/uv uv sync --frozen --no-dev
|
||||
|
||||
# collectstatic needs a settings module that imports: a throwaway key, never used at runtime.
|
||||
RUN DJANGO_SECRET_KEY=build-only-not-a-secret \
|
||||
DJANGO_STATICFILES_BACKEND=whitenoise.storage.CompressedManifestStaticFilesStorage \
|
||||
|
||||
Reference in New Issue
Block a user