Skip to content

Connect your clients: Docker, containerd, GitOps

Content has landed in your zone — promoted continuously by a passthrough instance, or imported from removable media in an isolated zone. Either way, this page is the end of the journey: pointing the machines that consume it at the right place. Everything here applies to both modes.

Why docker.io/x becomes registry.zone/docker.io/x

Section titled “Why docker.io/x becomes registry.zone/docker.io/x”

Tobby relocates every ingredient under its nominal source host:

<zone-registry>[/<base-prefix>]/<canonical-source-host>[_<port>]/<repository-path>

docker.io/bitnami/wordpress is pullable at registry.zone.example/docker.io/bitnami/wordpress, with an unchanged digest. The rule (ADR-0013, FR-035) buys three things at the cost of longer names:

  • No collisions. A flattened registry.zone/bitnami/wordpress cannot tell docker.io/foo/bar from ghcr.io/foo/bar — a correctness bug and a repository-confusion attack surface in one.
  • Predictability. Given a recipe and a destination, the location of every ingredient is computable with no extra metadata — audits, cleanup tooling, and Tobby’s own differential comparison all rely on the same pure function.
  • Cascade invariance. The path is derived from the host written in the recipe, not the host contacted, so it is identical in every zone of a multi-hop chain.

Hosts are canonicalized (lowercased; index.docker.io and registry-1.docker.io fold to docker.io), and a port’s : becomes _ — lab.example.com:5000 relocates under lab.example.com_5000/. Names are never truncated: a destination that cannot take a relocated name fails explicitly, before the push.

upstream zone registry docker.io/library/alpine sha256:9b2a28eb… Tobby no rewrite, no re-sign registry.zone.example/ docker.io/library/alpine sha256:9b2a28eb… Same digest on both sides — only the registry host changes; the nominal source host survives in the path

You never compute those paths by hand. Each recipe exposes its source→destination table — every ingredient’s nominal reference, its relocated repository, tag, and pinned digest (FR-065):

  • UI: the recipe’s mapping view at /recipes/{recipe}/mapping, with copyable references.
  • API: GET /api/v1/recipes/{recipe}/mapping — the same data as JSON, one entry per resolved version, ingredients included.

The per-recipe mapping table: upstream reference to relocated zone path, with copy buttons

For runtime image pulls, you do not have to touch a single chart value: containerd rewrites references at pull time. On each K3s/RKE2 node, write /etc/rancher/{k3s,rke2}/registries.yaml by hand from the mapping table:

/etc/rancher/rke2/registries.yaml
mirrors:
docker.io:
endpoint:
- "https://registry.zone.example"
rewrite:
"^(.*)$": "docker.io/$1"
ghcr.io:
endpoint:
- "https://registry.zone.example"
rewrite:
"^(.*)$": "ghcr.io/$1"
configs:
registry.zone.example:
auth:
username: puller
password: "…"

With that in place, a pod referencing docker.io/bitnami/wordpress pulls from the zone registry, and the chart deploys as published — values untouched, digests intact.

Two classes of reference bypass containerd and must name relocated paths explicitly, using the mapping table:

  • GitOps chart sources. An Argo CD repoURL or Flux HelmRepository pointing at oci://registry.zone.example/... must use the relocated chart path — the Helm client does not go through node-level mirrors.
  • Admission policies. Rules matching image references (Kyverno, Gatekeeper, signature-verification admission) see the reference as written in the pod spec. Decide which side of the rewrite your policies match, and keep it consistent.

One more caveat for verifiers: cosign signatures embed the origin reference in critical.identity.docker-reference. Tobby copies signatures bit-exact alongside the content, so a policy engine that matches docker-reference against the pulled location will see docker.io/... while pulling from registry.zone.example/.... Configure the policy against the nominal reference. Details in content trust.

Tobby never rewrites chart values — that would break digests and signatures, the exact thing it exists to preserve.

The embedded registry and the zone-facing surfaces use the instance’s own accounts (FR-076) — the same ones as the UI:

Terminal window
docker login registry.tobby.zone.example
helm registry login registry.tobby.zone.example
oras login registry.tobby.zone.example

Create dedicated read-only accounts or CI tokens for consumers; see authentication and RBAC.

OS packages over HTTP: the /files/ endpoint

Section titled “OS packages over HTTP: the /files/ endpoint”

A FileSet ingredient can package an apt or rpm repository, and Tobby can serve its verified content read-only over HTTP under /files/<name>/… (FR-047) — making a bare host installable with no other infrastructure. Serving is off by default and enabled per FileSet:

files:
filesets:
- name: debs # served under /files/debs/
ref: registry.example.com/filesets/site-packages
version: "1.4.0" # empty = highest semver present locally
platform: linux/amd64 # only needed for multi-platform FileSets
anonymous: true # opt-in unauthenticated reads

Only what the store holds and verified is served; range requests are supported; there is no upload surface. Reads require the viewer role by default. anonymous: true exists for the bootstrap case — a bare host that cannot authenticate before it has installed anything — and is never silent: every anonymously served FileSet is named in a permanent UI banner and reported by the API, like every other relaxation of the secure default (FR-075).

Terminal window
# /etc/apt/sources.list.d/zone.list on a client host
deb [trusted=yes] https://tobby.zone.example/files/debs stable main

Package-manager trust of the repository metadata is your distribution’s mechanism; what Tobby guarantees is that the served tree came from a FileSet that passed signature verification on its way into the store.

Sometimes the files you need to serve have no upstream to fetch them from: a handful of vendor drivers, a locally built repository, a bundle handed over on a disk. tobby fileset pack turns a directory into a FileSet — a standard OCI image whose single layer is the directory’s file tree — and imports it into the store, pinned by its digest:

Terminal window
tobby fileset pack ./apt-repo debs:1.0.0

Packing is reproducible: the same directory always produces the same digest, so packing twice transfers nothing the second time. Timestamps and ownership are deliberately not carried — only the file tree, its permissions and its symbolic links. The directory is refused, naming the entry, when it holds something a FileSet extraction would have to refuse anyway: a symbolic link leaving the directory, a setuid bit, a device node or a socket, or a name the image layer format reserves.

Two limits, stated because they are the point:

  • A packed FileSet is unsigned. Tobby holds no signing key, so it is recorded as a manual import of local origin and every listing says so. It is content you vouch for, not content the trust roots vouch for.
  • Serving it is a separate, explicit step. Packing puts it in the store; nothing is served until it is named under files.filesets and the instance is restarted. The command prints the exact block to add.

The command reads the host’s filesystem with the rights of whoever runs it. The same operation on the interface and the API (POST /filesets/pack) is restricted to administrators and confined to the directories files.packRoots names — with no entry configured, the form is not offered at all. This is the only way local files enter a store, and it is deliberately not an upload endpoint.

Next: bring content in outside any recipe — one-off imports — or jump to operating over time.