Chapter 02 · Dossier 005 · operations

The Mirror

A pull-through OCI registry, taken apart.

What the deployed version guarantees, the retention behaviour that evicts digest-pinned content, and how each lesson translates to production. Every claim carries an evidence label — and the corrections to my own first draft are on the page, not quietly edited out.

Registry
zot v2.1.17
Upstreams
5, on-demand
Platform
Talos / containerd
Open tests
6
Registry
zot v2.1.17 (digest-pinned image; latest upstream release at last check: v2.1.20)
Platform
Talos Linux v1.13.4, containerd 2.2.4, single-node Kubernetes
Upstreams
5 (Docker Hub, GHCR, Quay, registry.k8s.io, NGC), all on-demand pull-through
Storage
50Gi local PVC; dedupe on, GC on (1h delay / 24h interval); no retention config
Auth state
anonymous read enabled; per-node pull credentials staged, not yet enforced
Fallback
origin fallback ON (platform default; deliberately not disabled)
Reviewed
2026-08-25: live cluster configuration read; upstream docs and v2.1.17 source checked; live runtime tests listed separately in the open register
Open items
pull-aware digest-entry retention (needs v2.1.19+; blanket alternatives are capacity-heavy), auth canary negative test, upstream namespace prefixes, disconnected-serve drill

Evidence labels used below: lab reproduced here with a dated receipt · config present in the inspected config · source established from the deployed version's source · docs stated in version-appropriate documentation · reported described in an unresolved upstream issue · proposed not deployed · open not yet run

The build that "hung"

An armoured way-station machine exploded into parts: hexagonal hull, roof plate, a shelf of glowing bricks, intake pods and an output nozzle - the mirror as a physical machine.
The mirror as a machine: upstream intakes, one cache shelf, one serving nozzle.

I added zot after a CI pull hit Docker Hub's anonymous rate limit and the retry wrapper hid the 429. At the time of the incident, this site's build-stage pull was anonymous.

Docker Hub does not go silent when it limits you. It answers with HTTP 429 and the error code toomanyrequests. Under the current published limits, unauthenticated users get 100 pulls per six hours per IPv4 address (or IPv6 /64); a single-platform image counts as one pull, a multi-architecture image counts once per architecture pulled, version checks do not count, and a HEAD request can read the rate-limit headers without consuming a pull. A separate abuse limiter covers all request types with its own 429 form. The silence in this incident was manufactured on my side, by retries swallowing the answer.

The swallower, recovered from the build script's git history, was a double retry stack: an outer shell wrapper allowing four attempts, wrapped around buildah build --retry 3. Buildah's flag counts retries - one initial try plus up to three more, applying to registry push/pull operations. Four outer attempts, each containing up to four inner attempts: up to sixteen requests for one repeatedly failing registry operation (a build with several pulls can produce more), each billed against the limit it was trying to outlast. Retry amplification turns a throttle into an outage and hides the evidence while doing it.

Receipt · the retry stack, as committed to the repo
retry() {
  local n=0 max=4
  until "$@"; do
    n=$((n+1)); [ "$n" -ge "$max" ] && { echo ">> failed after ${max} attempts" >&2; return 1; }
    echo ">> attempt ${n} failed, retrying in $((n*8))s..." >&2; sleep $((n*8))
  done
}
retry buildah build --retry 3 --retry-delay 5s ...
# --retry 3 = one attempt + three retries; the wrapper multiplies that by four.
# A 429 answered every one of them, and the backoffs read as a hang.

Pinning the base image by digest stopped the tag re-resolution but not the round-trip: an ephemeral runner with a cold store still fetches the manifest behind that digest every build. The repeated origin pulls justified a shared cache. This page takes it apart, including the parts that turned out not to work the way I first believed.

FIELD NOTE
The mirror did not fix the red X in CI. That red was a separate upstream bug in the CI system's log-finalise step - cosmetic, tolerated, documented. Two problems, one symptom; diagnose them separately.

What the mirror actually does

The mirror is zot: one registry, one 50Gi cache volume, digest-pinned like everything it serves. Five public registries front it in on-demand pull-through mode - a miss fetches and caches, a hit serves from the shelf. zot also offers polled mirroring and pre-seeding; Docker Hub is on-demand only, per its documentation.

The storage block, from the live config:

"storage": {
  "rootDirectory": "/var/lib/registry",
  "commit": true,
  "dedupe": true,
  "gc": true,
  "gcDelay": "1h",
  "gcInterval": "24h"
}

commit asks zot to commit writes to disk immediately instead of relying on buffered flushing; it narrows the buffered-write window, while end-to-end power-loss durability still depends on the filesystem, volume and disk. dedupe uses hard links on local filesystem storage (remote backends implement it differently), saving capacity when five upstreams ship the same base layers under different names - at the cost of a startup reconciliation pass that matters operationally when toggled on existing data. And gc with its two timers looks innocuous here; section 06 is about why it is the most consequential block on this page.

Diagram A · system context System context: consumers, the mirror, its storage, five upstreams, and the trust boundaries between them CI and Kubernetes nodes pull from the zot mirror over the LAN. The mirror stores content on a 50Gi volume and syncs on demand from five upstream registries across the internet boundary. Anonymous read is currently allowed inbound; outbound upstream credentials live in one mounted secret. The origin-fallback path from nodes directly to upstreams is shown dashed. CI (buildah)bastion host k8s nodescontainerd + local store zot v2.1.17anon read (today)sync: onDemand x5 50Gi PVCdedupe + GC 1h/24h docker.io ghcr.io quay.io registry.k8s.io nvcr.io origin fallback (dashed = only when the mirror cannot serve) internet boundary - outbound creds in ONE mounted secret LAN - anonymous read today; per-node creds staged

solid = normal pull path · dashed cyan = on-demand sync on miss · dashed magenta = origin fallback

One cache, two consumer classes; the two credential domains never mix.

Consumers point at it via the platform's machine-level registry config:

machine:
  registries:
    mirrors:
      docker.io:
        endpoints:
          - https://zot.bztmon.org
      ghcr.io:
        endpoints:
          - https://zot.bztmon.org

The mirror is the only listed endpoint, yet not a hard dependency: Talos tries endpoints in order "and by default the last implicit endpoint is the original upstream registry", unless skipFallback: true. An early version listed the upstream explicitly as endpoint two; it was removed as redundant - origin fallback here is a Talos default, not an endpoint this project maintains.

One asymmetry decides section 07's rollout order. Talos's v1alpha1 machine-config reference states for registry auth: "changes to the registry auth will not be picked up by the CRI containerd plugin without a reboot" - matching what this lab saw on v1.13.4. Mirror endpoint changes applied live here (same versions; the docs are silent on that half, so it stays a lab observation, not a cross-version guarantee). Secrets the registry consumes from a mount rotate with a pod restart. Put each credential on the side that can move.

One pod, one PVC, one node is an accepted failure domain here, recorded as such; section 09 covers what changes when the requirements do.

Request routing and five upstreams

Five registries feed one endpoint: when a node asks the mirror for pause:3.10, how does it know which origin that means? Three facts:

  1. containerd names the origin - mirror requests carry it as a query parameter: /v2/pause/manifests/3.10?ns=registry.k8s.io (documented).
  2. zot v2.1.17 ignores it - no ns handling exists in the deployed request path; using it for upstream selection is open feature request zot 4187. The URL path alone selects the local repository.
  3. This deployment has no per-upstream prefixes - the live sync config gives all five upstreams prefix: "**" and no destination. On a miss, zot tries upstreams in config order until one has the path; the docs' own multi-registry example instead gives each a distinct destination.
Upstream map - observed configuration
OriginLocal namespaceOutbound authSync modeDigest preservedOn missStatus
docker.ioflat (no prefix - collision-ambiguous by construction)Hub login (rate-limit lift)onDemandyesupstreams tried in config order; first that resolves the path winsobserved
ghcr.iononeonDemandyesobserved
quay.iononeonDemandyesobserved
registry.k8s.iononeonDemandyesobserved
nvcr.io$oauthtoken + keyonDemandyesobserved

The flat namespace has not bitten because the origins use largely disjoint paths - Hub's official images live under library/ (the implicit prefix behind bare names like alpine), NGC under nvidia/. Largely disjoint is a probability: an organisation existing on both GHCR and Quay would collide, and config order would pick the winner. Accepted risk, recorded; not a design.

PROPOSED - NOT YET DEPLOYED
The safer shape, documented on both halves: a distinct destination prefix per upstream in zot (/docker, /ghcr, ...), with each Talos mirror pointed at the prefixed path via overridePath: true (suppressing the automatic /v2 append). The requested path then names the origin and collisions become impossible. Migration cost: every cached repository changes local path, so the cache re-warms.
Diagram B · request flow Request flow from a node through containerd to the mirror, with hit, miss, revalidation and fallback branches A pull begins at the node's local content store. On local miss, containerd asks the mirror. A cached digest request is served from mirror storage without upstream contact. A tag request is revalidated against the upstream even when cached, on the deployed version. A storage miss triggers on-demand sync from the matching upstream. If the mirror cannot serve, containerd falls back to the origin registry. node content storehit = no network at all containerd -> mirror digest request, cachedserved locally (verified) tag request, cachedstill revalidates upstream storage misson-demand sync + cache origin registryrate limits live here local miss containerd fallback: mirror endpoints exhausted -> origin direct

green = fully local · amber = the surprising branch on v2.1.17 · magenta = fallback

The amber branch: a warm cache does not take the network out of the story.

Bootstrap, fallback and failure modes

Six machine parts arranged in a ring around an empty centre; one of them is a quarter-scale replica of the large way-station machine, representing the mirror's own image passing through the mirror.
The dependency ring: one of the orbiting parts is the mirror itself - its own image is served through the thing it is.

Four circular dependencies live in this design; each got a different answer.

Loop 1 - the mirror's own image comes through the mirror

zot runs as a container whose image lives on a registry zot mirrors. When the hosting node boots, it asks the mirror - which is not running, because the node is booting. The loop breaks on the documented fallback: mirror endpoints exhaust, containerd falls through to the origin. Keeping skipFallback unset is the load-bearing choice.

FIELD NOTE · LAB, 2026-06
Exercised by accident: a config change shipped with preserveDigest enabled but without its mandatory partner http.compat - a pairing the registry refuses to start without. The mirror crashlooped; the fleet fell through to upstream and nothing user-visible broke. Since then, every config change runs the registry's own verify in a throwaway pod before merging.

Loop 2 - the recovery tooling ignores the mirror

The fleet's rescue tooling does not pull through the mirror: its image is cached on the operations host, outside the cluster, so it remains available while zot or its cluster is down.

Loop 3 - authentication cannot flip everywhere at once

Enforcing auth needs every node to carry credentials that only apply after a reboot, so anonymous read must survive until the last node is proven - and the proof must be designed not to lie. Section 07 covers it.

Loop 4 - the mirror's host is also its customer

The node hosting the mirror boots its own workloads through it, including the tunnel serving this page - so restarting that node carries a blast radius beyond the mirror, and its reboot runbook lists every public-facing dependant and the order they return.

Diagram D · two bootstrap worlds Connected fail-open bootstrap versus disconnected preseeded bootstrap, as separate paths Left: the connected lab path - node boots, mirror miss, implicit fallback to origin, mirror comes up afterwards. Right: the disconnected path - no origin exists; the registry image and release set must be preseeded onto the host or imported from disk before anything else can start. CONNECTED LAB - FAIL-OPEN (observed) node boots, asks mirror: down implicit fallback -> origin serves boot images mirror starts (its image came via fallback) estate converges back onto the mirror DISCONNECTED SITE - PRESEEDED (design, not deployed here) no origin exists - fallback is not a plan registry image preseeded / imported from disk release set, trust roots, creds staged locally site serves itself; syncs on a schedule

left = this lab, observed · right = the airgap translation, a design requirement not a deployed claim

Loop 1 assumes an upstream to fall through to; the airgap deletes that assumption and forces Loop 2's discipline onto the mirror itself.

Fallback is a policy decision. This lab keeps it on for bootstrap survivability and availability through mirror outages; the cost is that pulls can bypass the mirror unnoticed and land on origin rate limits. A regulated or disconnected estate makes the opposite call - skipFallback: true, egress restriction, preseeded release stock, a tested recovery path - because there the bypass is the failure mode. Failure classes differ in symptom, path and consequence:

Failure modes - symptom, path, fallback and consequence
FailureUser-visible symptomRequest pathFallback?DetectionSecurity consequence
Mirror pod downpublic pulls may continue transparently; private images, origin limits, DNS or TLS issues can slow or fail themnode -> originafter the mirror endpoint fails; elapsed time differs by failure class (refused vs DNS vs TLS vs blackhole)mirror probes red; origin egress risespolicy/audit bypass while down
Mirror up, sync wedgedpulls hang up to sync timeoutnode -> mirror (blocked)only after timeoutmanifest probe stalls while /v2/ still answersavailability, not integrity
Upstream 429miss/tag pulls fail or crawlmirror -> origin refusedfallback also reaches the origin and may hit a pull or abuse limit - the quota bucket depends on the node's identity and source IP, not necessarily zot'szot logs; retry storms amplifyself-inflicted denial of service via retries
Upstream down, digest cachednone expectedmirror serves locallynot needed-none (source-verified short-circuit; disconnected drill still open)
Upstream down, tag cachedreported on v2.1.17: failure or waiting until timeout, with content possibly served after itmirror revalidates the tag upstream firstorigin also downthe test matrix in section 08availability surprise for anyone assuming cached means offline-safe (upstream-reported; not yet reproduced here)
PVC fullnew pulls fail, cached OK-ishmirror 5xx on writesyes for missing contentcapacity metrics (proposed)availability; GC pressure
Digest entry GC-evictedsilent re-fetch on next pullmirror -> origin re-syncn/aupstream egress for "cached" contentrate-limit exposure returns (section 06)
Node auth wrong (post-flip)ImagePullBackOffmirror 401; fallback unreliable on 401unreliablethe canary gate (section 07)the outage the gate exists to prevent

Digests, manifests and the supply chain

Half the mistakes on this page came from conflating these objects:

For a mirror, digests carry one sharp operational rule: zot converts Docker-schema manifests to OCI by default, and conversion changes the digest - breaking every digest pin and signature downstream. The pairing that prevents it (preserveDigest: true per upstream, with http.compat: ["docker2s2"]) is mandatory in both directions: the registry refuses to start with one and not the other (the Loop 1 field note is this rule being learned). This deployment runs both on all five upstreams.

Digest preservation and artifact completeness are different properties. The docs tie preserveDigest/compat to keeping mirrored manifest bytes and media types - and therefore signature validity - aligned with upstream. Whether signature, SBOM and attestation objects arrive is separate: OCI 1.1 referrers ride the subject field and their own API, legacy Cosign signatures ride tag-schema conventions, and both depend on origin support and this version's sync behaviour. This deployment has not verified a complete referrer graph for any origin and performs no cryptographic signature verification; disconnected verification would also need locally held trust material. Both gaps sit in the open register. Production estates promote releases by digest and decide per policy whether referrers travel too; this lab runs only the digest half.

Retention, GC and the digest-only trap

A stuck intake pod ringed in magenta with three glowing cargo bricks queued behind it - cached content waiting on a jammed process.
Cached bricks queued behind a jammed intake: availability problems here are quiet, not loud.

This section corrects the largest error in this page's first edition, which said "no retention config means keep everything". True for tags; false for everything else - and "everything else" includes the content this mirror exists to hold. The semantics, from upstream documentation and the deployed version's source:

Combined with section 04's revalidation behaviour: tag pulls are cached but contact upstream anyway; digest pulls serve locally but their entries are GC-eligible between sweeps - a digest-pinned fleet caches exactly the entry class GC may delete. On v2.1.17 this mirror is a rate-limit shield and a latency win, not yet a disconnection shelf; the first edition implied otherwise and was wrong.

Diagram C · what keeps an object alive The OCI object graph and which references protect content from garbage collection A tag points to an index; the index references platform manifests; manifests reference layers and a config blob. Referrers attach to a manifest via a subject field. A separate digest-only manifest sits with no tag pointing at it; it is eligible for garbage collection after the delay on the deployed version. Retention rules that keep tags do not protect the untagged manifest. tag: v1.2 index (multi-arch)own digest manifest amd64 manifest arm64 layers + config layers + config referrer (sig/SBOM)subject -> manifest a tag is a keep-alive digest-only cached manifestNO tag references it GC after gcDelay (1h here)deleteUntagged default: true keepTags protects TAGS in its repo. On v2.1.17/18 the amber box has one blanket lever: deleteUntagged:false, which keeps ALL untagged manifests (capacity trade). Selective pull-aware keepUntagged ships in v2.1.19.

green = protected by a tag · amber/red = the eviction path · dashed magenta = referrers (separate lifecycle)

GC evaluates reference reachability: an untagged digest-only manifest can be deleted even while its blobs remain present, and node-local image stores can mask the eviction for days.

Retention policy semantics - two nearby systems use opposite matching rules:

Mitigations, ranked: upgrade to v2.1.19+ for pull-aware keepUntagged (the designed fix, schema to be validated against the actual binary before rollout); until then, widen gcDelay/retention.delay (the upstream maintainer's interim suggestion), or set deleteUntagged: false, which protects every untagged manifest at the cost of unbounded cache growth. Disabling GC entirely swaps eviction for storage exhaustion with no reclaim path.

SAFE VALIDATION - PROPOSED, NOT YET RUN
The eviction claim is documented upstream and consistent with this config; it has not been reproduced here. The reproduction, in a disposable instance only: record version and sanitised config; pull by digest; confirm the stored manifest is untagged; shorten GC timers; observe the manifest across a sweep; then block upstream and re-pull from a clean runtime store, recording serve vs re-fetch. Two upstream warnings: the retention verification tool executes orphan-blob GC for real even in dry-run, and local storage requires the registry stopped. Never point retention experiments at live storage.

The authentication migration

Four parts left to right: a waiting keyed cartridge, a key wedge, a tall gate frame, and a cartridge beyond the gate with its keyway lit - per-node credentials proven at a checkpoint.
The proving gate: a node counts as migrated when an authenticated pull succeeds where an anonymous one cannot.

Target state: the mirror refuses anonymous pulls. Current state: anonymous read on, per-node credentials staged inert in machine configs until each reboot. The sequencing problem has one central hazard: while anonymous read is on, an ordinary pull says nothing about auth - a node whose credentials never applied is served as an anonymous reader and false-passes the gate. The canary must live in a repository that denies anonymous read, so success can only mean an authenticated pull.

The design is upstream-supported: authorisation resolves per-repository policies by longest match (** is the default for anything unmatched), and maintainer guidance confirms anonymous and authenticated access evaluate independently. So a canary/** entry granting named identities read, with no anonymousPolicy, denies anonymous on that path while the glob keeps the rest of the shelf open. The staged policy:

"accessControl": {
  "repositories": {
    "**": {
      "anonymousPolicy": ["read"],
      "policies": [
        { "users": ["zot-push"], "actions": ["read", "create", "update"] },
        { "users": ["zot-pull"], "actions": ["read"] }
      ]
    },
    "canary/**": {
      "policies": [
        { "users": ["zot-pull", "zot-push"], "actions": ["read"] }
      ]
    }
  }
}
CLIENT CAVEAT · DOCUMENTED
Mixed anonymous/authenticated policies trigger a Docker-client-specific workaround present in this exact version: /v2/ returns 401 to Docker user agents so the Docker CLI sends credentials, meaning anonymous docker users must log in even for anonymous repos. Podman and containerd are unaffected. This estate pulls with containerd, podman and buildah, so the caveat is noted rather than felt.

The gate, per node, and what each step proves:

  1. Stage credentials in the node's machine config (inert; documented as requiring reboot).
  2. Reboot the node at a planned window.
  3. Identity check: pull the protected canary through the node's own runtime. Success = authenticated (anonymous cannot); 401 = back to step 1. The expected split - 401 for missing/wrong credentials, 403 for a valid identity lacking the action - sits in the open register, since the docs state the 403 case only for OIDC identities.
  4. Interception check, separately: the canary is a direct mirror reference, so it says nothing about whether docker.io/... references route through the mirror. That needs an original-upstream reference pulled on a node with a cold runtime store, correlated with the mirror's request log - node-local content would satisfy the pull without any network and fake a pass.
  5. Anonymous read comes off the glob only when every node passes both checks. Rollback is the previous config commit, named before the flip.
Diagram E · the migration ladder The authentication migration ladder with its per-node proof gate and rollback point Five stages: anonymous baseline, credentials staged inert, node reboot, the two-part proof - authenticated canary pull plus logged mirror interception - and the final anonymous-off flip with a named rollback commit. A failing node loops from the proof back to staging. anon read ONbaseline (today) creds stagedinert in config node rebootsauth goes live two-part proofcanary + loggedinterception anon OFFrollback = prior commit 401: node loops back - it does not count

the flip is per-fleet; the proof is per-node - one unproven node under a fleet-wide flip is an outage wearing a green tick

Identity and interception fail independently; each has a false-pass mode the other cannot detect.

Five trust domains never share material: node pull credentials (machine config, reboot-bound), the push credential (used by the operator-run build host for registry logins, not embedded in CI configuration), the mirror's upstream credentials (one mounted secret, pod-restart-bound), TLS trust, and human admin access (SSO in front of the UI). The push credential carries this page's oldest incident: in July 2026 its bcrypt hash and plaintext drifted apart in the secrets manager, rotation became impossible, and pushes stayed dead for twelve days - three applications shipped the day the rebuilt flow landed. Today the hash and plaintext are separate entries in the same secrets manager, rotated as a pair by a human; the registry consumes only the derived htpasswd file, each plaintext reaches only its consumer, and the sync identity is read-only so automation cannot half-rotate the pair. Residual cost, stated: one secrets-manager project holds both halves, so its compromise yields verifier and credential together - versioned, consumer-scoped secret objects remain a possible refinement.

Observability that means something

The worst incident in this mirror's life: an in-flight sync wedged, every pull from that upstream hung, and the health endpoint returned 200 throughout:

Replay · observed 2026-07-07 · the stalled sync
"image already demanded, waiting on channel"
$ kubectl -n zot rollout restart deploy zot   <- recovery action (not a root cause)
# probe an actual manifest, not the process's opinion of itself:
$ curl -sfS --max-time 10 -o /dev/null -w '%{http_code}\n' \
    https://zot.bztmon.org/v2/library/busybox/manifests/latest \
    -H 'Accept: application/vnd.oci.image.index.v1+json'
200                                           <- THIS is "the mirror works"

The deployed source refines that story: the log line is coalescing by design - concurrent requests join the first sync, which runs on a detached background context with a three-hour default timeout and survives client disconnects. Waiters blocking on a stalled sync until that timeout matches what we saw; the restart was recovery, and the root cause was never isolated. Two knobs this config leaves unset - sync maxRetries (disabled by default upstream) and a tighter syncTimeout - are the first change on recurrence.

From the running version's source: /livez, /readyz and /startupz exist as health endpoints (absent from this version's docs), alongside the spec's /v2/. The Prometheus metrics extension exists upstream - series names include zot_http_requests_total, zot_repo_storage_bytes, zot_repo_downloads_total and zot_storage_lock_latency_seconds - and is not enabled here; wiring it, with scrape auth, is on the open list.

The layered checks, one question each:

Layered checks - one question each
CheckQuestion it answersStatus here
/livez / /readyzis the process up / initialisedavailable
authenticated manifest GET of a local-only canarycan the right identity read real content from local storageproposed
anonymous GET of the canary expecting 401is the protection actually protectingproposed
original-reference pull on a cold node + mirror log lineis the runtime actually routed through the mirrorproposed
digest pull with upstream blocked (disposable env)does "cached" mean "served locally"open - section 06
PVC usage + growth, GC activity, sync latencywhen does capacity or eviction become the storyneeds metrics ext
certificate expiry, credential agewhat breaks on a scheduleproposed

The caching test matrix (digest rows: source-verified and consistent with behaviour here; disconnected rows: not yet run in this lab):

Cache behaviour - each row is an experiment
ScenarioExpected on v2.1.17What proves it
cached tag, upstream reachableserved; upstream contacted anyway (revalidation)zot log shows upstream request; latency includes round-trip
cached tag, upstream unreachablefails or stalls until timeout, then local fallback pathpull timing vs sync timeout; error text
cached digest, upstream reachableserved locally, no upstream contactabsence of upstream request in zot logs during the pull
cached digest, upstream unreachableserved locally - IF the entry survived GCthe section-06 disposable-instance drill
cold node, warm mirrormirror serves; node store fillsmirror log + no origin egress
warm node, evicted mirror entrypull succeeds from node store - masking the evictionthis is the false-pass: only mirror-side inspection reveals it
multi-arch index + child manifestsindex and per-platform manifests are separate cache entriesper-digest existence checks on the mirror

The production translation

A monolithic vault with a wall of glowing bricks visible through its door, attended by four smaller self-sufficient way-stations - a central release source and site-local mirrors.
A central curated source and site-local registries: each site holds its own shelf, so a cut WAN idles nothing.

The first edition of this section asserted what "production" does; production chooses from patterns against requirements:

Whatever the topology: recovery time and point objectives get numbers before an incident supplies them; backups are restore-tested copies off the failure domain (array snapshots and RAID protect against disk loss, not against the array, the site or operator error); cache capacity is planned from the section 06 retention policy; and the section 04 circular dependencies get drawn per site, because each exists at every scale - only the cost of ignoring them changes.

Verified, open, and where the claims come from

A wide flat node slab in an otherwise empty void; a single luminous brick hovers midway in its descent toward the slab - one small transfer of bytes as proof of life.
Proof of life is one authenticated byte transfer through the real path - not a status page.

When a node powers on, the fleet's admission check performs an authenticated manifest pull through the node's own runtime before the node counts as a member: a process-health response validates neither routing nor authorisation. Most of this page condenses into that one command.

Architecture decisions
DecisionChosenWhyTrade-offRollback / alternativeStatus
Origin fallbackON (default kept)bootstrap survivability, Loop 1silent mirror bypass possibleskipFallback:true + preseed (airgap shape)observed
Sync modeonDemand, all upstreamscache follows real usage; Hub-safetag pulls revalidate upstream; digest entries untaggedpolled sync for a curated release setobserved
Digest preservationpreserveDigest + docker2s2, x5digest pins + signatures survive mirroringmandatory config pairing (crashloop scar)none - non-negotiable for digest-pinned estatesobserved
Namespace layoutflat (no destinations)simplicity at build timeordered-trial upstream selection; theoretical collisionsper-origin destinations + overridePathredesign proposed
Retentionnone configuredpredates understanding the untagged ruledigest-only entries evict within hoursv2.1.19+ keepUntagged; interim wider gcDelaycorrection owed
Authanonymous read until per-node proofcontainerd 401-fallback unreliability makes big-bang flips dangerouswindow with no pull authstaged flip w/ canary gate + named rollbackin flight
Metricsnot enabledminimal first deploymentcapacity/eviction invisiblemetrics extension + scrape authproposed

Open verification register

  1. Digest-entry eviction reproduction in a disposable instance (section 06 procedure).
  2. Canary repo negative test: anonymous 401 on canary/** while anonymous read elsewhere still succeeds; observed 401-vs-403 boundaries recorded.
  3. Mirror interception proof: original-reference pull on a cold node correlated with the mirror's request log.
  4. Disconnected-serve drill: cached digest pull with upstream blocked, disposable environment first.
  5. Upgrade evaluation: v2.1.19/v2.1.20 for keepUntagged, with the retention config written and reviewed before the upgrade, not after.
  6. Namespace redesign migration plan (per-origin destinations + overridePath), including cache re-warm cost.

Verified against