[{"data":1,"prerenderedAt":544},["ShallowReactive",2],{"mdc-h7sfq0-key":3,"mdc-1aaf9k-key":21,"mdc-u0sze7-key":45,"mdc-4pqtk5-key":72,"mdc--yi7tll-key":84,"mdc-2vc0m3-key":240,"mdc-1d7h8w-key":297,"mdc--ba60hs-key":307,"mdc-76z6jz-key":421,"mdc-euca0b-key":433},{"data":4,"body":5},{},{"type":6,"children":7},"root",[8,16],{"type":9,"tag":10,"props":11,"children":12},"element","p",{},[13],{"type":14,"value":15},"text","Image size is not vanity. It is deploy latency on every node, storage on every registry and every host, and — the part that actually costs time — the number of packages a scanner has to find vulnerabilities in.",{"type":9,"tag":10,"props":17,"children":18},{},[19],{"type":14,"value":20},"A 1.2 GB Node image and a 90 MB one run the same code. One of them reports two hundred CVEs from software you never invoke.",{"data":22,"body":23},{},{"type":6,"children":24},[25,32],{"type":9,"tag":26,"props":27,"children":29},"h2",{"id":28},"measure-before-you-optimise",[30],{"type":14,"value":31},"Measure before you optimise",{"type":9,"tag":33,"props":34,"children":38},"pre",{"className":35,"code":37,"language":14},[36],"language-text","docker image ls myapp\ndocker history myapp:1.0 --no-trunc --format '{{.Size}}\\t{{.CreatedBy}}'\ndocker system df -v\n",[39],{"type":9,"tag":40,"props":41,"children":43},"code",{"__ignoreMap":42},"",[44],{"type":14,"value":37},{"data":46,"body":47},{},{"type":6,"children":48},[49,54,67],{"type":9,"tag":50,"props":51,"children":53},"terminal-teaser",{":lines":52},"[{\"cmd\":\"docker history myapp:1.0 --format '{{.Size}}\\\\t{{.CreatedBy}}' | head -6\",\"out\":\"0B        CMD [\\\"node\\\" \\\"dist/server.js\\\"]\\n2.1MB     COPY /app/dist ./dist\\n184MB     RUN npm ci\\n4.1kB     COPY package*.json ./\\n0B        WORKDIR /app\\n142MB     FROM node:22\"}]",[],{"type":9,"tag":10,"props":55,"children":56},{},[57,59,65],{"type":14,"value":58},"Two lines account for 326 of the 328 MB. That is the general shape: ",{"type":9,"tag":60,"props":61,"children":62},"strong",{},[63],{"type":14,"value":64},"the base image and the dependency install are almost always the whole story",{"type":14,"value":66},", and everything else is rounding.",{"type":9,"tag":10,"props":68,"children":69},{},[70],{"type":14,"value":71},"Which means the two decisions worth making are which base, and what ends up in the final stage.",{"data":73,"body":74},{},{"type":6,"children":75},[76],{"type":9,"tag":77,"props":78,"children":83},"quiz",{":answer":79,":options":80,"explanation":81,"question":82},"0","[\"A smaller base and a runtime stage that installs only production dependencies\",\"Combining the `COPY` instructions\",\"Compressing the source before copying it\"]","The small layers are noise. `node:22-alpine` or `-slim` cuts the base substantially, and `npm ci --omit=dev` in a runtime stage that only receives the build output typically halves the install.","`docker history` shows the base at 142 MB and one `RUN npm ci` at 184 MB. Where is the leverage?",[],{"data":85,"body":86},{},{"type":6,"children":87},[88,94,127,161,179,197,222],{"type":9,"tag":26,"props":89,"children":91},{"id":90},"the-moves-that-matter-in-order",[92],{"type":14,"value":93},"The moves that matter, in order",{"type":9,"tag":10,"props":95,"children":96},{},[97,102,104,110,112,118,119,125],{"type":9,"tag":60,"props":98,"children":99},{},[100],{"type":14,"value":101},"Pick a smaller base.",{"type":14,"value":103}," Usually the single biggest win and a one-line change. ",{"type":9,"tag":40,"props":105,"children":107},{"className":106},[],[108],{"type":14,"value":109},"node:22",{"type":14,"value":111}," → ",{"type":9,"tag":40,"props":113,"children":115},{"className":114},[],[116],{"type":14,"value":117},"node:22-slim",{"type":14,"value":111},{"type":9,"tag":40,"props":120,"children":122},{"className":121},[],[123],{"type":14,"value":124},"node:22-alpine",{"type":14,"value":126}," walks 142 MB down to about 50, then about 12. Verify at each step: Alpine's musl libc breaks prebuilt wheels and some native modules, and a broken image is not small, it is broken.",{"type":9,"tag":10,"props":128,"children":129},{},[130,135,137,143,145,151,153,159],{"type":9,"tag":60,"props":131,"children":132},{},[133],{"type":14,"value":134},"Ship only runtime dependencies.",{"type":14,"value":136}," A multi-stage build where the final stage installs with ",{"type":9,"tag":40,"props":138,"children":140},{"className":139},[],[141],{"type":14,"value":142},"--omit=dev",{"type":14,"value":144},", ",{"type":9,"tag":40,"props":146,"children":148},{"className":147},[],[149],{"type":14,"value":150},"--no-dev",{"type":14,"value":152},", or ",{"type":9,"tag":40,"props":154,"children":156},{"className":155},[],[157],{"type":14,"value":158},"--production",{"type":14,"value":160}," removes the entire test and build toolchain.",{"type":9,"tag":10,"props":162,"children":163},{},[164,169,171,177],{"type":9,"tag":60,"props":165,"children":166},{},[167],{"type":14,"value":168},"Do not ship the toolchain.",{"type":14,"value":170}," Compilers, headers, and ",{"type":9,"tag":40,"props":172,"children":174},{"className":173},[],[175],{"type":14,"value":176},"build-essential",{"type":14,"value":178}," belong in a build stage. If the final image can compile your code, you shipped the compiler.",{"type":9,"tag":10,"props":180,"children":181},{},[182,195],{"type":9,"tag":60,"props":183,"children":184},{},[185,187,193],{"type":14,"value":186},"Clean inside the same ",{"type":9,"tag":40,"props":188,"children":190},{"className":189},[],[191],{"type":14,"value":192},"RUN",{"type":14,"value":194},".",{"type":14,"value":196}," Layers are append-only, so a deletion in a later instruction removes nothing.",{"type":9,"tag":10,"props":198,"children":199},{},[200,212,214,220],{"type":9,"tag":60,"props":201,"children":202},{},[203,205,211],{"type":14,"value":204},"Have a real ",{"type":9,"tag":40,"props":206,"children":208},{"className":207},[],[209],{"type":14,"value":210},".dockerignore",{"type":14,"value":194},{"type":14,"value":213}," ",{"type":9,"tag":40,"props":215,"children":217},{"className":216},[],[218],{"type":14,"value":219},".git",{"type":14,"value":221}," on a mature repository is often larger than the application.",{"type":9,"tag":10,"props":223,"children":224},{},[225,238],{"type":9,"tag":60,"props":226,"children":227},{},[228,230,236],{"type":14,"value":229},"Do not ",{"type":9,"tag":40,"props":231,"children":233},{"className":232},[],[234],{"type":14,"value":235},"COPY . .",{"type":14,"value":237}," into the final stage.",{"type":14,"value":239}," Copy the built artifact. Source, tests, fixtures, and CI configuration have no business in a production image.",{"data":241,"body":242},{},{"type":6,"children":243},[244,250,255,266,278,283,292],{"type":9,"tag":26,"props":245,"children":247},{"id":246},"a-worked-before-and-after",[248],{"type":14,"value":249},"A worked before-and-after",{"type":9,"tag":10,"props":251,"children":252},{},[253],{"type":14,"value":254},"Before — 1.14 GB:",{"type":9,"tag":33,"props":256,"children":261},{"className":257,"code":259,"language":260,"meta":42},[258],"language-dockerfile","FROM node:22\nWORKDIR /app\nCOPY . .\nRUN npm install\nRUN npm run build\nCMD [\"node\", \"dist/server.js\"]\n","dockerfile",[262],{"type":9,"tag":40,"props":263,"children":264},{"__ignoreMap":42},[265],{"type":14,"value":259},{"type":9,"tag":10,"props":267,"children":268},{},[269,271,276],{"type":14,"value":270},"Every problem at once: fat base, whole context copied first (so nothing caches), dev dependencies installed and kept, source and ",{"type":9,"tag":40,"props":272,"children":274},{"className":273},[],[275],{"type":14,"value":219},{"type":14,"value":277}," shipped, build tooling in the final image.",{"type":9,"tag":10,"props":279,"children":280},{},[281],{"type":14,"value":282},"After — 78 MB:",{"type":9,"tag":33,"props":284,"children":287},{"className":285,"code":286,"language":260,"meta":42},[258],"# syntax=docker/dockerfile:1\n\nFROM node:22-alpine AS build\nWORKDIR /app\nCOPY package*.json ./\nRUN --mount=type=cache,target=/root/.npm npm ci\nCOPY . .\nRUN npm run build\n\nFROM node:22-alpine AS runtime\nWORKDIR /app\nENV NODE_ENV=production\nCOPY package*.json ./\nRUN --mount=type=cache,target=/root/.npm npm ci --omit=dev\nCOPY --from=build /app/dist ./dist\nUSER node\nCMD [\"node\", \"dist/server.js\"]\n",[288],{"type":9,"tag":40,"props":289,"children":290},{"__ignoreMap":42},[291],{"type":14,"value":286},{"type":9,"tag":10,"props":293,"children":294},{},[295],{"type":14,"value":296},"Same application. Fifteen times smaller, and it rebuilds in seconds because the dependency layers only move when the lockfile does.",{"data":298,"body":299},{},{"type":6,"children":300},[301],{"type":9,"tag":77,"props":302,"children":306},{":answer":79,":options":303,"explanation":304,"question":305},"[\"The build stage's `node_modules` includes dev dependencies; a fresh production install ships only what runs\",\"Copying `node_modules` between stages is not permitted\",\"It is faster\"]","You could copy it — and you would carry the test framework, the bundler, and the type checker into production. The second install is cached on the lockfile, so it is nearly free, and it is the difference between shipping 400 packages and 90.","Why does the runtime stage run `npm ci --omit=dev` again instead of copying `node_modules` from the build stage?",[],{"data":308,"body":309},{},{"type":6,"children":310},[311,317,322,340,349,363,372,400],{"type":9,"tag":26,"props":312,"children":314},{"id":313},"going-smaller-still",[315],{"type":14,"value":316},"Going smaller still",{"type":9,"tag":10,"props":318,"children":319},{},[320],{"type":14,"value":321},"Below Alpine there are two more steps, both trading debuggability for surface.",{"type":9,"tag":10,"props":323,"children":324},{},[325,330,332,338],{"type":9,"tag":60,"props":326,"children":327},{},[328],{"type":14,"value":329},"Distroless",{"type":14,"value":331}," — the language runtime and nothing else. No shell, no package manager, no ",{"type":9,"tag":40,"props":333,"children":335},{"className":334},[],[336],{"type":14,"value":337},"ls",{"type":14,"value":339},":",{"type":9,"tag":33,"props":341,"children":344},{"className":342,"code":343,"language":260,"meta":42},[258],"FROM gcr.io/distroless/nodejs22-debian12\nCOPY --from=build /app/dist /app/dist\nCMD [\"/app/dist/server.js\"]\n",[345],{"type":9,"tag":40,"props":346,"children":347},{"__ignoreMap":42},[348],{"type":14,"value":343},{"type":9,"tag":10,"props":350,"children":351},{},[352,361],{"type":9,"tag":60,"props":353,"children":354},{},[355],{"type":9,"tag":40,"props":356,"children":358},{"className":357},[],[359],{"type":14,"value":360},"scratch",{"type":14,"value":362}," — genuinely empty. Only for static binaries:",{"type":9,"tag":33,"props":364,"children":367},{"className":365,"code":366,"language":260,"meta":42},[258],"FROM scratch\nCOPY --from=build /out/server /server\nCOPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/\nENTRYPOINT [\"/server\"]\n",[368],{"type":9,"tag":40,"props":369,"children":370},{"__ignoreMap":42},[371],{"type":14,"value":366},{"type":9,"tag":10,"props":373,"children":374},{},[375,377,382,384,390,392,398],{"type":14,"value":376},"That certificate copy is the detail everyone hits: an empty image has no CA bundle, so every outbound HTTPS call fails with a certificate error that looks nothing like \"your image is empty\". ",{"type":9,"tag":40,"props":378,"children":380},{"className":379},[],[381],{"type":14,"value":360},{"type":14,"value":383}," images also usually need ",{"type":9,"tag":40,"props":385,"children":387},{"className":386},[],[388],{"type":14,"value":389},"/etc/passwd",{"type":14,"value":391}," for a non-root user and ",{"type":9,"tag":40,"props":393,"children":395},{"className":394},[],[396],{"type":14,"value":397},"/tmp",{"type":14,"value":399}," if anything writes there.",{"type":9,"tag":10,"props":401,"children":402},{},[403,405,411,413,419],{"type":14,"value":404},"The real cost of both is at 3am. ",{"type":9,"tag":40,"props":406,"children":408},{"className":407},[],[409],{"type":14,"value":410},"docker exec",{"type":14,"value":412}," needs a shell, and there is not one. ",{"type":9,"tag":40,"props":414,"children":416},{"className":415},[],[417],{"type":14,"value":418},"docker debug",{"type":14,"value":420}," (Docker Desktop) attaches a toolbox to a container without changing the image, and is the reason distroless is practical at all.",{"data":422,"body":423},{},{"type":6,"children":424},[425],{"type":9,"tag":426,"props":427,"children":432},"fill-blank",{":answer":428,"hint":429,"placeholder":430,"prompt":431},"[\"docker history --no-trunc myapp:1.0\",\"docker history myapp:1.0 --no-trunc\",\"docker image history --no-trunc myapp:1.0\"]","The history command, plus the flag that stops it abbreviating.","docker history ...","Show the layer sizes of `myapp:1.0` with the full untruncated commands.",[],{"data":434,"body":435},{},{"type":6,"children":436},[437,515,527],{"type":9,"tag":438,"props":439,"children":441},"deep-dive",{"title":440},"What small actually buys, and when to stop",[442,447,457,475,485,510],{"type":9,"tag":10,"props":443,"children":444},{},[445],{"type":14,"value":446},"The honest accounting, because \"smaller is better\" is not a strategy.",{"type":9,"tag":10,"props":448,"children":449},{},[450,455],{"type":9,"tag":60,"props":451,"children":452},{},[453],{"type":14,"value":454},"Deploy speed.",{"type":14,"value":456}," Layers are cached per host, so on a machine that already has the base, a redeploy only pulls what changed. Well-ordered layers matter more than total size — a 500 MB image with a stable 480 MB base redeploys faster than a 200 MB image whose bottom layer changes every build.",{"type":9,"tag":10,"props":458,"children":459},{},[460,465,467,473],{"type":9,"tag":60,"props":461,"children":462},{},[463],{"type":14,"value":464},"Attack surface.",{"type":14,"value":466}," This is the serious one. A distro base ships hundreds of packages, and a scanner reports vulnerabilities in every one — including the ones your application never calls. Most are unreachable in practice, but \"unreachable in practice\" is not a thing you can put in a compliance report, so somebody spends a week triaging. Fewer packages, less triage. That is what ",{"type":9,"tag":468,"props":469,"children":470},"em",{},[471],{"type":14,"value":472},"Docker in Production",{"type":14,"value":474}," picks up with hardened images and VEX.",{"type":9,"tag":10,"props":476,"children":477},{},[478,483],{"type":9,"tag":60,"props":479,"children":480},{},[481],{"type":14,"value":482},"Cost.",{"type":14,"value":484}," Registry storage and egress are real at scale and negligible below it.",{"type":9,"tag":10,"props":486,"children":487},{},[488,493,495,501,503,508],{"type":9,"tag":60,"props":489,"children":490},{},[491],{"type":14,"value":492},"Where to stop.",{"type":14,"value":494}," Alpine or ",{"type":9,"tag":40,"props":496,"children":498},{"className":497},[],[499],{"type":14,"value":500},"-slim",{"type":14,"value":502}," plus a multi-stage build gets most of the benefit for almost no operational cost. Distroless is worth it for a service you deploy often and rarely debug interactively. ",{"type":9,"tag":40,"props":504,"children":506},{"className":505},[],[507],{"type":14,"value":360},{"type":14,"value":509}," is worth it for a static binary and painful for anything else.",{"type":9,"tag":10,"props":511,"children":512},{},[513],{"type":14,"value":514},"The one thing not worth doing is squashing layers to make the number smaller. It destroys layer sharing, so every deploy pulls the entire image instead of the changed part — a smaller image that is slower to deploy, which is the opposite of the point.",{"type":9,"tag":10,"props":516,"children":517},{},[518,520,525],{"type":14,"value":519},"That is ",{"type":9,"tag":468,"props":521,"children":522},{},[523],{"type":14,"value":524},"Docker in Practice",{"type":14,"value":526},": multi-stage builds, BuildKit's mounts, secrets that stay out of layers, multi-platform images, Bake, the Compose features a real project needs, lifecycle done properly, cache that survives CI, and size on purpose.",{"type":9,"tag":10,"props":528,"children":529},{},[530,534,536,542],{"type":9,"tag":468,"props":531,"children":532},{},[533],{"type":14,"value":472},{"type":14,"value":535}," takes it the rest of the way — the engine underneath, container security, supply chain and attestations, Scout, hardened images, observability, and running AI workloads with Model Runner and Compose's ",{"type":9,"tag":40,"props":537,"children":539},{"className":538},[],[540],{"type":14,"value":541},"models",{"type":14,"value":543}," support.",1787908868330]