From 3eebeb8af914d283c8f956b3f1930153cf89f77b Mon Sep 17 00:00:00 2001 From: Sebastian Mendel Date: Wed, 15 Jul 2026 14:20:33 +0200 Subject: [PATCH 1/2] docs: add multi-stage caching reference New reference multi-stage-caching.md: dev/CI images that layer apt/xdebug/ chromium/npm-ci FROM the code stage re-run all of it on every source change (COPY . . invalidates everything downstream). Fix by re-parenting the tooling into a sibling stage FROM base and pulling the built tree via one COPY --from=deps. Covers lineage isolation (xdebug/chromium out of prod/ profiling), same-layer node_modules cleanup, and verifying the cache with a content change (not touch). Not indexed in SKILL.md's References list because that file is at its 500-word cap; suggest indexing when word budget allows. Signed-off-by: Sebastian Mendel --- .../references/multi-stage-caching.md | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 skills/docker-development/references/multi-stage-caching.md diff --git a/skills/docker-development/references/multi-stage-caching.md b/skills/docker-development/references/multi-stage-caching.md new file mode 100644 index 0000000..c6f54f2 --- /dev/null +++ b/skills/docker-development/references/multi-stage-caching.md @@ -0,0 +1,22 @@ +# Multi-stage caching: keep code-independent installs off the code-copy lineage + +**Symptom:** a dev/CI image rebuilds heavy tooling (apt, pecl/xdebug, browser installs, `npm ci`) on **every** source change, even though none of it depends on the code. + +**Cause:** the tooling stage is `FROM `, and the code stage ends with `COPY . .`. Docker invalidates every layer downstream of that copy on any tracked-file content change — so the tooling, layered on top, re-runs each time. + +**Fix — re-parent, don't reorder.** Put the code-independent installs in a *sibling* stage `FROM base` (a stage with **no** code copy), then pull the built tree into the leaf via one `COPY --from=`: + +```dockerfile +FROM base AS deps # composer/npm install + COPY . . + build (code-dependent) +FROM base AS devtools # apt, xdebug, symfony-cli, npm ci, chromium — NO code copy +FROM devtools AS dev +COPY --from=deps --chown=app:app /app /app # the ONE code-dependent layer of dev +``` + +A source-only edit now invalidates `deps` (and dev's copy) but leaves the whole `devtools` lineage CACHED. + +## Guardrails + +- **Isolation by lineage:** keep xdebug/chromium in the `devtools → dev → e2e` branch only. A `production`/`profiling`/`tools` stage that is `FROM base`/`FROM deps` must not inherit `devtools`, or it gains xdebug (skews profiling timings) and browser bloat. Verify with the actual `FROM` chain, not by hoping. +- **Same-layer cleanup:** a transient `node_modules` needed only to run `npx playwright install` should be `rm -rf`'d in the *same* `RUN` (the leaf's `COPY --from=deps` overwrites it anyway) so it never bloats the layer. The browser binary lives in `~/.cache/ms-playwright`, outside `node_modules`, so it survives. +- **Verify the cache, not just the build:** `docker build --check` + a cold build prove it *builds*; only a **content** change to a source file + rebuild proves the *caching* — BuildKit hashes content, not mtime, so a bare `touch` won't invalidate. Look for `CACHED` on the tooling layers. From 0019aeb7859aa7161fa77760d31e4576091e3b16 Mon Sep 17 00:00:00 2001 From: Sebastian Mendel Date: Wed, 15 Jul 2026 14:33:18 +0200 Subject: [PATCH 2/2] docs: Dockerfile comments on their own lines (review) Inline # after FROM/COPY is not parsed as a comment by the Dockerfile parser. Signed-off-by: Sebastian Mendel --- .../docker-development/references/multi-stage-caching.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/skills/docker-development/references/multi-stage-caching.md b/skills/docker-development/references/multi-stage-caching.md index c6f54f2..42db687 100644 --- a/skills/docker-development/references/multi-stage-caching.md +++ b/skills/docker-development/references/multi-stage-caching.md @@ -7,10 +7,13 @@ **Fix — re-parent, don't reorder.** Put the code-independent installs in a *sibling* stage `FROM base` (a stage with **no** code copy), then pull the built tree into the leaf via one `COPY --from=`: ```dockerfile -FROM base AS deps # composer/npm install + COPY . . + build (code-dependent) -FROM base AS devtools # apt, xdebug, symfony-cli, npm ci, chromium — NO code copy +# composer/npm install + COPY . . + build (code-dependent) +FROM base AS deps +# apt, xdebug, symfony-cli, npm ci, chromium — NO code copy +FROM base AS devtools FROM devtools AS dev -COPY --from=deps --chown=app:app /app /app # the ONE code-dependent layer of dev +# the ONE code-dependent layer of dev +COPY --from=deps --chown=app:app /app /app ``` A source-only edit now invalidates `deps` (and dev's copy) but leaves the whole `devtools` lineage CACHED.