Skip to content

Versioning, Immutability, and Retention

An artifact registry is only as trustworthy as the guarantee that a given reference always means the same bytes. This page covers what makes that guarantee hold — tags vs. digests, tag immutability, and retention policies — and what breaks when it doesn't.

TL;DR — in 30 seconds:

  • A tag (v1.2.3, latest) is a human-friendly, mutable-by-default pointer; a digest (a content hash) is the artifact's real, permanent identity — the same digest always means the same bytes.
  • Tag immutability (a setting most registries support) rejects any push that would overwrite an existing tag — turning a tag back into a reliable, fixed reference.
  • Retention policies (age- or count-based) keep a registry from growing forever and purge old, unpatched images — without them, storage cost and unpatched-image sprawl both grow unchecked.

1. Tags vs. digests

A digest (e.g. sha256:abcd…) is computed from the artifact's actual content — it's the one identifier that's cryptographically guaranteed to mean exactly one set of bytes, forever. A tag is just a label someone attached to a digest at push time — nothing stops someone from later pushing a different digest under the same tag, silently changing what that tag points to.

Tag (v1.2.3, latest) Digest (sha256:...)
Human-readable Yes No
Fixed forever Only if immutability is enabled Always
Can be reused/overwritten By default, yes Never — it's the content hash

latest is the extreme case: by convention it's re-pushed on every build, so it's expected to keep changing — which is exactly why anything actually deployed should reference a specific version tag (or better, a digest), never latest.

2. Tag immutability

Enabling tag immutability (a per-repository setting in GHCR, ECR, Nexus, and Artifactory) makes the registry reject any push attempting to overwrite an existing tag. Once turned on, v1.2.3 means exactly one thing for the life of that repository — the guarantee "this tag = this artifact" stops being a convention and becomes an enforced rule.

Without it, two real risks appear:

  • A compromised or buggy pipeline re-pushes a tag with different, untested content, and every consumer that trusted the tag now silently runs something else.
  • A rollback that re-deploys "the same tag" may not actually restore the artifact that was running before, if that tag was overwritten in between.

3. Retention and lifecycle policies

A registry with no retention policy grows without bound: every build, forever, stays stored. That costs money (storage, and often data-transfer) and creates a security liability — old images with long-since- patched vulnerabilities remain pullable indefinitely, and nothing stops someone from pulling one by accident.

A retention (lifecycle) policy automates cleanup with rules like:

  • Keep only the last N images per repository.
  • Purge anything older than X days that isn't currently referenced by a running deployment.
  • Always keep anything tagged as a release (e.g. matching a semver pattern), purge untagged/dangling images more aggressively.

The one rule every policy needs regardless of the specifics: never purge something a live deployment still references — a retention policy should track what's actually in use, not just apply a blind age cutoff.

flowchart LR
    A["Every build pushed"] --> B{"Retention policy"}
    B -->|"referenced by a live deploy<br/>or matches a keep rule"| C["Kept"]
    B -->|"old / unreferenced /<br/>past the cutoff"| D["Purged"]

Common gotchas

  • Deploying anything against a mutable tag like latest. Fix: deploy against a specific version tag (immutable, if the registry supports it) or a digest — never a tag that's expected to keep moving.
  • Setting a retention policy purely on age, with no "referenced by a live deployment" exception. Fix: always exempt anything a running deployment currently points to, regardless of age.
  • Assuming immutability is on by default. Fix: it's opt-in configuration on every registry that supports it — check and enable it explicitly for anything you actually deploy from.
Check yourself — a rollback re-deploys 'the same tag' that was running last week, but the app that comes up behaves differently. What's the first thing to check?

Whether that tag was overwritten in between — without tag immutability, a later push can silently repoint a tag at different content, so "the same tag" no longer means "the same artifact." Checking the digest the tag currently resolves to (and comparing it against what was actually running last week) is the way to confirm.

Done when: you can explain the difference between a tag and a digest, and state, in one sentence, what tag immutability actually prevents.