Container image release notes: tags, digests, and CVE rebuilds

A container image is the most opaque artifact most teams ship. Your users never see the code, never run the build, and usually never read the Dockerfile β€” they run docker pull and deploy whatever comes back, frequently into production, frequently via automation that nobody watches. That makes image release notes less like app-store copy and more like firmware notes: you are shipping updates to systems you don’t control, to people who will only find out what changed when something behaves differently at 3am.

Container publishing also has two properties that make the usual changelog habits actively misleading: tags are mutable pointers, and the artifact changes even when your code doesn’t. Both need to show up in how you write and publish notes β€” and neither is covered by pointing at your git history.

Tags are pointers, not versions

A git tag names a commit forever. A container tag is a label you can silently move to a different image any time you push. That difference drives everything:

  • Rolling tags (latest, 1, 1.4) are a subscription: pulling them tomorrow gets a different artifact than today. Useful β€” but only if there’s a changelog explaining what the subscription delivered.
  • Immutable version tags (1.4.2) are a promise you have to keep by policy, because the registry won’t enforce it for you. Re-pushing 1.4.2 with different contents is a silent re-release β€” the same trust failure the hotfix guide warns about, except in containers it’s endemic because nothing stops you.
  • Digests (sha256:…) are the only immutable name an image has. Cautious consumers pin them; automated updaters bump them. Your release notes are what tells a digest-pinned consumer whether moving to the new digest is worth it.

Write your tag policy down once, on the same page as the changelog: which tags roll, which are immutable, whether you ever re-push, and what latest actually tracks (stable? edge? the last thing CI built?). Every fifth issue on a popular image’s tracker is someone discovering the answer the hard way. If you do have to replace a version tag β€” a botched build, a broken manifest β€” treat it like a hotfix: same-version re-push plus a dated entry saying the digest changed and why, never a quiet swap.

The release where your code didn’t change

Base images ship CVE fixes constantly, so responsible publishers rebuild on a schedule: same application code, new base layers, new digest. This is a release. It changes what people run in production, it changes what their vulnerability scanner says, and it deserves an entry β€” even a one-liner:

2026-07-30 β€” Rebuild on alpine 3.20.3
No application changes. Picks up base-image fixes for CVE-2026-31742
and CVE-2026-30198 (both in libcrypto; neither reachable from the
default configuration, patched anyway). All 1.4.x tags re-pointed;
digests updated below.

Two audiences need this. Scanner-driven users see Trivy or Grype flag your image and want to know whether a fixed build exists before they file the issue β€” the entry is the deflection. Change-averse users see a new digest appear with no release in your git repo and want to know why; “rebuild, no app changes” is exactly the reassurance that lets them wave it through. Vague variants (“security rebuild” with no CVE list) fail both audiences the same way “security improvements” fails everywhere else β€” the security advisory guide covers why precision about impact beats vagueness even when you keep the mechanism quiet.

What actually breaks image consumers

The image’s interface is not its API β€” it’s everything a deployment manifest, healthcheck, or debug session touches. These are the changes that must be flagged as breaking (the breaking-changes playbook applies in full), and most of them never look breaking in a code diff:

  • Default user changes. Root to non-root is the classic: better for everyone, and it breaks every deployment writing to a path the new user can’t. Announce it a release ahead, with the exact runAsUser/volume-permission fix.
  • Entrypoint and command changes. Anything that overrides command: or wraps the entrypoint in an init system breaks quietly.
  • Base distro switches. Alpine to distroless removes the shell β€” and with it every kubectl exec debug session and every shell-based healthcheck. Musl/glibc moves change what native extensions load. A distro switch is always a headline entry, never a bullet.
  • Env var defaults, exposed ports, volume and config paths. The things manifests reference by name. Renaming a config mount point is an API removal in every practical sense.
  • Dropped architectures. Removing arm64 from the manifest list turns someone’s entire cluster into exec format error. Dropping an arch needs the full deprecation treatment; adding one is a feature worth announcing.
  • Size jumps. Not breaking, but a 40MB image becoming 400MB changes pull times, node disk pressure, and cold-start latency. State the delta and the reason.

One release, many tags: the variant matrix

Real images ship variants β€” 1.4.2, 1.4.2-slim, 1.4.2-alpine, 1.4.2-bookworm, each a multi-arch manifest. Readers holding one variant need to know two things your git history won’t tell them: which variants this entry applies to, and what each tag resolves to now. The fix is mechanical: write one entry per release, scope variant-specific notes inline (“alpine variant only: musl bumped to 1.2.5”), and close the entry with the tag→digest map for the release. That map is also your silent-re-push insurance: it turns “did this tag move?” from archaeology into a text search. If the variant list itself changes β€” a new slim build, bookworm retired β€” that’s an entry of its own, with dates, per the sunset playbook.

Where the notes live (registries won’t help you)

Here’s the uncomfortable survey: no major registry has a changelog surface. Docker Hub gives you one mutable description blob for the whole repository β€” no per-release anything. GHCR renders even less. Quay, ECR Public: the same. So the two-layer pattern that solves app stores solves registries too, except the store layer here is nearly empty:

  • A canonical changelog page with dated entries and stable permalinks, linked prominently from the registry description. This is the only layer you fully control.
  • OCI labels that point home. Bake org.opencontainers.image.url, .source, .version and .revision into every build, and point .url (or a dedicated label) at the changelog. Labels travel inside the artifact β€” anyone holding the image can docker inspect their way to your notes, which no registry description can promise.
  • GitHub Releases, if your source is public β€” not for humans, for robots: Renovate embeds release notes into the bump PRs it opens, which puts your words in front of the person clicking merge. The open-source guide covers keeping Releases mirrored from a canonical source instead of hand-maintained twice.
  • A machine-readable feed. Fleet operators who watch vendor images (the discipline in vendor changelog monitoring) need RSS/JSON, not a description blob β€” see machine-readable changelogs for the structural rules that make a feed automatable.

Write for the automated updater

A large share of your “readers” are Renovate, Dependabot, and Watchtower-style auto-pullers. The humans appear at exactly one moment: the bump PR. That entry gets the same 30-second read as a library dependency bump, so the same rules apply β€” verdict first (“safe to bump: rebuild only, no behavior changes” or “Breaking: container now runs as UID 10001 β€” see migration”), symptoms not internals, migration steps inline. Watchtower users never see a PR at all: for them, your changelog is the only warning system that exists, which is one more reason breaking changes in images deserve a release of deprecation lead time, not a morning surprise.

Anti-patterns

  • Latest-only publishing. One rolling tag, no version tags, no history. Consumers can’t roll back, can’t pin, can’t tell what changed β€” the image equivalent of editing prod in place.
  • The silent tag move. Re-pushing an existing version tag with different contents and telling nobody. Detected eventually, trusted never again.
  • Notes only in the source repo. Image users pull from a registry; many never visit the repo and some can’t find it (that’s what the .source label is for). Meet them where the artifact is.
  • “Security rebuild” with no CVE list. Unverifiable reassurance. Scanners name the CVEs; your entry should too.
  • Dropping an architecture or variant silently. The people it breaks are by definition the ones not reading your Slack.
  • Variant soup. Six suffixes, no table saying how they differ or which one is recommended. Every variant you publish is a support surface; document the matrix or shrink it.

Where Wakelog fits

Wakelog is a good home for the canonical layer. The publish step is one curl, so the same CI job that pushes the image posts the entry β€” digest map and all β€” the pattern from the CI automation guide. Scheduled CVE rebuilds can post with the security tag so operators can filter for exactly those; every entry gets a stable permalink you can bake into an OCI label; and the page ships RSS and JSON feeds plus a list API for the fleet operators and scripts watching you. Honest caveats: Wakelog never inspects your images β€” it won’t notice a digest change or diff layers for you (pair it with your registry’s webhook or a scanner for detection); it publishes what your pipeline tells it, nothing more.

Start your image changelog — free   Next: IaC module changelogs →

Related guides

Last updated 2026-07-31 Β· All guides