[{"data":1,"prerenderedAt":534},["ShallowReactive",2],{"mdc--m4ea77-key":3,"mdc-g1ilxv-key":29,"mdc--52ttkh-key":66,"mdc--f714w3-key":75,"mdc--crih5c-key":205,"mdc--78t84z-key":217,"mdc-439glc-key":308,"mdc-nfucmv-key":318,"mdc-h2ou17-key":390,"mdc--1rrxxx-key":402},{"data":4,"body":5},{},{"type":6,"children":7},"root",[8,24],{"type":9,"tag":10,"props":11,"children":12},"element","p",{},[13,16,22],{"type":14,"value":15},"text","Each instruction in a Dockerfile produces a ",{"type":9,"tag":17,"props":18,"children":19},"strong",{},[20],{"type":14,"value":21},"layer",{"type":14,"value":23}," — a record of what changed in the filesystem. An image is that stack of layers, and a container adds one thin writable layer on top.",{"type":9,"tag":10,"props":25,"children":26},{},[27],{"type":14,"value":28},"This one mechanism explains build speed, image size, and a whole category of \"why is my secret still in there\" security incidents.",{"data":30,"body":31},{},{"type":6,"children":32},[33,40,53],{"type":9,"tag":34,"props":35,"children":37},"h2",{"id":36},"layers-are-stacked-and-shared",[38],{"type":14,"value":39},"Layers are stacked and shared",{"type":9,"tag":41,"props":42,"children":46},"pre",{"className":43,"code":45,"language":14},[44],"language-text","myapp:1.0\n├── layer 5  COPY . .                 (2 MB)   your source\n├── layer 4  RUN npm ci --omit=dev    (48 MB)  dependencies\n├── layer 3  COPY package*.json ./    (4 KB)\n├── layer 2  WORKDIR /app             (0 B)\n└── layer 1  FROM node:22-alpine      (52 MB)  base\n",[47],{"type":9,"tag":48,"props":49,"children":51},"code",{"__ignoreMap":50},"",[52],{"type":14,"value":45},{"type":9,"tag":10,"props":54,"children":55},{},[56,58,64],{"type":14,"value":57},"Layers are content-addressed and shared. Ten images built ",{"type":9,"tag":48,"props":59,"children":61},{"className":60},[],[62],{"type":14,"value":63},"FROM node:22-alpine",{"type":14,"value":65}," store that base once. Pulling a new version of your app downloads only the layers that changed — which is why a well-ordered image redeploys in seconds and a badly-ordered one re-downloads everything.",{"data":67,"body":68},{},{"type":6,"children":69},[70],{"type":9,"tag":71,"props":72,"children":74},"terminal-teaser",{":lines":73},"[{\"cmd\":\"docker history myapp:1.0 --format '{{.Size}}\\\\t{{.CreatedBy}}'\",\"out\":\"2MB      COPY . . # buildkit\\n48MB     RUN npm ci --omit=dev # buildkit\\n4.1kB    COPY package*.json ./ # buildkit\\n0B       WORKDIR /app\\n52.3MB   FROM node:22-alpine\"}]",[],{"data":76,"body":77},{},{"type":6,"children":78},[79,85,97,102,113,118,127,132],{"type":9,"tag":34,"props":80,"children":82},{"id":81},"the-cache-and-what-invalidates-it",[83],{"type":14,"value":84},"The cache, and what invalidates it",{"type":9,"tag":10,"props":86,"children":87},{},[88,90,95],{"type":14,"value":89},"BuildKit reuses a layer if the instruction and its inputs are unchanged. ",{"type":9,"tag":17,"props":91,"children":92},{},[93],{"type":14,"value":94},"When one layer misses, every layer after it is rebuilt",{"type":14,"value":96}," — the stack is ordered, so nothing below a change can be trusted.",{"type":9,"tag":10,"props":98,"children":99},{},[100],{"type":14,"value":101},"Which makes instruction order a performance decision. Compare:",{"type":9,"tag":41,"props":103,"children":108},{"className":104,"code":106,"language":107,"meta":50},[105],"language-dockerfile","COPY . .                        # \u003C- changes on every commit\nRUN npm ci --omit=dev           # \u003C- therefore reinstalls every build\n","dockerfile",[109],{"type":9,"tag":48,"props":110,"children":111},{"__ignoreMap":50},[112],{"type":14,"value":106},{"type":9,"tag":10,"props":114,"children":115},{},[116],{"type":14,"value":117},"against:",{"type":9,"tag":41,"props":119,"children":122},{"className":120,"code":121,"language":107,"meta":50},[105],"COPY package*.json ./           # \u003C- changes only when deps change\nRUN npm ci --omit=dev           # \u003C- cached across source edits\nCOPY . .                        # \u003C- the cheap layer goes last\n",[123],{"type":9,"tag":48,"props":124,"children":125},{"__ignoreMap":50},[126],{"type":14,"value":121},{"type":9,"tag":10,"props":128,"children":129},{},[130],{"type":14,"value":131},"Same image, same result. The second rebuilds in two seconds where the first takes ninety.",{"type":9,"tag":10,"props":133,"children":134},{},[135,137,142,144,150,152,158,160,166,168,174,175,181,182,188,190,196,197,203],{"type":14,"value":136},"The principle generalises to every ecosystem: ",{"type":9,"tag":17,"props":138,"children":139},{},[140],{"type":14,"value":141},"copy the dependency manifest, install, then copy the source.",{"type":14,"value":143}," ",{"type":9,"tag":48,"props":145,"children":147},{"className":146},[],[148],{"type":14,"value":149},"requirements.txt",{"type":14,"value":151}," then ",{"type":9,"tag":48,"props":153,"children":155},{"className":154},[],[156],{"type":14,"value":157},"pip install",{"type":14,"value":159},", ",{"type":9,"tag":48,"props":161,"children":163},{"className":162},[],[164],{"type":14,"value":165},"go.mod",{"type":14,"value":167},"/",{"type":9,"tag":48,"props":169,"children":171},{"className":170},[],[172],{"type":14,"value":173},"go.sum",{"type":14,"value":151},{"type":9,"tag":48,"props":176,"children":178},{"className":177},[],[179],{"type":14,"value":180},"go mod download",{"type":14,"value":159},{"type":9,"tag":48,"props":183,"children":185},{"className":184},[],[186],{"type":14,"value":187},"Cargo.toml",{"type":14,"value":189}," then a warmup build, ",{"type":9,"tag":48,"props":191,"children":193},{"className":192},[],[194],{"type":14,"value":195},"pom.xml",{"type":14,"value":151},{"type":9,"tag":48,"props":198,"children":200},{"className":199},[],[201],{"type":14,"value":202},"mvn dependency:go-offline",{"type":14,"value":204},".",{"data":206,"body":207},{},{"type":6,"children":208},[209],{"type":9,"tag":210,"props":211,"children":216},"quiz",{":answer":212,":options":213,"explanation":214,"question":215},"0","[\"`COPY . .` runs before the install step, so any source change invalidates the install layer\",\"The build cache was cleared\",\"Comments count as dependency changes\"]","Cache invalidation cascades downward. Copying everything before installing means every commit busts the install layer. Copy the manifest first, install, then copy the rest.","You add a comment to one source file and your build reinstalls all dependencies. What is wrong?",[],{"data":218,"body":219},{},{"type":6,"children":220},[221,233,238,247,282,287,296],{"type":9,"tag":34,"props":222,"children":224},{"id":223},"run-chaining-and-what-it-no-longer-buys",[225,231],{"type":9,"tag":48,"props":226,"children":228},{"className":227},[],[229],{"type":14,"value":230},"RUN",{"type":14,"value":232}," chaining, and what it no longer buys",{"type":9,"tag":10,"props":234,"children":235},{},[236],{"type":14,"value":237},"You will see a lot of Dockerfiles doing this:",{"type":9,"tag":41,"props":239,"children":242},{"className":240,"code":241,"language":107,"meta":50},[105],"RUN apt-get update && apt-get install -y curl \\\n    && rm -rf /var/lib/apt/lists/*\n",[243],{"type":9,"tag":48,"props":244,"children":245},{"__ignoreMap":50},[246],{"type":14,"value":241},{"type":9,"tag":10,"props":248,"children":249},{},[250,252,258,260,265,267,273,275,280],{"type":14,"value":251},"The ",{"type":9,"tag":48,"props":253,"children":255},{"className":254},[],[256],{"type":14,"value":257},"&&",{"type":14,"value":259}," chaining is not stylistic. ",{"type":9,"tag":17,"props":261,"children":262},{},[263],{"type":14,"value":264},"A layer is immutable once written",{"type":14,"value":266},", so deleting a file in a ",{"type":9,"tag":268,"props":269,"children":270},"em",{},[271],{"type":14,"value":272},"later",{"type":14,"value":274}," instruction does not remove its bytes from the image — it records a deletion on top, and the original layer still ships and still gets pulled. Cleanup only shrinks the image if it happens inside the same ",{"type":9,"tag":48,"props":276,"children":278},{"className":277},[],[279],{"type":14,"value":230},{"type":14,"value":281}," that created the mess.",{"type":9,"tag":10,"props":283,"children":284},{},[285],{"type":14,"value":286},"That is why the classic mistake is expensive:",{"type":9,"tag":41,"props":288,"children":291},{"className":289,"code":290,"language":107,"meta":50},[105],"RUN apt-get install -y build-essential   # +400 MB\nRUN make && make install\nRUN apt-get remove -y build-essential    # image is still +400 MB\n",[292],{"type":9,"tag":48,"props":293,"children":294},{"__ignoreMap":50},[295],{"type":14,"value":290},{"type":9,"tag":10,"props":297,"children":298},{},[299,301,306],{"type":14,"value":300},"Modern BuildKit gives you better answers than chaining — cache mounts, and multi-stage builds where the toolchain simply never exists in the final image. Both are the first two lessons of ",{"type":9,"tag":268,"props":302,"children":303},{},[304],{"type":14,"value":305},"Docker in Practice",{"type":14,"value":307},". Chaining remains the fallback when neither applies.",{"data":309,"body":310},{},{"type":6,"children":311},[312],{"type":9,"tag":210,"props":313,"children":317},{":answer":212,":options":314,"explanation":315,"question":316},"[\"Roughly 300 MB larger — the files still exist in the earlier layer and are still shipped\",\"Unchanged, since the files are gone from the final filesystem\",\"Smaller, because the delete layer compresses well\"]","Layers are append-only. The later layer records a whiteout marker; the bytes below it are still in the image and still pulled by everyone. Same instruction, or a separate build stage.","A Dockerfile installs a 300 MB toolchain in one `RUN` and deletes it in a later `RUN`. What is the final image size impact?",[],{"data":319,"body":320},{},{"type":6,"children":321},[322,328,333,342,371],{"type":9,"tag":34,"props":323,"children":325},{"id":324},"anything-written-into-a-layer-is-public",[326],{"type":14,"value":327},"Anything written into a layer is public",{"type":9,"tag":10,"props":329,"children":330},{},[331],{"type":14,"value":332},"This is the security half, and it is worth being blunt.",{"type":9,"tag":41,"props":334,"children":337},{"className":335,"code":336,"language":107,"meta":50},[105],"COPY .env /app/.env\nRUN ./setup.sh && rm /app/.env     # the file is gone from the final filesystem\n",[338],{"type":9,"tag":48,"props":339,"children":340},{"__ignoreMap":50},[341],{"type":14,"value":336},{"type":9,"tag":10,"props":343,"children":344},{},[345,347,353,355,361,363,369],{"type":14,"value":346},"The file is still in the image. Anyone who pulls it can ",{"type":9,"tag":48,"props":348,"children":350},{"className":349},[],[351],{"type":14,"value":352},"docker save",{"type":14,"value":354}," the image, untar it, and read the layer. The same is true of a secret passed as ",{"type":9,"tag":48,"props":356,"children":358},{"className":357},[],[359],{"type":14,"value":360},"ARG",{"type":14,"value":362},", which additionally shows up in ",{"type":9,"tag":48,"props":364,"children":366},{"className":365},[],[367],{"type":14,"value":368},"docker history",{"type":14,"value":370}," in plain text.",{"type":9,"tag":10,"props":372,"children":373},{},[374,376,382,384,388],{"type":14,"value":375},"There is no way to remove it after the fact except rebuilding without it and treating the credential as compromised. The real answer is ",{"type":9,"tag":48,"props":377,"children":379},{"className":378},[],[380],{"type":14,"value":381},"RUN --mount=type=secret",{"type":14,"value":383},", covered in ",{"type":9,"tag":268,"props":385,"children":386},{},[387],{"type":14,"value":305},{"type":14,"value":389}," — a secret mounted for one instruction and never written to any layer.",{"data":391,"body":392},{},{"type":6,"children":393},[394],{"type":9,"tag":395,"props":396,"children":401},"fill-blank",{":answer":397,"hint":398,"placeholder":399,"prompt":400},"[\"docker history myapp:1.0\",\"docker image history myapp:1.0\"]","One word, and it means what it sounds like.","docker ...","Show the layer-by-layer history of the image `myapp:1.0`.",[],{"data":403,"body":404},{},{"type":6,"children":405},[406,529],{"type":9,"tag":407,"props":408,"children":410},"deep-dive",{"title":409},"Reading a BuildKit build, and when to break the cache",[411,416,425,443,455,465,474,479],{"type":9,"tag":10,"props":412,"children":413},{},[414],{"type":14,"value":415},"BuildKit's output tells you what it did with each step:",{"type":9,"tag":41,"props":417,"children":420},{"className":418,"code":419,"language":14},[44]," => CACHED [3/6] COPY package*.json ./\n => CACHED [4/6] RUN npm ci --omit=dev\n => [5/6] COPY . .                          0.3s\n",[421],{"type":9,"tag":48,"props":422,"children":423},{"__ignoreMap":50},[424],{"type":14,"value":419},{"type":9,"tag":10,"props":426,"children":427},{},[428,434,436,441],{"type":9,"tag":48,"props":429,"children":431},{"className":430},[],[432],{"type":14,"value":433},"CACHED",{"type":14,"value":435}," means reused. A step with a duration ran. The first non-",{"type":9,"tag":48,"props":437,"children":439},{"className":438},[],[440],{"type":14,"value":433},{"type":14,"value":442}," line is where your cache broke — and if that line is higher up than you expected, that is the thing to fix.",{"type":9,"tag":10,"props":444,"children":445},{},[446,448,453],{"type":14,"value":447},"BuildKit also builds a ",{"type":9,"tag":17,"props":449,"children":450},{},[451],{"type":14,"value":452},"graph",{"type":14,"value":454},", not a list: independent stages run in parallel, and a stage nothing depends on is skipped entirely. This is why a multi-stage build is often faster than the single-stage version rather than slower.",{"type":9,"tag":10,"props":456,"children":457},{},[458,463],{"type":9,"tag":17,"props":459,"children":460},{},[461],{"type":14,"value":462},"Deliberately breaking the cache",{"type":14,"value":464}," is occasionally what you want:",{"type":9,"tag":41,"props":466,"children":469},{"className":467,"code":468,"language":14},[44],"docker build --no-cache -t myapp .          # rebuild everything\ndocker build --no-cache-filter deps -t myapp .   # rebuild one stage\n",[470],{"type":9,"tag":48,"props":471,"children":472},{"__ignoreMap":50},[473],{"type":14,"value":468},{"type":9,"tag":10,"props":475,"children":476},{},[477],{"type":14,"value":478},"The second is the more useful and the less known: it discards the cache for a named stage and keeps the rest.",{"type":9,"tag":10,"props":480,"children":481},{},[482,484,490,492,498,500,506,508,514,516,521,523,528],{"type":14,"value":483},"There is one cache miss that is not your fault. ",{"type":9,"tag":48,"props":485,"children":487},{"className":486},[],[488],{"type":14,"value":489},"RUN apt-get update",{"type":14,"value":491}," caches happily, and a week later it is installing package lists that no longer match the mirror — the notorious ",{"type":9,"tag":48,"props":493,"children":495},{"className":494},[],[496],{"type":14,"value":497},"404 Not Found",{"type":14,"value":499}," on a version that was fine yesterday. Keeping ",{"type":9,"tag":48,"props":501,"children":503},{"className":502},[],[504],{"type":14,"value":505},"update",{"type":14,"value":507}," and ",{"type":9,"tag":48,"props":509,"children":511},{"className":510},[],[512],{"type":14,"value":513},"install",{"type":14,"value":515}," in the same ",{"type":9,"tag":48,"props":517,"children":519},{"className":518},[],[520],{"type":14,"value":230},{"type":14,"value":522}," is the standard defence, which is the other reason for that ",{"type":9,"tag":48,"props":524,"children":526},{"className":525},[],[527],{"type":14,"value":257},{"type":14,"value":204},{"type":9,"tag":10,"props":530,"children":531},{},[532],{"type":14,"value":533},"Next up: volumes — where data goes when the container it lived in is deleted.",1787908867635]