From efc6e7770da3c3523b612790ac01d85c2f74d9e0 Mon Sep 17 00:00:00 2001 From: Rob Martin Date: Mon, 28 Sep 2026 15:29:11 -0600 Subject: [PATCH 1/2] perf(docker): drop the C toolchain from the runtime image The base stage installed gcc, g++, libffi-dev and python3-dev to build C extensions, and they stayed in both the full and lite images: 290 MB installed. Nothing is compiled any more. cffi, gevent, greenlet and zope-interface all ship cp314 manylinux wheels for amd64 and arm64, and the sdist-only packages in uv.lock are pure Python. python3-dev also pulled Debian's libpython3.13 into a Python 3.14 image. --- Dockerfile | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 1b2395a4..5fff948d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -57,11 +57,11 @@ ENV FLASK_PORT=8084 # Configure locale, timezone, and perform initial cleanup in a single layer RUN apt-get update && \ apt-get install -y --no-install-recommends \ - # For building C-extensions (cffi, gevent, etc.) - gcc \ - g++ \ - libffi-dev \ - python3-dev \ + # No C toolchain: every compiled dependency (cffi, gevent, greenlet, + # zope-interface) ships a cp314 manylinux wheel for amd64 and arm64, and + # the sdist-only ones are pure Python. The toolchain was 290MB of the + # runtime image, and python3-dev pulled Debian's libpython3.13 into a 3.14 + # image. If a dependency ever needs compiling, build it in a separate stage. # For locale locales tzdata \ # For healthcheck From 21d748ed996e8fc9f666b5e30fb3b5ec3503a695 Mon Sep 17 00:00:00 2001 From: Michael Ngo Date: Mon, 28 Sep 2026 16:16:03 -0600 Subject: [PATCH 2/2] build: stop shipping build-only content in the runtime image `COPY . .` put the whole build context into /app, including tests/, docs/ and developer-tool caches. .dockerignore now excludes them. It applies to every stage, so src/ can't be listed there (frontend-builder needs it); the final stages remove it after the built dist is copied. data/ stays, since languages.py reads data/book-languages.json at runtime, and genDebug.sh stays because the final stage chmod +x's it. Also drops the venv's pip, which uv seeded alongside the system pip. Nothing installs at runtime. setuptools stays: deps still import pkg_resources. Extracted from DrNgo/shelfmark-fork@075caff and re-applied onto upstream v1.4.0 by Rob Martin, without that commit's ffmpeg and Mesa/LLVM removals. --- .dockerignore | 19 +++++++++++++++++++ Dockerfile | 17 +++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/.dockerignore b/.dockerignore index df06f328..8491d13e 100644 --- a/.dockerignore +++ b/.dockerignore @@ -48,3 +48,22 @@ src/frontend/.vite/ templates/ static/css/ static/js/ + +# Developer-only trees. `COPY . .` puts the whole context into /app, so anything +# left here ships to production: the CodeGraph index alone was 38MB of the image. +# NOTE: this file applies to EVERY stage, so `src/` cannot be listed — the +# frontend-builder stage needs it. The final stage drops it after the copy. +# Deliberately NOT listed: `data/` (languages.py:19 reads +# data/book-languages.json at runtime) and `genDebug.sh` (the final stage +# chmod +x's it, so excluding it fails the build). +.codegraph/ +.ruff_cache/ +.claude/ +tests/ +docs/ +scripts/ +compose/ +downloaded_files/ +frontend-dist/ +SESSION_STATE.md +Makefile diff --git a/Dockerfile b/Dockerfile index 5fff948d..cb01304d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -209,6 +209,13 @@ RUN --mount=type=cache,target=/root/.cache/uv \ uv pip install --python /app/.venv/bin/python --reinstall python-xlib==0.33 && \ /app/.venv/bin/python -c "import Xlib.X; assert hasattr(Xlib.X, 'FamilyServerInterpreted'), 'Xlib.X.FamilyServerInterpreted missing after fix'; print('Xlib namespace OK:', Xlib.__version__)" +# The venv's own pip goes too. uv seeded a copy into /app/.venv and nothing +# installs at runtime. setuptools deliberately STAYS: several deps still import +# pkg_resources on the hot path. +RUN rm -rf /app/.venv/bin/pip /app/.venv/bin/pip3 /app/.venv/bin/pip3.* \ + /app/.venv/lib/python*/site-packages/pip \ + /app/.venv/lib/python*/site-packages/pip-*.dist-info + # Keep SeleniumBase's bundled driver cache writable for the fixed non-root user. RUN SELENIUMBASE_DRIVERS_DIR=$(/app/.venv/bin/python -c "import pathlib, seleniumbase; print(pathlib.Path(seleniumbase.__file__).resolve().parent / 'drivers')") && \ chown -R 1000:1000 "${SELENIUMBASE_DRIVERS_DIR}" && \ @@ -224,6 +231,11 @@ COPY . . COPY --from=frontend-builder /frontend/dist /app/frontend-dist +# The frontend SOURCE cannot be excluded via .dockerignore — that file applies +# to every stage and frontend-builder needs src/. Only the built dist (copied +# above) is served at runtime, so drop the source here. +RUN rm -rf /app/src + # Image-owned runtime paths for the fixed non-root user. Root/PUID mode still # re-homes ownership at startup when needed. RUN mkdir -p \ @@ -262,6 +274,11 @@ COPY . . COPY --from=frontend-builder /frontend/dist /app/frontend-dist +# The frontend SOURCE cannot be excluded via .dockerignore — that file applies +# to every stage and frontend-builder needs src/. Only the built dist (copied +# above) is served at runtime, so drop the source here. +RUN rm -rf /app/src + # Image-owned runtime paths for the fixed non-root user. Root/PUID mode still # re-homes ownership at startup when needed. RUN mkdir -p \