{"id":"l-dk-2-3","courseId":"docker-kubernetes","lang":"en","kind":"lesson","summary":"Name, version and publish notes-api images so every deploy is traceable, pin images by digest, and build for both amd64 and arm64 so the cluster can run what your laptop built.","html":"<p>At 11 p.m. on a release night, two Pods of the same service gave different answers to the same request. Both were running <code dir=\"ltr\">api:latest</code>. Someone had re-pushed <code dir=\"ltr\">latest</code> in the afternoon, and one node had pulled the new image while the other was still using its cached copy. We spent an hour proving the code was fine before anyone looked at the image. Names are part of your deployment, so make them precise.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Anatomy of an image reference</h2>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"text\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">Text</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code>ghcr.io/skylane/notes-api:1.4.0@sha256:3f1c9e…\n└──┬──┘ └──┬──┘ └───┬───┘ └─┬─┘ └─────┬─────┘\nregistry  namespace  repo    tag      digest</code></pre></div>\n<ul>\n<li>The <strong>registry</strong> is the server that stores images: Docker Hub (the default when you leave it out), GitHub Container Registry (<code dir=\"ltr\">ghcr.io</code>), Amazon ECR, Google Artifact Registry and so on.</li>\n<li>The <strong>tag</strong> is a human-friendly label. It’s a pointer, and pointers can move.</li>\n<li>The <strong>digest</strong> is a SHA-256 hash of the image manifest (or, for a multi-platform image, of its image index), which in turn lists the hash of every layer. Change one byte anywhere and the digest changes. It can’t be moved.</li>\n</ul>\n<p><code dir=\"ltr\">notes-api:latest</code> isn’t special. <code dir=\"ltr\">latest</code> is just the tag Docker uses when you don’t specify one. It doesn’t mean “newest”, and nothing updates it unless someone pushes it.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">A tagging scheme that holds up</h2>\n<p>For each build that might be deployed, push two tags for the same image:</p>\n<ul>\n<li>A <strong>version</strong> people can talk about: <code dir=\"ltr\">1.4.0</code>.</li>\n<li>The <strong>commit</strong> it came from: <code dir=\"ltr\">sha-9f2c1ab</code>.</li>\n</ul>\n<p>When someone reports a bug in “1.4.0”, the SHA tag takes you straight to the code. And the rule that prevents the release-night incident: <strong>never push different content under a tag that’s already deployed.</strong> If you fix something, that’s <code dir=\"ltr\">1.4.1</code>. Many registries can enforce this; Amazon ECR, for example, has a tag immutability setting that rejects a push to an existing tag.</p>\n<p>Build once, promote many times. The image that passes staging should be the exact image that reaches production, identified by the same digest. Don’t rebuild from the same commit for production “to be safe”: a rebuild can pull a newer base image or a different transitive dependency, and then production runs something no one tested. Promotion is a change to which digest each environment’s manifest points at, not a new build.</p>\n<p>Keep the registry tidy too. Retention rules that delete untagged images and old SHA tags after a few months keep storage costs down, but exclude anything a manifest still references, or your next rollback will fail on a missing image.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">Publish to a registry</h2>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">Terminal</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">echo</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> \"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">$GHCR_TOKEN</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> docker</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> login</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> ghcr.io</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> -u</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> amara-skylane</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --password-stdin</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">GIT_SHA</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">$(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">git</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> rev-parse</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --short</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> HEAD</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">docker</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> buildx</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> build</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> \\</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  --platform</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> linux/amd64,linux/arm64</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> \\</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  -t</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> ghcr.io/skylane/notes-api:1.4.0</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> \\</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  -t</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> ghcr.io/skylane/notes-api:sha-</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">$GIT_SHA </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">\\</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  --push</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> .</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">docker</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> buildx</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> imagetools</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> inspect</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> ghcr.io/skylane/notes-api:1.4.0</span></span></code></pre></div>\n<p><code dir=\"ltr\">--password-stdin</code> keeps the token out of your shell history. In CI, use the platform’s short-lived token rather than a personal one. The last command prints the image’s digest and the platforms it contains.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">Multi-platform images</h2>\n<p>Your laptop may be arm64 (Apple Silicon) while the cluster’s nodes are amd64, or the reverse. An image built for one CPU architecture fails on the other with the terse <code dir=\"ltr\">exec format error</code>. A multi-platform build fixes that: <code dir=\"ltr\">--platform linux/amd64,linux/arm64</code> builds both variants and pushes an <strong>image index</strong>, a small list that maps each platform to its own manifest. When a node pulls <code dir=\"ltr\">notes-api:1.4.0</code>, the runtime picks the manifest for its own architecture.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 700 250\" role=\"img\" aria-labelledby=\"l-dk-2-3-t1\"><defs><marker id=\"arrow-l-dk-2-3-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-dk-2-3-t1\">The tag 1.4.0 points to an image index digest. The index lists an amd64 manifest and an arm64 manifest, each listing its own layers.</title>\n  <rect class=\"d-box-warn\" x=\"20\" y=\"100\" width=\"130\" height=\"50\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"85\" y=\"130\" text-anchor=\"middle\">tag 1.4.0</text>\n  <path class=\"d-arrow\" d=\"M150 125 L208 125\" marker-end=\"url(#arrow-l-dk-2-3-1)\"/>\n  <rect class=\"d-box-primary\" x=\"210\" y=\"90\" width=\"170\" height=\"70\" rx=\"10\"/>\n  <text class=\"d-label-strong\" x=\"295\" y=\"118\" text-anchor=\"middle\">image index</text>\n  <text class=\"d-code\" x=\"295\" y=\"142\" text-anchor=\"middle\">sha256:3f1c…</text>\n  <path class=\"d-arrow\" d=\"M380 110 L458 62\" marker-end=\"url(#arrow-l-dk-2-3-1)\"/>\n  <path class=\"d-arrow\" d=\"M380 140 L458 188\" marker-end=\"url(#arrow-l-dk-2-3-1)\"/>\n  <rect class=\"d-box-accent\" x=\"460\" y=\"30\" width=\"220\" height=\"64\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"570\" y=\"56\" text-anchor=\"middle\">linux/amd64 manifest</text>\n  <text class=\"d-label-muted\" x=\"570\" y=\"80\" text-anchor=\"middle\">layers for x86 nodes</text>\n  <rect class=\"d-box-accent\" x=\"460\" y=\"156\" width=\"220\" height=\"64\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"570\" y=\"182\" text-anchor=\"middle\">linux/arm64 manifest</text>\n  <text class=\"d-label-muted\" x=\"570\" y=\"206\" text-anchor=\"middle\">layers for ARM nodes</text>\n  <text class=\"d-label-muted\" x=\"85\" y=\"175\" text-anchor=\"middle\">movable</text>\n  <text class=\"d-label-muted\" x=\"295\" y=\"185\" text-anchor=\"middle\">fixed</text>\n</svg><figcaption>One tag points to an index; each node pulls the manifest for its CPU</figcaption></figure>\n<p>Building for a foreign architecture runs under emulation, which can be slow for heavy <code dir=\"ltr\">npm ci</code> steps. CI runners with native arm64 and amd64 machines are faster when builds grow. To keep multi-platform images locally, Docker needs the containerd image store, which is the default on new Docker Desktop installs; check that it’s turned on in older setups.</p>\n<h2 id=\"sec-5\" tabindex=\"-1\">Pin by digest</h2>\n<p>A digest guarantees every node runs identical bytes, whatever happens to the tag later. You can deploy with both, and the digest wins:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"yaml\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">YAML</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">image</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">ghcr.io/skylane/notes-api:1.4.0@sha256:3f1c9e…</span></span></code></pre></div>\n<p>The tag stays in the reference for humans; the runtime pulls by digest. Do the same for base images in your Dockerfile, so a rebuild next month doesn’t silently pick up a different <code dir=\"ltr\">node:24-slim</code>:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"dockerfile\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">Dockerfile</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">FROM</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> node:24-slim@sha256:&#x3C;digest-from-imagetools-inspect> </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> build</span></span></code></pre></div>\n<p>Pinning trades automatic patches for predictability, so pair it with a bot such as Dependabot or Renovate that opens a pull request when a new base digest is published. You get updates, reviewed and tested, instead of surprises.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Pinning and forgetting</span></p>\n<p>A digest-pinned base never gets security fixes on its own. I’ve seen pinned images run for a year on a base with a known critical CVE because nobody owned the update. Pin <em>and</em> automate the bump, or don’t pin.</p>\n</aside>\n<p>Your image is now named, versioned and published. Before anyone deploys it, you should know what’s inside it, which is the next lesson.</p>\n","toc":[{"id":"sec-1","text":"Anatomy of an image reference","level":2},{"id":"sec-2","text":"A tagging scheme that holds up","level":2},{"id":"sec-3","text":"Publish to a registry","level":2},{"id":"sec-4","text":"Multi-platform images","level":2},{"id":"sec-5","text":"Pin by digest","level":2}],"takeaways":["An image reference is <code dir=\"ltr\">registry/namespace/repository:tag@digest</code>; the tag is a movable label, the digest is the content’s fingerprint.","<code dir=\"ltr\">latest</code> is only the default tag name, not the newest build; deploy explicit versions such as <code dir=\"ltr\">1.4.0</code> or a Git SHA.","Never push different content under a tag that’s already deployed; enable tag immutability where your registry supports it.","Pinning by digest (<code dir=\"ltr\">@sha256:…</code>) guarantees every node runs the same bytes, for your app and for base images.","Build multi-platform images with <code dir=\"ltr\">docker buildx build --platform linux/amd64,linux/arm64</code> when developers and servers use different CPUs."],"further":[{"title":"docker image tag","url":"https://docs.docker.com/reference/cli/docker/image/tag/"},{"title":"Multi-platform builds","url":"https://docs.docker.com/build/building/multi-platform/"},{"title":"Images in Kubernetes","url":"https://kubernetes.io/docs/concepts/containers/images/"}],"quiz":[{"q":"<p>Two Pods of the same Deployment, both using <code dir=\"ltr\">notes-api:latest</code>, behave differently. What’s the most likely explanation?</p>","options":[{"text":"Kubernetes randomly picks between cached image versions.","why":"Nothing random happens. Each node pulled whatever the tag pointed to at the time it pulled."},{"text":"<code dir=\"ltr\">latest</code> always resolves to the newest image on every container start.","why":"It resolves to whatever the tag points to when the node pulls. A node with a cached copy may never pull again."},{"text":"The tag was re-pushed with new content, and the two nodes pulled it at different times.","why":"Correct. Tags are mutable, so the same name meant different bytes on different nodes. Use versioned tags or digests."},{"text":"One of the Pods is running a corrupted image.","why":"Registries verify layers by digest, so corruption fails the pull instead of running quietly."}],"answer":2},{"q":"<p>What does a digest such as <code dir=\"ltr\">sha256:3f1c…</code> identify?</p>","options":[{"text":"The date and time the image was built.","why":"Build time is metadata inside the image config, not what the digest is."},{"text":"The exact image content; any change to the image produces a different digest.","why":"Correct. It’s a hash of the manifest, which in turn lists layer hashes. Same digest, same bytes, everywhere."},{"text":"The Git commit the image was built from.","why":"A Git SHA is a fine <em>tag</em>, but the digest is computed from the image itself."},{"text":"The registry account that pushed the image.","why":"The digest is independent of who pushed it or where it’s stored."}],"answer":1},{"q":"<p>A developer builds notes-api on an Apple Silicon laptop and pushes it. On the amd64 cluster the Pod logs <code dir=\"ltr\">exec format error</code>. What’s the fix?</p>","options":[{"text":"Add <code dir=\"ltr\">EXPOSE</code> for the cluster’s architecture.","why":"<code dir=\"ltr\">EXPOSE</code> documents ports; it can’t change the CPU architecture of the binaries."},{"text":"Switch the base image to Alpine.","why":"Alpine images also come per architecture. The image built on the laptop is still arm64."},{"text":"Set <code dir=\"ltr\">imagePullPolicy</code> to <code dir=\"ltr\">Always</code> on the Deployment.","why":"Pulling again fetches the same arm64-only image."},{"text":"Build with <code dir=\"ltr\">docker buildx build --platform linux/amd64,linux/arm64</code> (ideally in CI) so the image index contains both.","why":"Correct. The laptop built only arm64. A multi-platform build pushes one tag whose index points at a manifest per architecture."}],"answer":3},{"q":"<p>Which tagging scheme gives the best traceability for notes-api releases?</p>","options":[{"text":"A semantic version plus a Git SHA tag, for example <code dir=\"ltr\">1.4.0</code> and <code dir=\"ltr\">sha-9f2c1ab</code>, pushed for the same build.","why":"Correct. The version is what people talk about; the SHA tells you exactly which commit to read when something breaks."},{"text":"Only <code dir=\"ltr\">latest</code>, re-pushed on every merge to <code dir=\"ltr\">main</code>.","why":"You lose any way to tell which build is running, or to roll back to a specific one by name."},{"text":"The build date, such as <code dir=\"ltr\">2026-10-05</code>.","why":"Two builds on one day collide, and a date doesn’t point to the code that produced it."},{"text":"The developer’s name, such as <code dir=\"ltr\">amara-test</code>.","why":"Fine for a throwaway experiment, useless for knowing what runs in production."}],"answer":0}],"exercise":{"kind":"order","items":["build","test","push","digest","manifest","apply","verify"],"title":"Ship release 1.4.0","prompt":"<p>Put the release steps for notes-api 1.4.0 in order, from source code to a verified deployment that every node runs byte-for-byte.</p>\n","hints":["You can’t test an image that doesn’t exist, and you can’t read a digest the registry hasn’t seen.","The manifest should reference the digest, so the digest has to be known before you edit it.","Verification comes last, after the cluster has the new spec."],"explanation":"<p>Build the image, test it, push it with its version and SHA tags, read the digest the registry assigned, write that digest into the Deployment manifest, apply it, then verify the rollout. Testing before pushing keeps broken builds out of the registry, and deploying by digest means what you tested is exactly what runs.</p>\n","checkLabels":{},"itemText":{"build":"<code dir=\"ltr\">docker buildx build --platform linux/amd64,linux/arm64 -t ghcr.io/skylane/notes-api:1.4.0 -t ghcr.io/skylane/notes-api:sha-9f2c1ab .</code>","test":"Run the test suite and a scan against the built image","push":"Push both tags to <code dir=\"ltr\">ghcr.io</code>","digest":"<code dir=\"ltr\">docker buildx imagetools inspect ghcr.io/skylane/notes-api:1.4.0</code> to read the digest","manifest":"Set <code dir=\"ltr\">image: ghcr.io/skylane/notes-api:1.4.0@sha256:…</code> in the Deployment manifest","apply":"<code dir=\"ltr\">kubectl apply -f k8s/</code>","verify":"<code dir=\"ltr\">kubectl rollout status deployment/notes-api</code>"}},"words":750,"readMinutes":4}