infra(image): a .dockerignore that is read, 69 MB nobody reads, and bytecode in the layer - #11211
Merged
Conversation
…69 MB nobody reads `api/.dockerignore` was never consulted: Docker reads the ignore file from the build context, and both api/cloudbuild.yaml and .github/workflows/ci-image.yml build with `.`. The file proved it itself — it excluded `*.md` while the builder's `COPY … README.md` kept succeeding. Every build uploaded ~230 MB. The replacement is a root allowlist. A denylist that misses a new directory only makes the context quietly fatter; an allowlist that misses one fails at the COPY line. Narrowing the context to `api/` is not available: the image needs `core/` and the lock files, which live above it. `COPY plots/` leaves the runtime stage — 16.6 MB of every pulled image for a directory nothing under api/ or core/ opens; the implementations come from Postgres, as ci-image.yml already notes. `UV_COMPILE_BYTECODE=1` plus `compileall` over api and core take ~1.8 s off every cold start (import api.main: 3.56–4.43 s cold vs 1.79–2.26 s with .pyc), at 68 MB more compressed venv layer. uv is pinned, and UV_PYTHON names the interpreter so uv cannot download a managed CPython the runtime stage lacks. With the uv pin, hadolint's DL3013 exception is obsolete; DL3008 and DL3025 move from the workflow's file-wide `ignore:` onto the instructions they excuse, so a new occurrence elsewhere is caught rather than swallowed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UEScQMZFvxxNNyNJYryfa3
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UEScQMZFvxxNNyNJYryfa3
Contributor
There was a problem hiding this comment.
🔵 Needs a closer look
It changes production image composition and makes an operational image-size versus cold-start tradeoff requiring human review.
Pull request overview
Optimizes the API container build context and runtime image.
Changes:
- Adds a root allowlist-based
.dockerignore. - Removes unused plot assets and precompiles Python bytecode.
- Pins uv and scopes Hadolint exceptions to individual instructions.
File summaries
| File | Description |
|---|---|
.dockerignore |
Defines the API build-context allowlist. |
api/.dockerignore |
Removes the ineffective nested ignore file. |
api/Dockerfile |
Optimizes image contents and startup behavior. |
.github/workflows/ci-image.yml |
Moves Hadolint exceptions into the Dockerfile. |
CHANGELOG.md |
Documents the image-build changes. |
Review details
- Files reviewed: 5/5 changed files
- Comments generated: 2
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Copilot: python:3.13-slim is a mutable tag and the apt packages are deliberately unpinned, so uv was never the image's only unpinned input — only the unpinned link in the chain uv.lock otherwise fixes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UEScQMZFvxxNNyNJYryfa3
… the image Copilot: this PR removed the COPY, and the comment explaining why plots/** is not a build trigger still described it as copied. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UEScQMZFvxxNNyNJYryfa3
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three findings from the 2026-09-02/03 sibling-transfer audit, all in the API image build.
1.
api/.dockerignorewas never readDocker reads the ignore file from the build context, and the context is the repo root —
api/cloudbuild.yamlbuilds-f api/Dockerfile .,ci-image.ymlusescontext: .. Soapi/.dockerignorehas been decorative since it was written.The proof is inside the file: it excludes
*.md, and the builder'sCOPY pyproject.toml uv.lock README.md ./has succeeded on every build.Measured cost (clean checkout of
origin/mainplus the.gita Cloud Build/Actions checkout brings):.gitFix: a root
.dockerignore, written as an allowlist (api,core,pyproject.toml,uv.lock,README.md). A denylist that misses a new directory only makes the context quietly fatter; an allowlist that misses one fails at the COPY line — the loud failure. Narrowing the context toapi/instead is not available: the image needscore/and the lock files, which live above it.app/is unaffected — the frontend build's context isapp, soapp/.dockerignorestill governs it.2.
COPY plots/ ./plots/is dead weightNothing under
api/orcore/opens a path belowplots/— the implementations this API serves come from Postgres (core/database).ci-image.ymlalready says so where it explains whyplots/**is not a build trigger.Measured on the
latestmanifest in Artifact Registry (2026-09-03):plots/layer (compressed)plots/unpackedtar cf - plots \| gzipgives 16.57 MB, which is how layer 10 was identified.3. No bytecode in the image
The venv shipped source only, and the
.dockerignoreexcludes__pycache__, so the COPYs brought none either: every cold start recompiled the whole import graph.Measured on this exact dependency set (a throwaway venv from this
pyproject.toml+uv.lock,import api.main, three runs each):PYTHONDONTWRITEBYTECODE=1, empty cache prefix)~1.8 s off every cold start. It is not free, and the number is worth stating plainly:
uv sync --frozenUV_COMPILE_BYTECODE=1 uv sync --frozen(142.25 MB reproduces layer 7 of the live manifest at 142.71 MB, which is what validates the whole measurement.)
Net on the image: 215.30 → ~266.8 MB compressed — 16.6 MB out for
plots/, 68.0 MB in for.pyc. The trade is one bigger pull per node against ~1.8 s on every container's first request; Cloud Run streams layers and caches them per node, so the repeated cost is the import, not the pull. Say the word and item 3 comes back out — items 1 and 2 stand on their own.Also here:
uvis pinned — the unpinned link in the dependency chainuv.lockotherwise fixes, not a claim that the image is reproducible (python:3.13-slimis a mutable tag and the apt packages stay deliberately unversioned) — andUV_PYTHONnames the interpreter, so uv can never quietly download a managed CPython that the runtime stage does not have at the same path.4. Hadolint exceptions move to the line they excuse
ci-image.ymlcarriedignore: DL3013,DL3008,DL3025file-wide and said in its own comment that this was the wrong home. Withpip install uv==0.10.9, DL3013 is simply gone. The other two are now# hadolint ignore=<code>comments at theapt-getand theHEALTHCHECKinstruction, so a new DL3008 or DL3025 elsewhere in the file blocks instead of being swallowed. The workflow keepsfailure-threshold: warningand nothing else changed in it.Verified locally against hadolint 2.15.1 — the version
hadolint-action@v3.5.0pins:Verification
ci-image.ymlbuilds this image and smokes the container on this PR — that is the loop for a Dockerfile change, and it is what proves the allowlist leaves the build everything it copies. No Docker locally, hence the layer figures come from the Artifact Registry manifest and the venv figures from a reproduceduv sync.Finding for a follow-up (not in this PR)
api/andcore/import none ofpandas,scipy,scikit-learn,statsmodelsoranthropic, andmatplotlibonly lazily inside twocore/palette.pyfunctions — yet all of them are unconditional[project] dependenciesand so sit in the API image. Of the 11,227.pycfilesUV_COMPILE_BYTECODE=1writes,import api.maintouches 1,149 (25 MB). Moving the plot-runtime libraries into an extra the image does not install would dwarf both numbers in this PR. That is a dependency-graph change with its own blast radius, so it is filed here rather than done.🤖 Generated with Claude Code
https://claude.ai/code/session_01UEScQMZFvxxNNyNJYryfa3