From 5d42691a10ab18171e627f7b2d2ca07d8f704c1e Mon Sep 17 00:00:00 2001 From: Bernard Siebens Date: Tue, 14 Jul 2026 18:07:54 +0200 Subject: [PATCH] Fix the image build: fetch the git dependency in its own stage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- DEPLOYMENT.md | 16 ++++++++++++++++ Dockerfile | 39 ++++++++++++++++++++++++++++----------- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index e3db10b..6111b3a 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -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`. diff --git a/Dockerfile b/Dockerfile index 7a9f6e6..92283b8 100644 --- a/Dockerfile +++ b/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 \