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/