Files
notify-bridge/Dockerfile
T
alexei.dolgolyov 592e1b6114 perf(docker): split external deps into a cacheable layer, swap pip for uv
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.
2026-04-23 20:52:36 +03:00

110 lines
3.6 KiB
Docker

# syntax=docker/dockerfile:1.7
# =============================================================================
# Stage 1: Build frontend (SvelteKit static output)
# =============================================================================
FROM node:22-alpine AS frontend-build
WORKDIR /build
# Cache npm install layer
COPY frontend/package.json frontend/package-lock.json ./
RUN npm ci
# Build static site
COPY frontend/ ./
RUN npm run build
# =============================================================================
# Stage 2: Build Python wheels + extract external dependency list
# =============================================================================
FROM python:3.12-slim AS python-build
WORKDIR /build
RUN pip install --no-cache-dir build
# Build core package wheel
COPY packages/core/ packages/core/
RUN python -m build packages/core/ --wheel --outdir /wheels
# Build server package wheel
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 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/
# Create non-root user and data directory
RUN useradd --create-home --shell /bin/bash appuser \
&& mkdir -p /data \
&& chown appuser:appuser /data
# Environment defaults
ENV NOTIFY_BRIDGE_DATA_DIR=/data \
NOTIFY_BRIDGE_STATIC_DIR=/app/static \
NOTIFY_BRIDGE_DEBUG=false
VOLUME /data
EXPOSE 8420
USER appuser
HEALTHCHECK --interval=30s --timeout=5s --retries=3 --start-period=10s \
CMD python -c "import os, urllib.request; urllib.request.urlopen(f'http://localhost:{os.environ.get(\"NOTIFY_BRIDGE_PORT\", 8420)}/api/health')"
CMD ["notify-bridge"]