[{"data":1,"prerenderedAt":463},["ShallowReactive",2],{"mdc--ggg1hl-key":3,"mdc-lnybfc-key":36,"mdc-wl1zat-key":96,"mdc--bzyqza-key":110,"mdc--jt246f-key":122,"mdc-s1yk3u-key":180,"mdc--oi84qo-key":283,"mdc-cp9nct-key":293,"mdc-740i7z-key":305},{"data":4,"body":5},{},{"type":6,"children":7},"root",[8,25],{"type":9,"tag":10,"props":11,"children":12},"element","p",{},[13,16,23],{"type":14,"value":15},"text","Almost every image built by someone learning Docker ships the tools that built it. The compiler, the dev dependencies, the package manager's cache, sometimes the source and the ",{"type":9,"tag":17,"props":18,"children":20},"code",{"className":19},[],[21],{"type":14,"value":22},".git",{"type":14,"value":24}," directory. None of it runs in production; all of it is pulled on every deploy and scanned by every CVE tool.",{"type":9,"tag":10,"props":26,"children":27},{},[28,34],{"type":9,"tag":29,"props":30,"children":31},"strong",{},[32],{"type":14,"value":33},"Multi-stage builds",{"type":14,"value":35}," are the fix, and they are one keyword.",{"data":37,"body":38},{},{"type":6,"children":39},[40,53,66,86],{"type":9,"tag":41,"props":42,"children":44},"h2",{"id":43},"more-than-one-from",[45,47],{"type":14,"value":46},"More than one ",{"type":9,"tag":17,"props":48,"children":50},{"className":49},[],[51],{"type":14,"value":52},"FROM",{"type":9,"tag":54,"props":55,"children":61},"pre",{"className":56,"code":58,"language":59,"meta":60},[57],"language-dockerfile","# syntax=docker/dockerfile:1\n\nFROM golang:1.25-alpine AS build\nWORKDIR /src\nCOPY go.mod go.sum ./\nRUN go mod download\nCOPY . .\nRUN go build -o /out/server ./cmd/server\n\nFROM alpine:3.22\nRUN adduser -D -u 10001 app\nCOPY --from=build /out/server /usr/local/bin/server\nUSER app\nENTRYPOINT [\"server\"]\n","dockerfile","",[62],{"type":9,"tag":17,"props":63,"children":64},{"__ignoreMap":60},[65],{"type":14,"value":58},{"type":9,"tag":10,"props":67,"children":68},{},[69,71,76,78,84],{"type":14,"value":70},"Each ",{"type":9,"tag":17,"props":72,"children":74},{"className":73},[],[75],{"type":14,"value":52},{"type":14,"value":77}," starts a new stage with a clean filesystem. ",{"type":9,"tag":17,"props":79,"children":81},{"className":80},[],[82],{"type":14,"value":83},"COPY --from=build",{"type":14,"value":85}," reaches back into an earlier one and takes exactly what it names.",{"type":9,"tag":10,"props":87,"children":88},{},[89,94],{"type":9,"tag":29,"props":90,"children":91},{},[92],{"type":14,"value":93},"Only the last stage becomes the image.",{"type":14,"value":95}," The Go toolchain, the module cache, the source tree — all of it existed during the build and none of it is in the result.",{"data":97,"body":98},{},{"type":6,"children":99},[100,105],{"type":9,"tag":101,"props":102,"children":104},"terminal-teaser",{":lines":103},"[{\"cmd\":\"docker build -t api:single -f Dockerfile.single .\",\"out\":\"=> => naming to docker.io/library/api:single\"},{\"cmd\":\"docker build -t api:multi .\",\"out\":\"=> => naming to docker.io/library/api:multi\"},{\"cmd\":\"docker image ls api\",\"out\":\"IMAGE   TAG      SIZE\\napi     single   1.14GB\\napi     multi    16.8MB\"}]",[],{"type":9,"tag":10,"props":106,"children":107},{},[108],{"type":14,"value":109},"Two orders of magnitude, same binary. The single-stage image is mostly the Go toolchain.",{"data":111,"body":112},{},{"type":6,"children":113},[114],{"type":9,"tag":115,"props":116,"children":121},"quiz",{":answer":117,":options":118,"explanation":119,"question":120},"0","[\"It is discarded — only the final stage becomes the image, and only what you `COPY --from` survives\",\"It becomes the image's lower layers\",\"It is kept as a separate cached image and pushed alongside\"]","Stages are independent filesystems. The builder keeps earlier stages around locally for caching, but they are not part of the image, are not pushed, and are not pulled by anyone.","In a two-stage build, what happens to the first stage's filesystem?",[],{"data":123,"body":124},{},{"type":6,"children":125},[126,132,137,147,156,166,175],{"type":9,"tag":41,"props":127,"children":129},{"id":128},"the-pattern-per-ecosystem",[130],{"type":14,"value":131},"The pattern per ecosystem",{"type":9,"tag":10,"props":133,"children":134},{},[135],{"type":14,"value":136},"The shape is always the same — a fat stage that produces an artifact, a thin stage that runs it. What differs is what \"artifact\" means.",{"type":9,"tag":10,"props":138,"children":139},{},[140,145],{"type":9,"tag":29,"props":141,"children":142},{},[143],{"type":14,"value":144},"Node",{"type":14,"value":146}," — install dev dependencies, build, then install production dependencies fresh:",{"type":9,"tag":54,"props":148,"children":151},{"className":149,"code":150,"language":59,"meta":60},[57],"FROM node:22-alpine AS build\nWORKDIR /app\nCOPY package*.json ./\nRUN npm ci\nCOPY . .\nRUN npm run build\n\nFROM node:22-alpine\nWORKDIR /app\nCOPY package*.json ./\nRUN npm ci --omit=dev\nCOPY --from=build /app/dist ./dist\nUSER node\nCMD [\"node\", \"dist/server.js\"]\n",[152],{"type":9,"tag":17,"props":153,"children":154},{"__ignoreMap":60},[155],{"type":14,"value":150},{"type":9,"tag":10,"props":157,"children":158},{},[159,164],{"type":9,"tag":29,"props":160,"children":161},{},[162],{"type":14,"value":163},"Python",{"type":14,"value":165},", where the artifact is a virtualenv rather than a binary:",{"type":9,"tag":54,"props":167,"children":170},{"className":168,"code":169,"language":59,"meta":60},[57],"FROM python:3.14-slim AS build\nRUN python -m venv /opt/venv\nENV PATH=/opt/venv/bin:$PATH\nCOPY requirements.txt .\nRUN pip install --no-cache-dir -r requirements.txt\n\nFROM python:3.14-slim\nCOPY --from=build /opt/venv /opt/venv\nENV PATH=/opt/venv/bin:$PATH\nWORKDIR /app\nCOPY . .\nUSER 10001\nCMD [\"python\", \"-m\", \"app\"]\n",[171],{"type":9,"tag":17,"props":172,"children":173},{"__ignoreMap":60},[174],{"type":14,"value":169},{"type":9,"tag":10,"props":176,"children":177},{},[178],{"type":14,"value":179},"Copying a whole venv works because it is self-contained — as long as both stages use the same base and the same Python version, which is why they are pinned identically.",{"data":181,"body":182},{},{"type":6,"children":183},[184,190,202,211,268,273],{"type":9,"tag":41,"props":185,"children":187},{"id":186},"stages-are-a-graph-not-a-list",[188],{"type":14,"value":189},"Stages are a graph, not a list",{"type":9,"tag":10,"props":191,"children":192},{},[193,195,200],{"type":14,"value":194},"BuildKit does not run stages top to bottom. It builds a dependency graph and ",{"type":9,"tag":29,"props":196,"children":197},{},[198],{"type":14,"value":199},"runs independent stages in parallel",{"type":14,"value":201},", skipping any stage nothing depends on.",{"type":9,"tag":54,"props":203,"children":206},{"className":204,"code":205,"language":59,"meta":60},[57],"FROM node:22-alpine AS deps\nCOPY package*.json ./\nRUN npm ci\n\nFROM deps AS test\nCOPY . .\nRUN npm test\n\nFROM deps AS build\nCOPY . .\nRUN npm run build\n\nFROM nginx:alpine AS runtime\nCOPY --from=build /app/dist /usr/share/nginx/html\n",[207],{"type":9,"tag":17,"props":208,"children":209},{"__ignoreMap":60},[210],{"type":14,"value":205},{"type":9,"tag":10,"props":212,"children":213},{},[214,220,222,228,230,236,238,244,246,251,253,259,261,266],{"type":9,"tag":17,"props":215,"children":217},{"className":216},[],[218],{"type":14,"value":219},"test",{"type":14,"value":221}," and ",{"type":9,"tag":17,"props":223,"children":225},{"className":224},[],[226],{"type":14,"value":227},"build",{"type":14,"value":229}," both extend ",{"type":9,"tag":17,"props":231,"children":233},{"className":232},[],[234],{"type":14,"value":235},"deps",{"type":14,"value":237}," and neither depends on the other, so they run concurrently. And a plain ",{"type":9,"tag":17,"props":239,"children":241},{"className":240},[],[242],{"type":14,"value":243},"docker build .",{"type":14,"value":245}," ",{"type":9,"tag":29,"props":247,"children":248},{},[249],{"type":14,"value":250},"never runs the tests",{"type":14,"value":252}," — nothing in the ",{"type":9,"tag":17,"props":254,"children":256},{"className":255},[],[257],{"type":14,"value":258},"runtime",{"type":14,"value":260}," chain references ",{"type":9,"tag":17,"props":262,"children":264},{"className":263},[],[265],{"type":14,"value":219},{"type":14,"value":267},", so BuildKit prunes it.",{"type":9,"tag":10,"props":269,"children":270},{},[271],{"type":14,"value":272},"Which is a feature, not a bug. Ask for it explicitly when you want it:",{"type":9,"tag":54,"props":274,"children":278},{"className":275,"code":277,"language":14},[276],"language-text","docker build --target test .        # run the tests\ndocker build -t app:latest .        # ship the image, skip them\n",[279],{"type":9,"tag":17,"props":280,"children":281},{"__ignoreMap":60},[282],{"type":14,"value":277},{"data":284,"body":285},{},{"type":6,"children":286},[287],{"type":9,"tag":115,"props":288,"children":292},{":answer":117,":options":289,"explanation":290,"question":291},"[\"Nothing in the final stage's dependency chain references `test`, so BuildKit prunes it\",\"Tests are disabled in BuildKit by default\",\"The `test` stage needs an explicit `RUN --network` flag\"]","BuildKit builds the graph needed to produce the target and nothing else. Run it with `--target test`, or make it a real gate in CI as its own build step.","A Dockerfile has a `test` stage running `npm test`, but `docker build .` never runs it. Why?",[],{"data":294,"body":295},{},{"type":6,"children":296},[297],{"type":9,"tag":298,"props":299,"children":304},"fill-blank",{":answer":300,"hint":301,"placeholder":302,"prompt":303},"[\"docker build --target build -t app:build .\",\"docker build -t app:build --target build .\",\"docker buildx build --target build -t app:build .\"]","One flag names the stage to stop at.","docker build ...","Build only up to the stage named `build`, tagging the result `app:build`.",[],{"data":306,"body":307},{},{"type":6,"children":308},[309,458],{"type":9,"tag":310,"props":311,"children":313},"deep-dive",{"title":312},"`COPY --link`, and copying from an image you never built",[314,344,353,373,389,398,403,445],{"type":9,"tag":10,"props":315,"children":316},{},[317,326,328,334,336,342],{"type":9,"tag":29,"props":318,"children":319},{},[320],{"type":9,"tag":17,"props":321,"children":323},{"className":322},[],[324],{"type":14,"value":325},"COPY --link",{"type":14,"value":327}," changes how a copy is layered. Normally a ",{"type":9,"tag":17,"props":329,"children":331},{"className":330},[],[332],{"type":14,"value":333},"COPY",{"type":14,"value":335}," layer is written on top of the previous filesystem, so it depends on everything below it — change an earlier layer and the copy is redone. With ",{"type":9,"tag":17,"props":337,"children":339},{"className":338},[],[340],{"type":14,"value":341},"--link",{"type":14,"value":343},", the copied content becomes an independent layer, merged in later:",{"type":9,"tag":54,"props":345,"children":348},{"className":346,"code":347,"language":59,"meta":60},[57],"COPY --link --from=build /out/server /usr/local/bin/server\n",[349],{"type":9,"tag":17,"props":350,"children":351},{"__ignoreMap":60},[352],{"type":14,"value":347},{"type":9,"tag":10,"props":354,"children":355},{},[356,358,363,365,371],{"type":14,"value":357},"The copy no longer depends on the layers under it, so changing the base image does not invalidate it. On a big ",{"type":9,"tag":17,"props":359,"children":361},{"className":360},[],[362],{"type":14,"value":333},{"type":14,"value":364}," after a frequently-changing base, it is a large rebuild saving. The caveat: the destination is created fresh rather than merged into what was there, so it is not a drop-in replacement when you are copying ",{"type":9,"tag":366,"props":367,"children":368},"em",{},[369],{"type":14,"value":370},"into",{"type":14,"value":372}," an existing populated directory.",{"type":9,"tag":10,"props":374,"children":375},{},[376,387],{"type":9,"tag":29,"props":377,"children":378},{},[379,385],{"type":9,"tag":17,"props":380,"children":382},{"className":381},[],[383],{"type":14,"value":384},"COPY --from",{"type":14,"value":386}," also accepts an image name",{"type":14,"value":388},", not just a stage:",{"type":9,"tag":54,"props":390,"children":393},{"className":391,"code":392,"language":59,"meta":60},[57],"COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv\n",[394],{"type":9,"tag":17,"props":395,"children":396},{"__ignoreMap":60},[397],{"type":14,"value":392},{"type":9,"tag":10,"props":399,"children":400},{},[401],{"type":14,"value":402},"This is how you pull a single binary out of a published image without a stage of your own, and it is increasingly how tools ship — one static binary in a scratch image, designed to be copied out.",{"type":9,"tag":10,"props":404,"children":405},{},[406,411,413,419,421,427,429,435,437,443],{"type":9,"tag":29,"props":407,"children":408},{},[409],{"type":14,"value":410},"Two more worth knowing",{"type":14,"value":412},", both recent frontend additions: ",{"type":9,"tag":17,"props":414,"children":416},{"className":415},[],[417],{"type":14,"value":418},"COPY --parents",{"type":14,"value":420}," (frontend 1.20) preserves directory structure when copying with wildcards, so ",{"type":9,"tag":17,"props":422,"children":424},{"className":423},[],[425],{"type":14,"value":426},"COPY --parents src/**/*.json ./",{"type":14,"value":428}," keeps the paths instead of flattening them. And ",{"type":9,"tag":17,"props":430,"children":432},{"className":431},[],[433],{"type":14,"value":434},"COPY --exclude",{"type":14,"value":436}," (1.19) filters within a copy without editing ",{"type":9,"tag":17,"props":438,"children":440},{"className":439},[],[441],{"type":14,"value":442},".dockerignore",{"type":14,"value":444},".",{"type":9,"tag":10,"props":446,"children":447},{},[448,450,456],{"type":14,"value":449},"All four need the syntax line — ",{"type":9,"tag":17,"props":451,"children":453},{"className":452},[],[454],{"type":14,"value":455},"# syntax=docker/dockerfile:1",{"type":14,"value":457}," — which is the next lesson's subject.",{"type":9,"tag":10,"props":459,"children":460},{},[461],{"type":14,"value":462},"Next up: BuildKit and the modern Dockerfile — heredocs, cache mounts, and the features that first line unlocks.",1787908867673]