592e1b6114
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.
110 lines
3.6 KiB
Docker
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"]
|