[{"data":1,"prerenderedAt":659},["ShallowReactive",2],{"mdc-3sgkuu-key":3,"mdc--1bl64q-key":16,"mdc-mnmpkg-key":95,"mdc-8xydy-key":104,"mdc-623soc-key":193,"mdc--hipjrk-key":205,"mdc--mz5jhe-key":277,"mdc--7tkyj8-key":327,"mdc--h0a8pq-key":335,"mdc--l54uhy-key":347,"mdc--wiwah0-key":520,"mdc-z70gcw-key":530},{"data":4,"body":5},{},{"type":6,"children":7},"root",[8],{"type":9,"tag":10,"props":11,"children":12},"element","p",{},[13],{"type":14,"value":15},"text","Every container you have run came from an image, and every image came from somewhere. This lesson is about that somewhere, and about the naming scheme that confuses people for longer than it should.",{"data":17,"body":18},{},{"type":6,"children":19},[20,27,32,45,66],{"type":9,"tag":21,"props":22,"children":24},"h2",{"id":23},"reading-an-image-name",[25],{"type":14,"value":26},"Reading an image name",{"type":9,"tag":10,"props":28,"children":29},{},[30],{"type":14,"value":31},"A full image reference has four parts, and you almost never write all of them:",{"type":9,"tag":33,"props":34,"children":38},"pre",{"className":35,"code":37,"language":14},[36],"language-text","docker.io/library/nginx:1.29-alpine\n└──┬───┘ └──┬────┘ └─┬─┘ └────┬────┘\nregistry  namespace  name    tag\n",[39],{"type":9,"tag":40,"props":41,"children":43},"code",{"__ignoreMap":42},"",[44],{"type":14,"value":37},{"type":9,"tag":10,"props":46,"children":47},{},[48,50,56,58,64],{"type":14,"value":49},"Omit the registry and you get Docker Hub. Omit the namespace on Docker Hub and you get ",{"type":9,"tag":40,"props":51,"children":53},{"className":52},[],[54],{"type":14,"value":55},"library",{"type":14,"value":57},", the official-images namespace. Omit the tag and you get ",{"type":9,"tag":40,"props":59,"children":61},{"className":60},[],[62],{"type":14,"value":63},"latest",{"type":14,"value":65},".",{"type":9,"tag":10,"props":67,"children":68},{},[69,71,77,79,85,87,93],{"type":14,"value":70},"So ",{"type":9,"tag":40,"props":72,"children":74},{"className":73},[],[75],{"type":14,"value":76},"nginx",{"type":14,"value":78}," and ",{"type":9,"tag":40,"props":80,"children":82},{"className":81},[],[83],{"type":14,"value":84},"docker.io/library/nginx:latest",{"type":14,"value":86}," are the same thing. And ",{"type":9,"tag":40,"props":88,"children":90},{"className":89},[],[91],{"type":14,"value":92},"ghcr.io/myorg/myapp:v2",{"type":14,"value":94}," is a different registry entirely — the registry is just a hostname, and every cloud provider runs one.",{"data":96,"body":97},{},{"type":6,"children":98},[99],{"type":9,"tag":100,"props":101,"children":103},"terminal-teaser",{":lines":102},"[{\"cmd\":\"docker pull nginx:1.29-alpine\",\"out\":\"1.29-alpine: Pulling from library/nginx\\nDigest: sha256:9d1b1c0f...\\nStatus: Downloaded newer image for nginx:1.29-alpine\"},{\"cmd\":\"docker image ls\",\"out\":\"IMAGE          TAG          ID             SIZE\\nnginx          1.29-alpine  4c1e2b8a91d7   52.3MB\\nalpine         latest       a8f4e2c11b09   7.8MB\"}]",[],{"data":105,"body":106},{},{"type":6,"children":107},[108,119,124,142,151],{"type":9,"tag":21,"props":109,"children":111},{"id":110},"latest-is-not-the-latest",[112,117],{"type":9,"tag":40,"props":113,"children":115},{"className":114},[],[116],{"type":14,"value":63},{"type":14,"value":118}," is not the latest",{"type":9,"tag":10,"props":120,"children":121},{},[122],{"type":14,"value":123},"This is the naming trap, and it costs people real time.",{"type":9,"tag":10,"props":125,"children":126},{},[127,132,134,140],{"type":9,"tag":40,"props":128,"children":130},{"className":129},[],[131],{"type":14,"value":63},{"type":14,"value":133}," is not a version. It is not resolved at pull time to whatever is newest. ",{"type":9,"tag":135,"props":136,"children":137},"strong",{},[138],{"type":14,"value":139},"It is just the default tag",{"type":14,"value":141}," — the one used when you do not name one — and it points at whatever the publisher last pushed under that name. Which may be months old, or a release candidate, or an entirely different major version than last week.",{"type":9,"tag":33,"props":143,"children":146},{"className":144,"code":145,"language":14},[36],"FROM node:latest        # unpredictable: could change under you tomorrow\nFROM node:22            # better: pinned to a major line, still gets patches\nFROM node:22.14-alpine  # better still for reproducibility\nFROM node@sha256:9f2a…  # exact bytes, immutable, what production wants\n",[147],{"type":9,"tag":40,"props":148,"children":149},{"__ignoreMap":42},[150],{"type":14,"value":145},{"type":9,"tag":10,"props":152,"children":153},{},[154,156,161,163,169,171,176,178,184,186,191],{"type":14,"value":155},"Tags are ",{"type":9,"tag":135,"props":157,"children":158},{},[159],{"type":14,"value":160},"mutable pointers",{"type":14,"value":162},". A publisher can move ",{"type":9,"tag":40,"props":164,"children":166},{"className":165},[],[167],{"type":14,"value":168},"node:22",{"type":14,"value":170}," to new bytes at any time, and most do — that is how you get security patches. A ",{"type":9,"tag":135,"props":172,"children":173},{},[174],{"type":14,"value":175},"digest",{"type":14,"value":177}," (",{"type":9,"tag":40,"props":179,"children":181},{"className":180},[],[182],{"type":14,"value":183},"@sha256:…",{"type":14,"value":185},") is the content hash and cannot move. The Advanced course argues for digests in production; for now, the habit worth forming is simply never writing ",{"type":9,"tag":40,"props":187,"children":189},{"className":188},[],[190],{"type":14,"value":63},{"type":14,"value":192}," in a Dockerfile.",{"data":194,"body":195},{},{"type":6,"children":196},[197],{"type":9,"tag":198,"props":199,"children":204},"quiz",{":answer":200,":options":201,"explanation":202,"question":203},"0","[\"The `latest` tag now points at different bytes — probably a new Python version\",\"Docker Hub rate-limited your pull\",\"The build cache expired\"]","Tags are mutable pointers and `latest` is the one most likely to move. This is the whole argument for pinning. Rate limiting is real too, but it fails loudly with a 429 rather than a compile error.","Your build worked yesterday and fails today. Nothing in your repository changed. The Dockerfile starts `FROM python:latest`. What is the likely cause?",[],{"data":206,"body":207},{},{"type":6,"children":208},[209,223,248,257],{"type":9,"tag":21,"props":210,"children":212},{"id":211},"what-docker-image-ls-shows-now",[213,215,221],{"type":14,"value":214},"What ",{"type":9,"tag":40,"props":216,"children":218},{"className":217},[],[219],{"type":14,"value":220},"docker image ls",{"type":14,"value":222}," shows now",{"type":9,"tag":10,"props":224,"children":225},{},[226,228,234,236,247],{"type":14,"value":227},"Docker Engine 29 changed this command's default output. It now uses what was previously behind ",{"type":9,"tag":40,"props":229,"children":231},{"className":230},[],[232],{"type":14,"value":233},"--tree",{"type":14,"value":235},", and it ",{"type":9,"tag":135,"props":237,"children":238},{},[239,241],{"type":14,"value":240},"no longer lists untagged images unless you pass ",{"type":9,"tag":40,"props":242,"children":244},{"className":243},[],[245],{"type":14,"value":246},"--all",{"type":14,"value":65},{"type":9,"tag":33,"props":249,"children":252},{"className":250,"code":251,"language":14},[36],"docker image ls              # tagged images, tree view\ndocker image ls --all        # including untagged intermediate layers\ndocker image ls --tree       # explicit, same as the default now\n",[253],{"type":9,"tag":40,"props":254,"children":255},{"__ignoreMap":42},[256],{"type":14,"value":251},{"type":9,"tag":10,"props":258,"children":259},{},[260,262,268,270,275],{"type":14,"value":261},"If you learned Docker before this and remember a flat table full of ",{"type":9,"tag":40,"props":263,"children":265},{"className":264},[],[266],{"type":14,"value":267},"\u003Cnone>",{"type":14,"value":269}," entries, that is what changed. The ",{"type":9,"tag":40,"props":271,"children":273},{"className":272},[],[274],{"type":14,"value":267},{"type":14,"value":276}," images did not go anywhere; they are just no longer the first thing you see.",{"data":278,"body":279},{},{"type":6,"children":280},[281,287,296,307],{"type":9,"tag":21,"props":282,"children":284},{"id":283},"building-and-tagging",[285],{"type":14,"value":286},"Building and tagging",{"type":9,"tag":33,"props":288,"children":291},{"className":289,"code":290,"language":14},[36],"docker build -t myapp:1.0 .\ndocker tag myapp:1.0 myapp:latest\ndocker tag myapp:1.0 ghcr.io/myorg/myapp:1.0\n",[292],{"type":9,"tag":40,"props":293,"children":294},{"__ignoreMap":42},[295],{"type":14,"value":290},{"type":9,"tag":10,"props":297,"children":298},{},[299,305],{"type":9,"tag":40,"props":300,"children":302},{"className":301},[],[303],{"type":14,"value":304},"docker tag",{"type":14,"value":306}," does not copy anything. It adds a second name pointing at the same image ID — like a hard link. Which is why re-tagging is instant regardless of image size, and why deleting one tag does not delete the image if another still points at it.",{"type":9,"tag":10,"props":308,"children":309},{},[310,312,318,320,325],{"type":14,"value":311},"To push, the tag must contain the destination registry. That is why the third line above exists: you cannot push ",{"type":9,"tag":40,"props":313,"children":315},{"className":314},[],[316],{"type":14,"value":317},"myapp:1.0",{"type":14,"value":319}," anywhere, because that name says Docker Hub's ",{"type":9,"tag":40,"props":321,"children":323},{"className":322},[],[324],{"type":14,"value":55},{"type":14,"value":326}," namespace, which you do not own.",{"data":328,"body":329},{},{"type":6,"children":330},[331],{"type":9,"tag":100,"props":332,"children":334},{":lines":333},"[{\"cmd\":\"docker login ghcr.io\",\"out\":\"Login Succeeded\"},{\"cmd\":\"docker push ghcr.io/myorg/myapp:1.0\",\"out\":\"The push refers to repository [ghcr.io/myorg/myapp]\\n1.0: digest: sha256:7c3e1a... size: 1163\"}]",[],{"data":336,"body":337},{},{"type":6,"children":338},[339],{"type":9,"tag":340,"props":341,"children":346},"fill-blank",{":answer":342,"hint":343,"placeholder":344,"prompt":345},"[\"docker tag myapp:1.0 ghcr.io/acme/myapp:1.0\",\"docker image tag myapp:1.0 ghcr.io/acme/myapp:1.0\"]","One command, source name then destination name.","docker tag ...","Give the existing image `myapp:1.0` a second name so it can be pushed to `ghcr.io/acme/myapp` as version `1.0`.",[],{"data":348,"body":349},{},{"type":6,"children":350},[351,357,362,508],{"type":9,"tag":21,"props":352,"children":354},{"id":353},"choosing-a-base-image",[355],{"type":14,"value":356},"Choosing a base image",{"type":9,"tag":10,"props":358,"children":359},{},[360],{"type":14,"value":361},"The base image decides most of your final size, most of your CVE count, and which debugging tools you have at 3am. The usual ladder:",{"type":9,"tag":363,"props":364,"children":365},"table",{},[366,390],{"type":9,"tag":367,"props":368,"children":369},"thead",{},[370],{"type":9,"tag":371,"props":372,"children":373},"tr",{},[374,380,385],{"type":9,"tag":375,"props":376,"children":377},"th",{},[378],{"type":14,"value":379},"Base",{"type":9,"tag":375,"props":381,"children":382},{},[383],{"type":14,"value":384},"Size",{"type":9,"tag":375,"props":386,"children":387},{},[388],{"type":14,"value":389},"Trade-off",{"type":9,"tag":391,"props":392,"children":393},"tbody",{},[394,425,447,469,487],{"type":9,"tag":371,"props":395,"children":396},{},[397,415,420],{"type":9,"tag":398,"props":399,"children":400},"td",{},[401,407,409],{"type":9,"tag":40,"props":402,"children":404},{"className":403},[],[405],{"type":14,"value":406},"ubuntu",{"type":14,"value":408},", ",{"type":9,"tag":40,"props":410,"children":412},{"className":411},[],[413],{"type":14,"value":414},"debian",{"type":9,"tag":398,"props":416,"children":417},{},[418],{"type":14,"value":419},"70–120 MB",{"type":9,"tag":398,"props":421,"children":422},{},[423],{"type":14,"value":424},"everything works, everything is present, largest surface",{"type":9,"tag":371,"props":426,"children":427},{},[428,437,442],{"type":9,"tag":398,"props":429,"children":430},{},[431],{"type":9,"tag":40,"props":432,"children":434},{"className":433},[],[435],{"type":14,"value":436},"*-slim",{"type":9,"tag":398,"props":438,"children":439},{},[440],{"type":14,"value":441},"25–80 MB",{"type":9,"tag":398,"props":443,"children":444},{},[445],{"type":14,"value":446},"same distro, docs and extras stripped",{"type":9,"tag":371,"props":448,"children":449},{},[450,459,464],{"type":9,"tag":398,"props":451,"children":452},{},[453],{"type":9,"tag":40,"props":454,"children":456},{"className":455},[],[457],{"type":14,"value":458},"alpine",{"type":9,"tag":398,"props":460,"children":461},{},[462],{"type":14,"value":463},"5–15 MB",{"type":9,"tag":398,"props":465,"children":466},{},[467],{"type":14,"value":468},"musl libc, not glibc — some binaries and wheels break",{"type":9,"tag":371,"props":470,"children":471},{},[472,477,482],{"type":9,"tag":398,"props":473,"children":474},{},[475],{"type":14,"value":476},"distroless",{"type":9,"tag":398,"props":478,"children":479},{},[480],{"type":14,"value":481},"2–20 MB",{"type":9,"tag":398,"props":483,"children":484},{},[485],{"type":14,"value":486},"no shell, no package manager; hard to debug, hard to exploit",{"type":9,"tag":371,"props":488,"children":489},{},[490,499,503],{"type":9,"tag":398,"props":491,"children":492},{},[493],{"type":9,"tag":40,"props":494,"children":496},{"className":495},[],[497],{"type":14,"value":498},"scratch",{"type":9,"tag":398,"props":500,"children":501},{},[502],{"type":14,"value":200},{"type":9,"tag":398,"props":504,"children":505},{},[506],{"type":14,"value":507},"nothing at all; static binaries only",{"type":9,"tag":10,"props":509,"children":510},{},[511,513,518],{"type":14,"value":512},"Alpine's catch is worth stating because it costs people an afternoon: it uses ",{"type":9,"tag":135,"props":514,"children":515},{},[516],{"type":14,"value":517},"musl",{"type":14,"value":519}," rather than glibc. Most things are fine; Python packages with compiled wheels, some Node native modules, and anything shipping a glibc-linked binary are not, and the failure is usually a confusing \"not found\" for a file that plainly exists.",{"data":521,"body":522},{},{"type":6,"children":523},[524],{"type":9,"tag":198,"props":525,"children":529},{":answer":200,":options":526,"explanation":527,"question":528},"[\"Alpine uses musl libc rather than glibc, so prebuilt wheels compiled against glibc don't load\",\"Alpine ships an older Python\",\"Alpine images cannot run Python at all\"]","The wheels are built for `manylinux`, which means glibc. On Alpine, pip falls back to building from source — so it either needs a compiler toolchain in the image or fails outright. `python:3.13-slim` is usually the better small base for Python.","A Python app runs on `python:3.13` but crashes on `python:3.13-alpine` with errors about missing shared libraries. Why?",[],{"data":531,"body":532},{},{"type":6,"children":533},[534,654],{"type":9,"tag":535,"props":536,"children":538},"deep-dive",{"title":537},"Rate limits, mirrors, and why your CI started failing",[539,544,557,562,633],{"type":9,"tag":10,"props":540,"children":541},{},[542],{"type":14,"value":543},"Docker Hub applies pull-rate limits to anonymous and free accounts, counted per IP. Which is fine on a laptop and not fine on shared CI, where every job on the runner shares one address and the pool exhausts before lunch.",{"type":9,"tag":10,"props":545,"children":546},{},[547,549,555],{"type":14,"value":548},"Symptoms: ",{"type":9,"tag":40,"props":550,"children":552},{"className":551},[],[553],{"type":14,"value":554},"toomanyrequests: You have reached your pull rate limit",{"type":14,"value":556},", usually appearing suddenly on a pipeline that was fine last week because the team grew.",{"type":9,"tag":10,"props":558,"children":559},{},[560],{"type":14,"value":561},"The fixes, in ascending order of effort:",{"type":9,"tag":563,"props":564,"children":565},"ul",{},[566,577,615],{"type":9,"tag":567,"props":568,"children":569},"li",{},[570,575],{"type":9,"tag":135,"props":571,"children":572},{},[573],{"type":14,"value":574},"Authenticate in CI.",{"type":14,"value":576}," A logged-in pull counts against your account, not the shared IP.",{"type":9,"tag":567,"props":578,"children":579},{},[580,585,587,593,594,600,601,607,608,614],{"type":9,"tag":135,"props":581,"children":582},{},[583],{"type":14,"value":584},"Pull from a different registry.",{"type":14,"value":586}," Many official images are mirrored — ",{"type":9,"tag":40,"props":588,"children":590},{"className":589},[],[591],{"type":14,"value":592},"mcr.microsoft.com",{"type":14,"value":408},{"type":9,"tag":40,"props":595,"children":597},{"className":596},[],[598],{"type":14,"value":599},"public.ecr.aws",{"type":14,"value":408},{"type":9,"tag":40,"props":602,"children":604},{"className":603},[],[605],{"type":14,"value":606},"quay.io",{"type":14,"value":408},{"type":9,"tag":40,"props":609,"children":611},{"className":610},[],[612],{"type":14,"value":613},"ghcr.io",{"type":14,"value":65},{"type":9,"tag":567,"props":616,"children":617},{},[618,623,625,631],{"type":9,"tag":135,"props":619,"children":620},{},[621],{"type":14,"value":622},"Run a pull-through cache.",{"type":14,"value":624}," The ",{"type":9,"tag":40,"props":626,"children":628},{"className":627},[],[629],{"type":14,"value":630},"registry:3",{"type":14,"value":632}," image can proxy Docker Hub, so your builders pull once and serve locally after that.",{"type":9,"tag":10,"props":634,"children":635},{},[636,638,644,646,652],{"type":14,"value":637},"The daemon also takes a ",{"type":9,"tag":40,"props":639,"children":641},{"className":640},[],[642],{"type":14,"value":643},"registry-mirrors",{"type":14,"value":645}," setting in ",{"type":9,"tag":40,"props":647,"children":649},{"className":648},[],[650],{"type":14,"value":651},"/etc/docker/daemon.json",{"type":14,"value":653},", which redirects Hub pulls without touching a single image name in your Dockerfiles.",{"type":9,"tag":10,"props":655,"children":656},{},[657],{"type":14,"value":658},"Next up: writing a Dockerfile — turning your own code into one of these things.",1787908867620]