[{"data":1,"prerenderedAt":663},["ShallowReactive",2],{"mdc-gxnlyj-key":3,"mdc-e5njy7-key":30,"mdc-nozr3x-key":71,"mdc--wk4meg-key":113,"mdc--isivuv-key":125,"mdc--6gli5-key":338,"mdc-tln7dw-key":399,"mdc-9h7ula-key":409,"mdc-5gdniw-key":495,"mdc--fdq18m-key":507},{"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","Your laptop rebuilds in four seconds. CI takes eight minutes for the same commit, every time, because a fresh runner has no cache and the first ",{"type":9,"tag":17,"props":18,"children":20},"code",{"className":19},[],[21],{"type":14,"value":22},"RUN",{"type":14,"value":24}," misses.",{"type":9,"tag":10,"props":26,"children":27},{},[28],{"type":14,"value":29},"Everything in the Beginner course about layer ordering still applies — but a cache only helps if the machine doing the build has it. This lesson is about moving it.",{"data":31,"body":32},{},{"type":6,"children":33},[34,41,46,59],{"type":9,"tag":35,"props":36,"children":38},"h2",{"id":37},"where-the-cache-lives",[39],{"type":14,"value":40},"Where the cache lives",{"type":9,"tag":10,"props":42,"children":43},{},[44],{"type":14,"value":45},"BuildKit keeps its cache in the builder, not in the image. So a cache is tied to a builder instance, and a CI runner that is created and destroyed per job has a cold one every time.",{"type":9,"tag":10,"props":47,"children":48},{},[49,51,57],{"type":14,"value":50},"The fix is a ",{"type":9,"tag":52,"props":53,"children":54},"strong",{},[55],{"type":14,"value":56},"cache backend",{"type":14,"value":58},": export the cache somewhere durable at the end of a build, import it at the start of the next.",{"type":9,"tag":60,"props":61,"children":65},"pre",{"className":62,"code":64,"language":14},[63],"language-text","docker buildx build \\\n  --cache-to   type=registry,ref=ghcr.io/acme/app:buildcache,mode=max \\\n  --cache-from type=registry,ref=ghcr.io/acme/app:buildcache \\\n  -t ghcr.io/acme/app:1.0 --push .\n",[66],{"type":9,"tag":17,"props":67,"children":69},{"__ignoreMap":68},"",[70],{"type":14,"value":64},{"data":72,"body":73},{},{"type":6,"children":74},[75,80],{"type":9,"tag":76,"props":77,"children":79},"terminal-teaser",{":lines":78},"[{\"cmd\":\"docker buildx build --cache-from type=registry,ref=ghcr.io/acme/app:buildcache -t app .\",\"out\":\"=> importing cache manifest from ghcr.io/acme/app:buildcache\\n=> CACHED [2/6] COPY package*.json ./\\n=> CACHED [3/6] RUN npm ci --omit=dev\\n=> [4/6] COPY . .                            0.4s\\n[+] Building 11.2s (14/14) FINISHED\"}]",[],{"type":9,"tag":10,"props":81,"children":82},{},[83,89,91,97,99,104,106,111],{"type":9,"tag":17,"props":84,"children":86},{"className":85},[],[87],{"type":14,"value":88},"mode=max",{"type":14,"value":90}," is the flag that matters and the one people miss. The default, ",{"type":9,"tag":17,"props":92,"children":94},{"className":93},[],[95],{"type":14,"value":96},"mode=min",{"type":14,"value":98},", exports only the layers of the final image — which for a multi-stage build means ",{"type":9,"tag":52,"props":100,"children":101},{},[102],{"type":14,"value":103},"none of the expensive intermediate stages",{"type":14,"value":105},", so the cache imports and the dependency install still runs. ",{"type":9,"tag":17,"props":107,"children":109},{"className":108},[],[110],{"type":14,"value":88},{"type":14,"value":112}," exports every stage.",{"data":114,"body":115},{},{"type":6,"children":116},[117],{"type":9,"tag":118,"props":119,"children":124},"quiz",{":answer":120,":options":121,"explanation":122,"question":123},"0","[\"`mode=max` on `--cache-to` — the default only exports the final stage's layers\",\"The `--push` flag\",\"A separate builder for each stage\"]","With `mode=min`, intermediate stages — where the install happens in any multi-stage build — are not exported at all. The import succeeds and caches almost nothing, which is why it looks like the feature is broken rather than misconfigured.","You added a registry cache to CI and dependency installs are still not cached. What is most likely missing?",[],{"data":126,"body":127},{},{"type":6,"children":128},[129,135,293,317],{"type":9,"tag":35,"props":130,"children":132},{"id":131},"the-backends",[133],{"type":14,"value":134},"The backends",{"type":9,"tag":136,"props":137,"children":138},"table",{},[139,163],{"type":9,"tag":140,"props":141,"children":142},"thead",{},[143],{"type":9,"tag":144,"props":145,"children":146},"tr",{},[147,153,158],{"type":9,"tag":148,"props":149,"children":150},"th",{},[151],{"type":14,"value":152},"Type",{"type":9,"tag":148,"props":154,"children":155},{},[156],{"type":14,"value":157},"Where it stores",{"type":9,"tag":148,"props":159,"children":160},{},[161],{"type":14,"value":162},"Good for",{"type":9,"tag":164,"props":165,"children":166},"tbody",{},[167,190,212,242,264],{"type":9,"tag":144,"props":168,"children":169},{},[170,180,185],{"type":9,"tag":171,"props":172,"children":173},"td",{},[174],{"type":9,"tag":17,"props":175,"children":177},{"className":176},[],[178],{"type":14,"value":179},"registry",{"type":9,"tag":171,"props":181,"children":182},{},[183],{"type":14,"value":184},"a tag in your container registry",{"type":9,"tag":171,"props":186,"children":187},{},[188],{"type":14,"value":189},"anywhere; the portable default",{"type":9,"tag":144,"props":191,"children":192},{},[193,202,207],{"type":9,"tag":171,"props":194,"children":195},{},[196],{"type":9,"tag":17,"props":197,"children":199},{"className":198},[],[200],{"type":14,"value":201},"gha",{"type":9,"tag":171,"props":203,"children":204},{},[205],{"type":14,"value":206},"GitHub Actions' cache service",{"type":9,"tag":171,"props":208,"children":209},{},[210],{"type":14,"value":211},"GitHub Actions",{"type":9,"tag":144,"props":213,"children":214},{},[215,232,237],{"type":9,"tag":171,"props":216,"children":217},{},[218,224,226],{"type":9,"tag":17,"props":219,"children":221},{"className":220},[],[222],{"type":14,"value":223},"s3",{"type":14,"value":225}," / ",{"type":9,"tag":17,"props":227,"children":229},{"className":228},[],[230],{"type":14,"value":231},"azblob",{"type":9,"tag":171,"props":233,"children":234},{},[235],{"type":14,"value":236},"object storage",{"type":9,"tag":171,"props":238,"children":239},{},[240],{"type":14,"value":241},"self-hosted runners, large caches",{"type":9,"tag":144,"props":243,"children":244},{},[245,254,259],{"type":9,"tag":171,"props":246,"children":247},{},[248],{"type":9,"tag":17,"props":249,"children":251},{"className":250},[],[252],{"type":14,"value":253},"local",{"type":9,"tag":171,"props":255,"children":256},{},[257],{"type":14,"value":258},"a directory",{"type":9,"tag":171,"props":260,"children":261},{},[262],{"type":14,"value":263},"a persistent runner or a mounted volume",{"type":9,"tag":144,"props":265,"children":266},{},[267,276,281],{"type":9,"tag":171,"props":268,"children":269},{},[270],{"type":9,"tag":17,"props":271,"children":273},{"className":272},[],[274],{"type":14,"value":275},"inline",{"type":9,"tag":171,"props":277,"children":278},{},[279],{"type":14,"value":280},"inside the image itself",{"type":9,"tag":171,"props":282,"children":283},{},[284,286,291],{"type":14,"value":285},"simple cases, ",{"type":9,"tag":17,"props":287,"children":289},{"className":288},[],[290],{"type":14,"value":96},{"type":14,"value":292}," only",{"type":9,"tag":10,"props":294,"children":295},{},[296,301,303,308,310,315],{"type":9,"tag":17,"props":297,"children":299},{"className":298},[],[300],{"type":14,"value":275},{"type":14,"value":302}," is the easy one and the limited one — the cache metadata rides along inside the published image, so there is nothing extra to manage, but it cannot do ",{"type":9,"tag":17,"props":304,"children":306},{"className":305},[],[307],{"type":14,"value":88},{"type":14,"value":309},". For anything multi-stage, use ",{"type":9,"tag":17,"props":311,"children":313},{"className":312},[],[314],{"type":14,"value":179},{"type":14,"value":316},".",{"type":9,"tag":10,"props":318,"children":319},{},[320,322,328,330,336],{"type":14,"value":321},"A registry cache is a real tag consuming real storage, so give it its own name and a retention policy. ",{"type":9,"tag":17,"props":323,"children":325},{"className":324},[],[326],{"type":14,"value":327},"app:buildcache",{"type":14,"value":329}," next to ",{"type":9,"tag":17,"props":331,"children":333},{"className":332},[],[334],{"type":14,"value":335},"app:1.0",{"type":14,"value":337}," is the convention.",{"data":339,"body":340},{},{"type":6,"children":341},[342,348,359,386],{"type":9,"tag":35,"props":343,"children":345},{"id":344},"in-github-actions",[346],{"type":14,"value":347},"In GitHub Actions",{"type":9,"tag":60,"props":349,"children":354},{"className":350,"code":352,"language":353,"meta":68},[351],"language-yaml","      - uses: docker/setup-buildx-action@v3\n\n      - uses: docker/build-push-action@v6\n        with:\n          context: .\n          push: true\n          tags: ghcr.io/acme/app:${{ github.sha }}\n          platforms: linux/amd64,linux/arm64\n          cache-from: type=gha\n          cache-to: type=gha,mode=max\n","yaml",[355],{"type":9,"tag":17,"props":356,"children":357},{"__ignoreMap":68},[358],{"type":14,"value":352},{"type":9,"tag":10,"props":360,"children":361},{},[362,368,370,376,378,384],{"type":9,"tag":17,"props":363,"children":365},{"className":364},[],[366],{"type":14,"value":367},"setup-buildx-action",{"type":14,"value":369}," is required — the default ",{"type":9,"tag":17,"props":371,"children":373},{"className":372},[],[374],{"type":14,"value":375},"docker",{"type":14,"value":377}," driver cannot export cache or build multi-platform. ",{"type":9,"tag":17,"props":379,"children":381},{"className":380},[],[382],{"type":14,"value":383},"type=gha",{"type":14,"value":385}," uses the runner's own cache service and needs no registry credentials.",{"type":9,"tag":10,"props":387,"children":388},{},[389,391,397],{"type":14,"value":390},"GitHub's cache is scoped per branch, with reads falling back to the default branch. So the first build on a new branch inherits ",{"type":9,"tag":17,"props":392,"children":394},{"className":393},[],[395],{"type":14,"value":396},"main",{"type":14,"value":398},"'s cache and only pays for what actually differs — which is the behaviour you want and is easy to lose by keying it manually.",{"data":400,"body":401},{},{"type":6,"children":402},[403],{"type":9,"tag":118,"props":404,"children":408},{":answer":120,":options":405,"explanation":406,"question":407},"[\"The default `docker` driver supports neither cache export nor multi-platform builds\",\"It installs Docker on the runner\",\"It authenticates to the registry\"]","The runner has Docker already, and login is a separate action. What is missing is a `docker-container` builder — the default driver builds one platform and cannot export cache to any backend.","Why does a workflow need `docker/setup-buildx-action` before `build-push-action` for a cached multi-platform build?",[],{"data":410,"body":411},{},{"type":6,"children":412},[413,419,447,483],{"type":9,"tag":35,"props":414,"children":416},{"id":415},"cache-mounts-do-not-travel",[417],{"type":14,"value":418},"Cache mounts do not travel",{"type":9,"tag":10,"props":420,"children":421},{},[422,424,430,432,437,439,445],{"type":14,"value":423},"A subtlety that costs people an afternoon: ",{"type":9,"tag":17,"props":425,"children":427},{"className":426},[],[428],{"type":14,"value":429},"RUN --mount=type=cache",{"type":14,"value":431}," directories are ",{"type":9,"tag":52,"props":433,"children":434},{},[435],{"type":14,"value":436},"builder-local and are not exported",{"type":14,"value":438}," by ",{"type":9,"tag":17,"props":440,"children":442},{"className":441},[],[443],{"type":14,"value":444},"--cache-to",{"type":14,"value":446},". A fresh CI runner starts with an empty npm cache no matter how well the registry cache is configured.",{"type":9,"tag":10,"props":448,"children":449},{},[450,452,457,459,465,467,473,475,481],{"type":14,"value":451},"What the registry cache preserves is the ",{"type":9,"tag":52,"props":453,"children":454},{},[455],{"type":14,"value":456},"layer",{"type":14,"value":458}," — so if ",{"type":9,"tag":17,"props":460,"children":462},{"className":461},[],[463],{"type":14,"value":464},"package-lock.json",{"type":14,"value":466}," has not changed, the whole ",{"type":9,"tag":17,"props":468,"children":470},{"className":469},[],[471],{"type":14,"value":472},"npm ci",{"type":14,"value":474}," step is a cache hit and the mount is never consulted. The mount only earns its keep when the layer ",{"type":9,"tag":476,"props":477,"children":478},"em",{},[479],{"type":14,"value":480},"does",{"type":14,"value":482}," miss, which on a persistent runner is often and on an ephemeral one is every time.",{"type":9,"tag":10,"props":484,"children":485},{},[486,488,493],{"type":14,"value":487},"The practical reading: layer caching is what makes CI fast; cache mounts are what make a ",{"type":9,"tag":476,"props":489,"children":490},{},[491],{"type":14,"value":492},"miss",{"type":14,"value":494}," less painful. Configure both, expect the first to do the work.",{"data":496,"body":497},{},{"type":6,"children":498},[499],{"type":9,"tag":500,"props":501,"children":506},"fill-blank",{":answer":502,"hint":503,"placeholder":504,"prompt":505},"[\"--cache-to type=registry,ref=ghcr.io/acme/app:buildcache,mode=max\",\"--cache-to=type=registry,ref=ghcr.io/acme/app:buildcache,mode=max\"]","Type, ref, and the mode that exports every stage.","--cache-to ...","Add the flag that exports the build cache to the registry ref `ghcr.io/acme/app:buildcache`, including intermediate stages.",[],{"data":508,"body":509},{},{"type":6,"children":510},[511,658],{"type":9,"tag":512,"props":513,"children":515},"deep-dive",{"title":514},"Managed builders, and when to stop optimising",[516,521,531,540,545,555,560,570,653],{"type":9,"tag":10,"props":517,"children":518},{},[519],{"type":14,"value":520},"There is a point where the answer is not a better cache but a better machine.",{"type":9,"tag":10,"props":522,"children":523},{},[524,529],{"type":9,"tag":52,"props":525,"children":526},{},[527],{"type":14,"value":528},"Docker Build Cloud",{"type":14,"value":530}," gives you managed native builders — real amd64 and arm64 machines with a shared persistent cache — used with one flag:",{"type":9,"tag":60,"props":532,"children":535},{"className":533,"code":534,"language":14},[63],"docker buildx build --builder cloud-acme-default --platform linux/amd64,linux/arm64 --push -t ghcr.io/acme/app:1.0 .\n",[536],{"type":9,"tag":17,"props":537,"children":538},{"__ignoreMap":68},[539],{"type":14,"value":534},{"type":9,"tag":10,"props":541,"children":542},{},[543],{"type":14,"value":544},"The cache is shared across your whole team and CI, so a colleague's build warms yours. And multi-arch stops being an emulation problem, because each platform is built on hardware that natively runs it.",{"type":9,"tag":10,"props":546,"children":547},{},[548,553],{"type":9,"tag":52,"props":549,"children":550},{},[551],{"type":14,"value":552},"Docker Offload",{"type":14,"value":554}," is the adjacent idea for running rather than building: containers execute on cloud hardware while the local CLI and Compose experience is unchanged. Useful when the workload wants a GPU or more memory than the laptop has.",{"type":9,"tag":10,"props":556,"children":557},{},[558],{"type":14,"value":559},"Both are paid. Whether they are worth it is arithmetic: developer-minutes per build times builds per day. A five-minute CI build that runs forty times a day is a working day of waiting, every day.",{"type":9,"tag":10,"props":561,"children":562},{},[563,568],{"type":9,"tag":52,"props":564,"children":565},{},[566],{"type":14,"value":567},"Before paying for anything",{"type":14,"value":569},", get the free wins, in this order — they are ordered by how much they typically return:",{"type":9,"tag":571,"props":572,"children":573},"ol",{},[574,592,602,612,633,643],{"type":9,"tag":575,"props":576,"children":577},"li",{},[578,590],{"type":9,"tag":52,"props":579,"children":580},{},[581,583,589],{"type":14,"value":582},"A real ",{"type":9,"tag":17,"props":584,"children":586},{"className":585},[],[587],{"type":14,"value":588},".dockerignore",{"type":14,"value":316},{"type":14,"value":591}," Sending a 500 MB context is pure waste before any cache is consulted.",{"type":9,"tag":575,"props":593,"children":594},{},[595,600],{"type":9,"tag":52,"props":596,"children":597},{},[598],{"type":14,"value":599},"Correct layer ordering.",{"type":14,"value":601}," Manifest, install, then source. This is still the single biggest lever.",{"type":9,"tag":575,"props":603,"children":604},{},[605,610],{"type":9,"tag":52,"props":606,"children":607},{},[608],{"type":14,"value":609},"Multi-stage builds",{"type":14,"value":611},", so heavy stages are skipped for the targets that do not need them.",{"type":9,"tag":575,"props":613,"children":614},{},[615,625,627,632],{"type":9,"tag":52,"props":616,"children":617},{},[618,623],{"type":9,"tag":17,"props":619,"children":621},{"className":620},[],[622],{"type":14,"value":88},{"type":14,"value":624}," cache export",{"type":14,"value":626}," to a registry or ",{"type":9,"tag":17,"props":628,"children":630},{"className":629},[],[631],{"type":14,"value":201},{"type":14,"value":316},{"type":9,"tag":575,"props":634,"children":635},{},[636,641],{"type":9,"tag":52,"props":637,"children":638},{},[639],{"type":14,"value":640},"Cache mounts",{"type":14,"value":642}," for package managers.",{"type":9,"tag":575,"props":644,"children":645},{},[646,651],{"type":9,"tag":52,"props":647,"children":648},{},[649],{"type":14,"value":650},"Then",{"type":14,"value":652}," consider paying for hardware.",{"type":9,"tag":10,"props":654,"children":655},{},[656],{"type":14,"value":657},"Most slow builds are still failing at steps one and two, and no amount of managed infrastructure fixes a build that copies the whole repository before installing dependencies.",{"type":9,"tag":10,"props":659,"children":660},{},[661],{"type":14,"value":662},"Next up: the last lesson — making images small on purpose, and measuring whether you succeeded.",1787908868320]