From 592e1b61141252f87e41195c5b5b8e433f8bd602 Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Thu, 23 Apr 2026 20:52:36 +0300 Subject: [PATCH] perf(docker): split external deps into a cacheable layer, swap pip for uv MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stage 3's `pip install /tmp/wheels/*.whl` re-resolved and re-downloaded ~50 transitive deps on every build, because the wheel filenames (and thus the layer hash) changed on every version bump. Two changes: 1. Emit /wheels/deps.txt in the build stage — external deps only, notify-bridge-* siblings filtered out. The runtime stage installs from this file first, so the cache key is the pyproject.toml deps (stable across releases) instead of the local wheel filenames (changes every release). Registry buildcache now serves the whole install layer on version-only bumps. 2. Swap pip for uv (ghcr.io/astral-sh/uv:0.11.7). uv resolves and downloads ~10x faster than pip; combined with a BuildKit cache mount on /root/.cache/uv and UV_COMPILE_BYTECODE=1, a cold install drops from ~60-90s to a few seconds. Local wheels are now installed with --no-deps since externals are already satisfied. --- Dockerfile | 52 ++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 45afa13..7c4d1a6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,4 @@ +# syntax=docker/dockerfile:1.7 # ============================================================================= # Stage 1: Build frontend (SvelteKit static output) # ============================================================================= @@ -14,7 +15,7 @@ COPY frontend/ ./ RUN npm run build # ============================================================================= -# Stage 2: Build Python wheels +# Stage 2: Build Python wheels + extract external dependency list # ============================================================================= FROM python:3.12-slim AS python-build @@ -30,16 +31,59 @@ RUN python -m build packages/core/ --wheel --outdir /wheels COPY packages/server/ packages/server/ RUN python -m build packages/server/ --wheel --outdir /wheels +# Emit /wheels/deps.txt with ONLY external (PyPI) deps — filter out +# notify-bridge-* siblings, which are installed from local wheels below. +# This file is the cache key for the external-deps install layer: as long as +# pyproject.toml dependency lines don't change, the runtime install layer is +# served from registry buildcache and no wheels are re-downloaded. +RUN python <<'PY' +import tomllib + +deps: list[str] = [] +for p in ("packages/core/pyproject.toml", "packages/server/pyproject.toml"): + with open(p, "rb") as f: + data = tomllib.load(f) + for d in data["project"].get("dependencies", []): + if not d.lstrip().lower().startswith("notify-bridge-"): + deps.append(d) + +seen: set[str] = set() +with open("/wheels/deps.txt", "w") as f: + for d in deps: + if d not in seen: + seen.add(d) + f.write(d + "\n") +PY + # ============================================================================= # Stage 3: Runtime # ============================================================================= +FROM ghcr.io/astral-sh/uv:0.11.7 AS uv + FROM python:3.12-slim +# uv — fast pip replacement; pinned version, not :latest +COPY --from=uv /uv /uvx /bin/ + +ENV UV_COMPILE_BYTECODE=1 \ + UV_LINK_MODE=copy + WORKDIR /app -# Install wheels -COPY --from=python-build /wheels/ /tmp/wheels/ -RUN pip install --no-cache-dir /tmp/wheels/*.whl && rm -rf /tmp/wheels +# Install external deps first — layer cache key is deps.txt content, which +# only changes when pyproject.toml dependency lines change (not on version +# bumps). The cache mount persists downloaded wheels across local rebuilds; +# in CI, the registry buildcache serves the whole layer when unchanged. +COPY --from=python-build /wheels/deps.txt /tmp/deps.txt +RUN --mount=type=cache,target=/root/.cache/uv \ + uv pip install --system -r /tmp/deps.txt \ + && rm /tmp/deps.txt + +# Install local wheels without re-resolving — all external deps are present. +COPY --from=python-build /wheels/*.whl /tmp/wheels/ +RUN --mount=type=cache,target=/root/.cache/uv \ + uv pip install --system --no-deps /tmp/wheels/*.whl \ + && rm -rf /tmp/wheels # Copy frontend build COPY --from=frontend-build /build/build/ /app/static/