{"id":"l-dk-1-3","courseId":"docker-kubernetes","lang":"en","kind":"lesson","summary":"Order Dockerfile instructions so dependency installs stay cached, read the build cache from <code dir=\"ltr\">docker history</code> and build output, and keep caches warm with cache mounts and CI cache exports.","html":"<p>A Skylane team once asked me why their CI builds had crept from forty seconds to eight minutes. Their Dockerfile looked like the one you wrote last lesson: <code dir=\"ltr\">COPY . .</code> and then <code dir=\"ltr\">npm ci</code>. Every commit changed some file, so every build reinstalled 900 packages from the internet. One reordering brought it back to forty seconds.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Layers and cache keys</h2>\n<p>Each <code dir=\"ltr\">FROM</code>, <code dir=\"ltr\">COPY</code>, <code dir=\"ltr\">ADD</code> and <code dir=\"ltr\">RUN</code> produces a layer. When you rebuild, BuildKit walks the Dockerfile top to bottom and asks of each step: “have I run exactly this before, with exactly these inputs?”</p>\n<ul>\n<li>For <code dir=\"ltr\">RUN</code>, the key is the command text plus the layer it runs on.</li>\n<li>For <code dir=\"ltr\">COPY</code> and <code dir=\"ltr\">ADD</code>, the key includes a checksum of the files being copied.</li>\n</ul>\n<p>The first step whose key doesn’t match is a <strong>cache miss</strong>, and from then on <em>every</em> later step rebuilds, because each one runs on top of a layer that’s now different. Cache hits are a prefix of the file. Order is everything.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 290\" role=\"img\" aria-labelledby=\"l-dk-1-3-t1\"><defs><marker id=\"arrow-l-dk-1-3-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-dk-1-3-t1\">Two layer stacks. With COPY . . before npm ci, a source edit misses at COPY and reruns npm ci. With package files copied first, npm ci stays cached and only the final COPY and build rerun.</title>\n  <text class=\"d-label-strong\" x=\"165\" y=\"22\" text-anchor=\"middle\">COPY . . first</text>\n  <text class=\"d-label-strong\" x=\"505\" y=\"22\" text-anchor=\"middle\">package files first</text>\n  <rect class=\"d-box-success\" x=\"40\" y=\"236\" width=\"250\" height=\"36\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"165\" y=\"259\" text-anchor=\"middle\">FROM node:24-slim</text>\n  <rect class=\"d-box-success\" x=\"40\" y=\"194\" width=\"250\" height=\"36\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"165\" y=\"217\" text-anchor=\"middle\">WORKDIR /app</text>\n  <rect class=\"d-box-warn\" x=\"40\" y=\"152\" width=\"250\" height=\"36\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"165\" y=\"175\" text-anchor=\"middle\">COPY . .   (src changed)</text>\n  <rect class=\"d-box-warn\" x=\"40\" y=\"110\" width=\"250\" height=\"36\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"165\" y=\"133\" text-anchor=\"middle\">RUN npm ci   (~70 s)</text>\n  <rect class=\"d-box-warn\" x=\"40\" y=\"68\" width=\"250\" height=\"36\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"165\" y=\"91\" text-anchor=\"middle\">RUN npm run build</text>\n  <rect class=\"d-box-success\" x=\"380\" y=\"236\" width=\"250\" height=\"36\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"505\" y=\"259\" text-anchor=\"middle\">FROM node:24-slim</text>\n  <rect class=\"d-box-success\" x=\"380\" y=\"194\" width=\"250\" height=\"36\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"505\" y=\"217\" text-anchor=\"middle\">COPY package*.json</text>\n  <rect class=\"d-box-success\" x=\"380\" y=\"152\" width=\"250\" height=\"36\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"505\" y=\"175\" text-anchor=\"middle\">RUN npm ci   (cached)</text>\n  <rect class=\"d-box-warn\" x=\"380\" y=\"110\" width=\"250\" height=\"36\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"505\" y=\"133\" text-anchor=\"middle\">COPY . .   (src changed)</text>\n  <rect class=\"d-box-warn\" x=\"380\" y=\"68\" width=\"250\" height=\"36\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"505\" y=\"91\" text-anchor=\"middle\">RUN npm run build</text>\n  <rect class=\"d-box-success\" x=\"200\" y=\"34\" width=\"14\" height=\"14\" rx=\"3\"/>\n  <text class=\"d-label-muted\" x=\"220\" y=\"46\">cache hit</text>\n  <rect class=\"d-box-warn\" x=\"320\" y=\"34\" width=\"14\" height=\"14\" rx=\"3\"/>\n  <text class=\"d-label-muted\" x=\"340\" y=\"46\">rebuilt</text>\n</svg><figcaption>One changed file invalidates its layer and every layer above it</figcaption></figure>\n<h2 id=\"sec-2\" tabindex=\"-1\">Reorder for the cache</h2>\n<p>Dependencies change weekly; source changes on every commit. So copy the dependency manifests on their own, install, and only then copy the rest:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"dockerfile\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">Dockerfile</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\"># syntax=docker/dockerfile:1</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">FROM</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> node:24-slim</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">WORKDIR</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> /app</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\"># Changes rarely: only when dependencies change</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">COPY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> package.json package-lock.json ./</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">RUN</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> npm ci</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\"># Changes on every commit</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">COPY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> . .</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">RUN</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> npm run build</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">EXPOSE</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> 3000</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">CMD</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"node\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"dist/server.js\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]</span></span></code></pre></div>\n<p>Edit <code dir=\"ltr\">src/server.ts</code> and rebuild. The output shows which steps were reused:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"text\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">Text</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code> =&gt; CACHED [2/6] WORKDIR /app\n =&gt; CACHED [3/6] COPY package.json package-lock.json ./\n =&gt; CACHED [4/6] RUN npm ci\n =&gt; [5/6] COPY . .\n =&gt; [6/6] RUN npm run build</code></pre></div>\n<p>The general rule: <strong>sort instructions from least to most frequently changing.</strong> System packages, then language dependencies, then your code, then anything derived from your code.</p>\n<p><code dir=\"ltr\">docker history notes-api:dev</code> lists the layers with their size and the instruction that created them. Use it when an image is bigger than you expect; the culprit is usually one fat layer.</p>\n<p>The same rule applies to build arguments. A line like <code dir=\"ltr\">ARG GIT_SHA</code> near the top, used in a later <code dir=\"ltr\">RUN</code> or <code dir=\"ltr\">LABEL</code>, gives every <code dir=\"ltr\">RUN</code> step below it a new input on every commit, because build arguments are visible to every later <code dir=\"ltr\">RUN</code> as environment variables. Declare such values as late as possible, right before the one step that uses them.</p>\n<p><code dir=\"ltr\">COPY</code> keys on file <em>contents</em>, not timestamps, so a fresh <code dir=\"ltr\">git clone</code> on a build server hits the same cache as your laptop as long as the bytes match. That’s also why a committed, up-to-date <code dir=\"ltr\">package-lock.json</code> matters: it is the input that decides whether the expensive install is reused.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">System packages: one RUN, cleaned up</h2>\n<p>If you need OS packages, install them in a single <code dir=\"ltr\">RUN</code>:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"dockerfile\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">Dockerfile</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">RUN</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> apt-get update \\</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> &#x26;&#x26; apt-get install -y --no-install-recommends ca-certificates \\</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> &#x26;&#x26; rm -rf /var/lib/apt/lists/*</span></span></code></pre></div>\n<p><code dir=\"ltr\">--no-install-recommends</code> skips optional extras, and deleting the package lists in the <em>same</em> step keeps them out of the layer. Deleting them in a later <code dir=\"ltr\">RUN</code> doesn’t shrink anything: the earlier layer still contains them.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Splitting apt-get update from apt-get install</span></p>\n<p>With <code dir=\"ltr\">RUN apt-get update</code> on its own line, that layer is cached by its text, which never changes. Months later you add a package to the install line; the install reruns against the months-old cached index and fails with “Unable to locate package” or installs an outdated version. Always chain them.</p>\n</aside>\n<h2 id=\"sec-4\" tabindex=\"-1\">Cache mounts: faster misses</h2>\n<p>Sometimes the install <em>must</em> rerun because you added a dependency. A cache mount keeps npm’s download cache in the builder between builds, without putting it in the image:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"dockerfile\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">Dockerfile</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">COPY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> package.json package-lock.json ./</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">RUN</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> --mount=type=cache,target=/root/.npm npm ci</span></span></code></pre></div>\n<p><code dir=\"ltr\">npm ci</code> still deletes and recreates <code dir=\"ltr\">node_modules</code>, but the tarballs come from local disk instead of the registry. On notes-api that turns a 70-second reinstall into about 15.</p>\n<h2 id=\"sec-5\" tabindex=\"-1\">Caches in CI</h2>\n<p>Your laptop keeps its cache between builds. Most CI runners start empty, so every build is cold however well you order the file. Export the cache somewhere durable and import it on the next run:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"4\"><div class=\"code-head\"><span class=\"code-title\">Terminal</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">docker</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> buildx</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> build</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> \\</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  --cache-from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> type=registry,ref=ghcr.io/skylane/notes-api:buildcache</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> \\</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  --cache-to</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> type=registry,ref=ghcr.io/skylane/notes-api:buildcache,mode=max</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> \\</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  -t</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> ghcr.io/skylane/notes-api:dev</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> .</span></span></code></pre></div>\n<p><code dir=\"ltr\">mode=max</code> stores intermediate layers too, which matters once you have several build stages. GitHub Actions users can use <code dir=\"ltr\">type=gha</code> instead of a registry.</p>\n<aside class=\"callout callout-tip\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">Tip</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Measure, don’t guess</span></p>\n<p>Run the build twice with <code dir=\"ltr\">--progress=plain</code> and compare. If a step you expected to be <code dir=\"ltr\">CACHED</code> isn’t, look at the instruction right above it: that’s the one whose inputs changed.</p>\n</aside>\n<p>Builds are fast now. Next you’ll get comfortable with the running side: ports, environment variables, logs, and what really happens when a container stops.</p>\n","toc":[{"id":"sec-1","text":"Layers and cache keys","level":2},{"id":"sec-2","text":"Reorder for the cache","level":2},{"id":"sec-3","text":"System packages: one RUN, cleaned up","level":2},{"id":"sec-4","text":"Cache mounts: faster misses","level":2},{"id":"sec-5","text":"Caches in CI","level":2}],"takeaways":["Each instruction’s cache key depends on the instruction and, for <code dir=\"ltr\">COPY</code>, on the checksum of the files copied; one miss rebuilds every later step.","Copy <code dir=\"ltr\">package.json</code> and <code dir=\"ltr\">package-lock.json</code> first, run <code dir=\"ltr\">npm ci</code>, then copy the source, so code edits don’t reinstall dependencies.","Put instructions that change rarely near the top and instructions that change on every commit near the bottom.","Run <code dir=\"ltr\">apt-get update</code> and <code dir=\"ltr\">apt-get install</code> in the same <code dir=\"ltr\">RUN</code> so the package index is never stale.","A cache mount (<code dir=\"ltr\">RUN --mount=type=cache</code>) keeps npm’s download cache between builds even when the layer itself must rebuild."],"further":[{"title":"Docker build cache","url":"https://docs.docker.com/build/cache/"},{"title":"Optimize cache usage in builds","url":"https://docs.docker.com/build/cache/optimize/"},{"title":"Cache storage backends","url":"https://docs.docker.com/build/cache/backends/"}],"quiz":[{"q":"<p>In this Dockerfile you edit only <code dir=\"ltr\">src/routes.ts</code> and rebuild. Which steps run again?</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"dockerfile\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">Dockerfile</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">FROM</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> node:24-slim</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">WORKDIR</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> /app</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">COPY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> package.json package-lock.json ./</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">RUN</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> npm ci</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">COPY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> . .</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">RUN</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> npm run build</span></span></code></pre></div>\n","options":[{"text":"Every step, because the build context changed.","why":"The context changing doesn’t invalidate everything. Each step is checked on its own until the first miss."},{"text":"Only <code dir=\"ltr\">RUN npm run build</code>, because that’s the step that compiles TypeScript.","why":"<code dir=\"ltr\">COPY . .</code> copies <code dir=\"ltr\">src/routes.ts</code>, so its checksum changes and it misses first; the build step then reruns because it comes after a miss."},{"text":"<code dir=\"ltr\">RUN npm ci</code> and everything after it.","why":"<code dir=\"ltr\">npm ci</code> sits after a <code dir=\"ltr\">COPY</code> of only the two package files, which didn’t change, so it’s still a cache hit."},{"text":"<code dir=\"ltr\">COPY . .</code> and <code dir=\"ltr\">RUN npm run build</code>; the dependency install stays cached.","why":"Correct. The first changed input is at <code dir=\"ltr\">COPY . .</code>, so it and every later step rerun. The expensive install above it is reused."}],"answer":3},{"q":"<p>Why does <code dir=\"ltr\">RUN apt-get update</code> on its own line, followed by <code dir=\"ltr\">RUN apt-get install -y curl</code>, cause trouble months later?</p>","options":[{"text":"Docker refuses to cache <code dir=\"ltr\">apt-get</code> commands.","why":"Docker caches <code dir=\"ltr\">RUN</code> steps by their text, apt or not. That’s exactly the problem here."},{"text":"The <code dir=\"ltr\">update</code> layer stays cached with an old package index, so a later change to the install line fetches stale or missing versions.","why":"Correct. The cache key is the command text, which never changes, so the index is from the day the cache was created. Combine them in one <code dir=\"ltr\">RUN</code>."},{"text":"Two <code dir=\"ltr\">RUN</code> instructions double the image size.","why":"Extra layers add a little metadata, not double the size. The real issue is a stale cached index."},{"text":"<code dir=\"ltr\">apt-get update</code> needs root and the second <code dir=\"ltr\">RUN</code> drops privileges.","why":"Both run as the same user unless a <code dir=\"ltr\">USER</code> instruction comes between them."}],"answer":1},{"q":"<p>Your CI runner starts with an empty Docker cache on every job, so every build is cold. What helps most?</p>","options":[{"text":"Add more <code dir=\"ltr\">RUN</code> instructions so there are more layers to cache.","why":"More layers don’t help when there’s no cache to read from in the first place."},{"text":"Switch from <code dir=\"ltr\">npm ci</code> to <code dir=\"ltr\">npm install</code> because it’s faster.","why":"<code dir=\"ltr\">npm install</code> may rewrite the lockfile and doesn’t fix the empty cache. Keep <code dir=\"ltr\">npm ci</code> for reproducibility."},{"text":"Export and import the build cache, for example with <code dir=\"ltr\">--cache-to</code> and <code dir=\"ltr\">--cache-from</code> pointing at a registry.","why":"Correct. Cache backends let fresh runners reuse layers built by earlier jobs."},{"text":"Use <code dir=\"ltr\">docker build --no-cache</code> so the result is predictable.","why":"That throws away caching entirely, which is the opposite of what you want for speed."}],"answer":2},{"q":"<p>What does <code dir=\"ltr\">RUN --mount=type=cache,target=/root/.npm npm ci</code> give you that layer caching doesn’t?</p>","options":[{"text":"npm’s download cache persists between builds, so even when this step must rerun, packages come from local disk instead of the network.","why":"Correct. A layer cache is all-or-nothing; a cache mount survives a layer miss and isn’t stored in the image."},{"text":"The <code dir=\"ltr\">node_modules</code> folder is mounted from your laptop into the image.","why":"Cache mounts live in the builder, not on your host, and they cache npm’s download store, not <code dir=\"ltr\">node_modules</code>."},{"text":"The installed packages are excluded from the final image to save space.","why":"<code dir=\"ltr\">node_modules</code> is still written into the layer. Only the mounted <code dir=\"ltr\">/root/.npm</code> directory stays out of the image."},{"text":"The step is never rerun, even if the lockfile changes.","why":"The step reruns whenever its inputs change. The mount only makes the rerun faster."}],"answer":0}],"exercise":{"kind":"spot-bug","language":"dockerfile","code":"# syntax=docker/dockerfile:1\nFROM node:24-slim\nRUN apt-get update\nRUN apt-get install -y --no-install-recommends ca-certificates\nWORKDIR /app\nCOPY . .\nRUN npm ci\nRUN npm run build\nEXPOSE 3000\nCMD [\"node\", \"dist/server.js\"]","bugLines":[3,6],"title":"Find the cache killers","prompt":"<p>CI builds for this Dockerfile take eight minutes even when a commit changes one line of TypeScript, and last week adding a second OS package failed with <code dir=\"ltr\">Unable to locate package</code>. Select the <strong>two</strong> lines responsible.</p>\n","hints":["Which line’s cache key changes on every commit, and what runs after it?","Which <code dir=\"ltr\">RUN</code> is cached by its text alone, so its result can be months old?"],"explanation":"<p>Line 6 copies the whole context before <code dir=\"ltr\">npm ci</code>, so any source change misses the cache there and reinstalls every dependency. Copy only the manifests first, install, then copy the rest.</p>\n<p>Line 3 caches <code dir=\"ltr\">apt-get update</code> on its own; when the install line changes, it runs against an index from the day the cache was made. Chain update and install in one <code dir=\"ltr\">RUN</code>:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"dockerfile\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">Dockerfile</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">RUN</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> apt-get update \\</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> &#x26;&#x26; apt-get install -y --no-install-recommends ca-certificates \\</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> &#x26;&#x26; rm -rf /var/lib/apt/lists/*</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">WORKDIR</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> /app</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">COPY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> package.json package-lock.json ./</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">RUN</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> npm ci</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">COPY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> . .</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">RUN</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> npm run build</span></span></code></pre></div>\n","checkLabels":{}},"words":678,"readMinutes":3}