{"id":"l-dk-2-1","courseId":"docker-kubernetes","lang":"en","kind":"lesson","summary":"Split the notes-api Dockerfile into build, dependency and runtime stages so the shipped image holds only compiled code and production dependencies, and keep junk out of the build context with .dockerignore.","html":"<p>The image from section 1 ships TypeScript, the type definitions, your test runner, your <code dir=\"ltr\">src/</code> folder, and every other dev dependency to production. None of it runs. All of it is attack surface, pull time and scanner noise. The fix is to build in one place and ship from another.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">One Dockerfile, several stages</h2>\n<p>Each <code dir=\"ltr\">FROM</code> starts a new <strong>stage</strong> with a fresh filesystem. Name stages with <code dir=\"ltr\">AS</code>, and copy files between them with <code dir=\"ltr\">COPY --from</code>. Only the last stage becomes the image you tag.</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>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\"># 1. Compile TypeScript (needs dev dependencies)</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 style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> build</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\"> --mount=type=cache,target=/root/.npm npm ci</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">COPY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> tsconfig.json ./</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">COPY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> src ./src</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:#6E7781;--shiki-dark:#8B949E\"># 2. Production dependencies only</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 style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> deps</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\"> --mount=type=cache,target=/root/.npm npm ci --omit=dev</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\"># 3. What actually ships</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 style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> runtime</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">ENV</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> NODE_ENV=production</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 ./</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">COPY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> --from=deps /app/node_modules ./node_modules</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">COPY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> --from=build /app/dist ./dist</span></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>The <code dir=\"ltr\">runtime</code> stage gets exactly three things: <code dir=\"ltr\">package.json</code>, production <code dir=\"ltr\">node_modules</code>, and compiled <code dir=\"ltr\">dist/</code>. TypeScript, <code dir=\"ltr\">src/</code> and the npm cache stay behind in stages nobody ships.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 700 260\" role=\"img\" aria-labelledby=\"l-dk-2-1-t1\"><defs><marker id=\"arrow-l-dk-2-1-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-2-1-t1\">The build stage compiles src to dist with all dependencies. The deps stage installs production dependencies only. The runtime stage copies dist from build and node_modules from deps.</title>\n  <rect class=\"d-box-accent\" x=\"20\" y=\"30\" width=\"250\" height=\"90\" rx=\"10\"/>\n  <text class=\"d-label-strong\" x=\"145\" y=\"56\" text-anchor=\"middle\">build</text>\n  <text class=\"d-code\" x=\"145\" y=\"80\" text-anchor=\"middle\">npm ci  (all deps)</text>\n  <text class=\"d-code\" x=\"145\" y=\"102\" text-anchor=\"middle\">tsc: src → dist</text>\n  <rect class=\"d-box-accent\" x=\"20\" y=\"150\" width=\"250\" height=\"80\" rx=\"10\"/>\n  <text class=\"d-label-strong\" x=\"145\" y=\"178\" text-anchor=\"middle\">deps</text>\n  <text class=\"d-code\" x=\"145\" y=\"204\" text-anchor=\"middle\">npm ci --omit=dev</text>\n  <rect class=\"d-box-success\" x=\"430\" y=\"70\" width=\"250\" height=\"130\" rx=\"10\"/>\n  <text class=\"d-label-strong\" x=\"555\" y=\"98\" text-anchor=\"middle\">runtime (shipped)</text>\n  <text class=\"d-code\" x=\"555\" y=\"126\" text-anchor=\"middle\">package.json</text>\n  <text class=\"d-code\" x=\"555\" y=\"150\" text-anchor=\"middle\">node_modules (prod)</text>\n  <text class=\"d-code\" x=\"555\" y=\"174\" text-anchor=\"middle\">dist/</text>\n  <path class=\"d-arrow\" d=\"M270 80 L428 140\" marker-end=\"url(#arrow-l-dk-2-1-1)\"/>\n  <text class=\"d-label-muted\" x=\"350\" y=\"92\" text-anchor=\"middle\">dist/</text>\n  <path class=\"d-arrow\" d=\"M270 190 L428 160\" marker-end=\"url(#arrow-l-dk-2-1-1)\"/>\n  <text class=\"d-label-muted\" x=\"350\" y=\"200\" text-anchor=\"middle\">node_modules</text>\n  <text class=\"d-label-muted\" x=\"145\" y=\"252\" text-anchor=\"middle\">left behind: TypeScript, src/, npm cache</text>\n</svg><figcaption>Stages build in parallel; the runtime stage copies only what it needs</figcaption></figure>\n<p>BuildKit works out the dependency graph between stages. <code dir=\"ltr\">build</code> and <code dir=\"ltr\">deps</code> don’t depend on each other, so they run in parallel, and a stage the target doesn’t need is skipped entirely.</p>\n<p>On notes-api this took the image down by roughly a third, and the vulnerability scan you’ll run later in this section went from dozens of findings in dev tooling to a handful in the runtime. The next lesson shrinks the base itself.</p>\n<p>Why copy <code dir=\"ltr\">package.json</code> into the runtime at all? notes-api’s <code dir=\"ltr\">package.json</code> contains <code dir=\"ltr\">&quot;type&quot;: &quot;module&quot;</code>, which tells Node to treat <code dir=\"ltr\">.js</code> files as ES modules. Leave it out and the container fails on start with <code dir=\"ltr\">SyntaxError: Cannot use import statement outside a module</code>, which looks like a build bug but is a missing file. If your project uses CommonJS you could skip it, but it costs nothing and keeps <code dir=\"ltr\">npm</code> metadata available.</p>\n<p>You may see a different pattern: one stage that runs <code dir=\"ltr\">npm ci</code>, builds, then runs <code dir=\"ltr\">npm prune --omit=dev</code> before copying <code dir=\"ltr\">node_modules</code> out. It works, and it saves one install. I prefer the separate <code dir=\"ltr\">deps</code> stage because it caches on its own: bumping a dev-only tool like a linter doesn’t invalidate the production dependency layer, so that layer is reused across many builds and the runtime image changes less often.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Using intermediate stages on purpose</h2>\n<p>Stages are also handy targets. Run the test suite in the environment that has dev dependencies, without touching the shipped image:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"1\"><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\"> build</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --target</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> build</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> -t</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> notes-api:test</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> .</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">docker</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> run</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --rm</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> notes-api:test</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> npm</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> test</span></span></code></pre></div>\n<p><code dir=\"ltr\">--target build</code> stops at the <code dir=\"ltr\">build</code> stage. CI can run this before building the runtime image, and both builds share the cache.</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>Copying the whole build stage</span></p>\n<p><code dir=\"ltr\">COPY --from=build /app ./</code> is the most common multi-stage mistake I see in review. It looks tidy and it ships everything you tried to leave behind: dev dependencies, <code dir=\"ltr\">src/</code>, test fixtures, sometimes a <code dir=\"ltr\">.env</code> file. Copy named paths, never a whole stage’s working directory.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">.dockerignore: control what the builder sees</h2>\n<p>When you run <code dir=\"ltr\">docker build .</code>, the whole directory is sent to the builder as the context, minus anything matched by <code dir=\"ltr\">.dockerignore</code>. Without one, your local <code dir=\"ltr\">node_modules</code> (hundreds of megabytes, possibly with macOS-compiled native modules), the <code dir=\"ltr\">.git</code> folder and any <code dir=\"ltr\">.env</code> file with real credentials all travel to the builder. Any <code dir=\"ltr\">COPY . .</code> puts them in a layer.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"text\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">.dockerignore</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>node_modules\ndist\ncoverage\n.git\n.env\n.env.*\n*.log\nDockerfile\ncompose*.yaml</code></pre></div>\n<p>Three wins at once: the context drops from hundreds of megabytes to a few hundred kilobytes, edits to ignored files no longer bust the cache, and secrets sitting in your working tree can’t be copied into an image by accident.</p>\n<aside class=\"callout callout-why\" 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\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><circle cx=\"12\" cy=\"12\" r=\"6\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/></svg><span><span class=\"callout-kind\">Why this matters</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>The .env that went public</span></p>\n<p>A team I worked with pushed an image to a public registry with <code dir=\"ltr\">COPY . .</code> and no <code dir=\"ltr\">.dockerignore</code>. Their <code dir=\"ltr\">.env</code> held a production database password. The image was public for about six hours. Rotating the credential took a day of careful work. A ten-line <code dir=\"ltr\">.dockerignore</code> would have prevented it.</p>\n</aside>\n<p>The syntax is like <code dir=\"ltr\">.gitignore</code>: one pattern per line, <code dir=\"ltr\">*</code> and <code dir=\"ltr\">**</code> wildcards, and <code dir=\"ltr\">!</code> to re-include something. Keep it in the root of the build context. If you need one ignored file after all, re-include it below the broad rule, for example <code dir=\"ltr\">.env.*</code> followed by <code dir=\"ltr\">!.env.example</code>, so a template file can still be copied while real ones can’t.</p>\n<p>Check what the builder actually received when in doubt: <code dir=\"ltr\">docker build --progress=plain .</code> prints the size of the transferred context on its first lines. If that number is in megabytes for a small API, something is missing from <code dir=\"ltr\">.dockerignore</code>.</p>\n<p>Next you’ll swap the base image for a smaller one and stop running as root.</p>\n","toc":[{"id":"sec-1","text":"One Dockerfile, several stages","level":2},{"id":"sec-2","text":"Using intermediate stages on purpose","level":2},{"id":"sec-3","text":".dockerignore: control what the builder sees","level":2}],"takeaways":["A multi-stage build uses several <code dir=\"ltr\">FROM</code> stages; only the last stage (or the one you <code dir=\"ltr\">--target</code>) becomes the image.","<code dir=\"ltr\">COPY --from=&lt;stage&gt;</code> pulls specific files out of an earlier stage, so compilers and dev dependencies never reach production.","Install production dependencies with <code dir=\"ltr\">npm ci --omit=dev</code> in their own stage and copy only <code dir=\"ltr\">node_modules</code> and <code dir=\"ltr\">dist</code> into the runtime stage.","A <code dir=\"ltr\">.dockerignore</code> keeps <code dir=\"ltr\">node_modules</code>, <code dir=\"ltr\">.git</code>, <code dir=\"ltr\">.env</code> files and build output out of the context, making builds faster and safer."],"further":[{"title":"Multi-stage builds","url":"https://docs.docker.com/build/building/multi-stage/"},{"title":"Build context and .dockerignore","url":"https://docs.docker.com/build/concepts/context/"},{"title":"Dockerfile reference: COPY --from","url":"https://docs.docker.com/reference/dockerfile/#copy---from"}],"quiz":[{"q":"<p>In a Dockerfile with stages <code dir=\"ltr\">build</code>, <code dir=\"ltr\">deps</code> and <code dir=\"ltr\">runtime</code> (in that order), what does <code dir=\"ltr\">docker build -t notes-api:1.0.0 .</code> produce?</p>","options":[{"text":"Three images, one per stage, all tagged <code dir=\"ltr\">notes-api:1.0.0</code>.","why":"A build produces one image. Earlier stages are intermediate and only exist in the build cache."},{"text":"An image made from the last stage, <code dir=\"ltr\">runtime</code>, containing only what that stage copied in.","why":"Correct. The final stage is the default target. Anything not copied into it is left behind."},{"text":"An image made from the first stage, since that’s where the build happens.","why":"The first stage is the default only when it’s the only one. With several stages the last one wins unless you pass <code dir=\"ltr\">--target</code>."},{"text":"One image containing the layers of all three stages stacked together.","why":"Stages don’t stack. Each <code dir=\"ltr\">FROM</code> starts a fresh filesystem; only explicit <code dir=\"ltr\">COPY --from</code> moves files across."}],"answer":1},{"q":"<p>Why install production dependencies in a separate <code dir=\"ltr\">deps</code> stage instead of pruning them in the <code dir=\"ltr\">build</code> stage?</p>","options":[{"text":"<code dir=\"ltr\">npm ci --omit=dev</code> only works in a stage named <code dir=\"ltr\">deps</code>.","why":"Stage names are arbitrary labels. The flag works in any stage."},{"text":"Separate stages are always smaller than one stage.","why":"Stage count alone doesn’t change size. What matters is what you copy into the final stage."},{"text":"Build stages can’t run npm scripts.","why":"Any stage can run any command; the build stage runs <code dir=\"ltr\">npm run build</code> itself."},{"text":"The build stage needs dev dependencies like TypeScript, while the runtime only needs production ones; a clean <code dir=\"ltr\">--omit=dev</code> install gives exactly that set.","why":"Correct. It also caches independently, so changing a dev-only dependency doesn’t invalidate the production install."}],"answer":3},{"q":"<p>Your build context is 1.2 GB and every build starts with “transferring context” for 40 seconds. What’s the most likely fix?</p>","options":[{"text":"Add a <code dir=\"ltr\">.dockerignore</code> that excludes <code dir=\"ltr\">node_modules</code>, <code dir=\"ltr\">.git</code> and build output.","why":"Correct. Those folders are usually most of the context, and none of them should be sent to the builder."},{"text":"Use <code dir=\"ltr\">COPY src ./src</code> instead of <code dir=\"ltr\">COPY . .</code>.","why":"Narrower copies help the cache and image contents, but the whole context is still sent unless you ignore files."},{"text":"Move the Dockerfile into a subdirectory.","why":"The context is whatever directory you pass, regardless of where the Dockerfile lives."},{"text":"Add <code dir=\"ltr\">--no-cache</code> to skip context transfer.","why":"<code dir=\"ltr\">--no-cache</code> disables layer reuse. The context is still transferred."}],"answer":0},{"q":"<p>How do you run notes-api’s unit tests inside the image’s build environment without shipping test tooling?</p>","options":[{"text":"Add <code dir=\"ltr\">RUN npm test</code> to the runtime stage.","why":"The runtime stage has no dev dependencies, so the test runner isn’t there; and you’d want tests out of the shipped image anyway."},{"text":"Copy the test files into the runtime stage and run them with <code dir=\"ltr\">docker exec</code>.","why":"That ships tests and their tooling to production, the exact thing multi-stage builds avoid."},{"text":"Build only the <code dir=\"ltr\">build</code> stage with <code dir=\"ltr\">--target build</code> and run the tests in a container from it.","why":"Correct. <code dir=\"ltr\">--target</code> stops at the named stage, which has dev dependencies and source, and nothing from it leaks into the runtime image."},{"text":"Use a second Dockerfile that copies the runtime image.","why":"The runtime image deliberately lacks dev dependencies, so tests can’t run there."}],"answer":2}],"exercise":{"kind":"spot-bug","language":"dockerfile","code":"# syntax=docker/dockerfile:1\nFROM node:24-slim AS build\nWORKDIR /app\nCOPY package.json package-lock.json ./\nRUN npm ci\nCOPY . .\nRUN npm run build\n\nFROM node:24-slim AS runtime\nENV NODE_ENV=production\nWORKDIR /app\nCOPY --from=build /app ./\nEXPOSE 3000\nCMD [\"node\", \"dist/server.js\"]","bugLines":[12],"title":"The multi-stage build that ships everything","prompt":"<p>This two-stage Dockerfile was meant to keep TypeScript and dev dependencies out of production. The image is barely smaller than before and the scanner still reports CVEs in test tooling. Select the line that defeats the purpose of the second stage.</p>\n","hints":["Look at what the runtime stage copies from the build stage.","The build stage’s <code dir=\"ltr\">/app</code> contains <code dir=\"ltr\">src/</code>, <code dir=\"ltr\">dist/</code> and a <code dir=\"ltr\">node_modules</code> with every dev dependency."],"explanation":"<p>Line 12 copies the build stage’s entire <code dir=\"ltr\">/app</code> into the runtime image, so <code dir=\"ltr\">src/</code>, dev dependencies and anything else the context contained all ship. Add a production-only dependency stage and copy named paths:</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 style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> deps</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 --omit=dev</span></span>\n<span class=\"line\"></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 style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> runtime</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">ENV</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> NODE_ENV=production</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 ./</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">COPY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> --from=deps /app/node_modules ./node_modules</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">COPY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> --from=build /app/dist ./dist</span></span></code></pre></div>\n","checkLabels":{}},"words":705,"readMinutes":3}