Container Registries
Flow steps that run containers (container.run, ansible.playbook, tf.plan, tf.apply) pull their images from container registries. Public images need nothing. Images in a registry that needs a login — a private package on ghcr.io, a Docker Hub account, a company registry — pull with a registry credential: a login an organization admin stores once, which the platform hands to every container step in the organization whose image lives on that registry. The flow itself does not change.
A deployment can also run the platform registry, Hegemony's own container registry on the object store. Container steps then keep copies of the images they pull in it and pull the copies on later runs, so an upstream registry's rate limits, speed or outages stop mattering, and a site can be prepared to run offline. Nobody logs in to it: steps get access on their own, for their own organization. See The platform registry.
What a Registry Credential Is
A registry credential belongs to one organization and holds:
- Registry host — the registry as it appears in image references, with an optional port:
ghcr.io,registry.example.com:5000. Docker Hub is stored asdocker.io; its other spellings (index.docker.io,registry-1.docker.io) are normalized to it, and the platform writes it under the key Docker expects (https://index.docker.io/v1/). - Username — the registry login name (for GitHub Container Registry, the GitHub user name; for a robot account, its name).
- Password reference — the password or token as a secret reference,
{{ secret('scheme://path/key') }}. Plaintext values and the platform-onlyenv()andfile()helpers are refused, as for file repositories: the value never enters the database, and it resolves in the organization's own secret namespace (orgs/<slug>/…) when a step uses it. - An optional description.
There is one credential per registry host per organization. Docker picks the login by host, so a second row for the same host could never be used.
Credentials are managed in Settings → Registry Credentials (organization admins; everyone with viewer can see them) and through the API under /registry-credentials. Rows of the designated shared organization are visible to other organizations read-only, with the password reference hidden; a consumer organization's steps use their own organization's credentials, never the shared organization's.
How Steps Use It
Before a container-backed step runs, the worker fetches the run organization's credentials (references only), resolves each reference in the run's organization, and writes a private Docker configuration (config.json) that lives only for that step. The container runner points DOCKER_CONFIG at it, so both ways it pulls — the Docker Engine API with its X-Registry-Auth header, and the docker pull fallback — present the login for the image's host. Every host the organization has a credential for is written, whichever image the step ends up pulling (handler defaults fill in the image after the flow is read). The directory is removed when the step ends.
The resolved password and its base64 user:password form join the step's redaction set, so neither appears in run events, evidence or step output.
What happens when something is missing:
- No credential for the image's host: the pull runs anonymously, as it did before credentials existed.
- A credential whose reference does not resolve: the step emits a warning event naming the host and skips that host; the step fails only if the pull then needs that login.
- The internal API cannot be reached: every pull of that step runs anonymously, with a warning event.
The step's evidence records the image it ran and, for steps that pin their image (tf.plan), the registry digest it resolved to.
Testing a Login
Test login on a credential's page logs in to the registry from the API the way docker login does: it asks the registry's /v2/ endpoint, follows its Basic or Bearer challenge (fetching a token from the challenge's realm with the credentials), and reports ok, or an error_kind of auth, network, endpoint_invalid or unknown. A successful test stamps the credential's last-tested time. The test refuses registry hosts that resolve to private or loopback addresses, because the API would be making the request itself (the same guard file imports use); steps on the sandbox can still use such a registry.
Configuration Exchange
Credentials travel in the registry_credentials section of a bundle (host, username, password reference, description) and through platform sync under registry_credentials/<host>.yaml, keyed by host. The reference travels verbatim; an import enforces the same rules as the API, so a bundle cannot smuggle in a plaintext password.
The Platform Registry
The platform registry is optional: the registry compose overlay starts it (see Deployment below), and HEGEMONY_REGISTRY_ENABLED tells the API and the worker it is there. Without it, everything above still holds and nothing below applies.
What it holds
Image names in the registry are registry.hegemony.internal/<namespace>/… (the host is HEGEMONY_REGISTRY_HOST):
| Path | What lives there | Who writes it |
|---|---|---|
orgs/<org slug>/<host>/<repository>:<tag> | the organization's cached copies of upstream images, named after the image's own registry and repository (orgs/acme/docker.io/library/alpine:3.20; a registry port is written after an underscore, registry.example.com_5000) | the organization's container steps |
orgs/<org slug>/<image>:<tag> | the organization's own images | organization admins (import) and build steps — both planned |
global/<image>:<tag>, global/<host>/<repository>:<tag> | platform-wide images and pre-loaded copies, readable by every organization | platform admins (import — planned) |
The slugs orgs, global, cache and mirror are reserved for these paths and cannot name an organization. A step reads its own organization's paths, the designated shared organization's, and global; it writes only under its own orgs/<slug>/<host>/…. Nothing one organization caches is ever served to another.
A cache path's host segment carries a dot (docker.io, registry.example.com_5000 with the port after an underscore) or is localhost, with or without such a port; nothing else is one. The shape is reserved: an organization's or the global namespace's own image names never start with such a segment, and the import and build steps to come refuse them. An image from a registry whose name has no dot and is not localhost (myregistry:5000) is pulled from it and never cached.
Cache image
Every container-backed step has a Cache image option, on by default. With the platform registry running, the step:
- asks the image's own registry which digest the tag points at today (one
HEADrequest, which Docker Hub does not count against its pull limits); a digest-qualified reference (image@sha256:…) needs no question; - pulls the copy from its organization's cache, or from
global, when one holds that digest; - otherwise pulls from the image's registry and saves a copy, under the tag and under
sha256_<upstream digest>, the key the next run looks up.
If the image's registry cannot be reached, the step runs the newest copy under the tag and says so in a warning event. If the platform registry cannot be reached, the step pulls from the image's registry without caching, with a note. A copy that cannot be pulled falls back to the image's registry. Saving a copy never fails a step: a refused or slow push leaves a note and the step runs the image it already has. Private images are cached too, in the organization's own namespace, pulled with the organization's registry credential. Turn the option off for an image that must always come straight from its registry.
The step's evidence records the reference the container ran by (image_served), where it came from (image_source: upstream, cache or platform) and whether this run saved a copy (image_cached). A tf.plan served from the cache records the copy's digest, which tf.apply then pulls from the platform registry.
How steps are let in
Before a container step runs, the worker asks the API for a short-lived registry token for the step (POST /internal/runs/{run_id}/registry-access) and writes it into the same per-step Docker configuration as the registry credentials, under the registry's host. The token acts for the run's organization and is revoked when the step ends. Every request to the registry passes through a proxy that asks the API's GET /registry/authorize first: the proxy presents a shared secret, the API checks the token and the repository, and answers 204 (go ahead), 401 with a Basic challenge (no usable token) or 403 (not this token's to do). A step may pull from the paths above, push only into its organization's cache, and never delete. The authorize route answers nobody but the proxy; through the UI it does not exist.
Cached copies nobody pulled or pushed for HEGEMONY_REGISTRY_CACHE_RETENTION (30 days by default) are removed by zot's retention; organization and global images outside the cache paths are never removed automatically.
Operations
The registry is zot with the S3 storage driver, storing in the bucket HEGEMONY_REGISTRY_BUCKET under the platform's S3 key. The API creates that bucket at startup: with the internal object store's buckets, or on its own with an external object store, where the platform's S3 key needs the right to create a bucket (or the bucket is created beforehand). zot's local metadata is a rebuildable index of the bucket, not the images; the bucket is backed up with the rest of the object store.
zot's own web page is not exposed; the Container Images pages above are the way to browse and delete. zot's page stays available as a break-glass console on the Docker host's loopback, opened on demand with task compose:registry:ui:dev or task compose:registry:ui:prod like the Temporal console: it talks to zot directly, past the proxy, so whoever reaches it browses and deletes every organization's images. Import and export of images and a build step are the planned next pieces.
Container Images pages
Artifacts → Container Images lists what the registry holds, one tab per namespace visible from the active organization: the organization's own (its images and the copies its steps cached), the shared organization's and the global one, each with its repository count and size. A row is a repository with its origin (a cached copy names the upstream it was copied from, read from its path), tag count, size, last update and pull count; it opens the repository page, which shows the full reference steps pull (registry.hegemony.internal/<namespace>/<path>) and every tag with its digest, platform, size, push time, last pull and pull count. A cached copy carries two tags for one image: the human one and the key tag named after the upstream digest (sha256_…), which the cache looks up. The key tag has an underscore because sha256-<digest> is the shape the OCI distribution spec reserves for referrers fallback tags: zot keeps such tags out of its index, pull statistics and retention, which would hide the copy and keep it forever.
Every size on these pages is what the registry stores, that is the bucket usage: the compressed layers plus the manifest and the configuration. A step's log reports the unpacked size of the same image on the sandbox daemon, typically two to four times larger. The two never match; neither is wrong.
Who sees and deletes what follows the namespaces:
| Namespace | Browse | Delete tags and repositories |
|---|---|---|
| The active organization's | image:view (every member) | image:manage (organization admins) |
| The shared organization's | every organization's members | its own admins, from within that organization |
global | every organization's members | platform admins (image:manage_global) |
Deleting a tag removes that tag alone; the image stays while another tag points at it (a cached copy keeps its key tag until that is deleted too). Deleting a repository removes every image in it. A step that pulled a deleted copy pulls from upstream again and caches it anew. Every delete is an audit entry. Platform admins also see Usage by namespace: every organization's and the global namespace's repository count and size, as zot counts them.
The pages read zot directly on the overlay's internal network (HEGEMONY_REGISTRY_BACKEND_URL), the one path past the proxy's step-only authorization; nothing a step holds reaches it. When the registry is not enabled, the page says so instead of failing. Repositories without a tagged image (everything deleted, not yet garbage-collected) are not listed.
Deployment
Add registry to SERVICES (next to s3, or with an external S3 endpoint in HEGEMONY_S3_*), set HEGEMONY_REGISTRY_PROXY_SECRET (openssl rand -hex 32) and start the stack; the overlay sets HEGEMONY_REGISTRY_ENABLED for the API and the worker. The registry listens on no host port: the sandbox daemon and the worker reach it as registry.hegemony.internal on the compose network, over plain HTTP, and the overlay lists that name in the daemon's insecure registries. zot itself serves only on its fixed address on the overlay's internal network (HEGEMONY_REGISTRY_NETWORK_SUBNET, HEGEMONY_REGISTRY_ADDRESS; change them together if the subnet collides with something on the host) and uses the compose network only to reach the object store. The proxy, the API and the break-glass console dial that fixed address, not the service name: Docker's DNS resolves the name to zot's address on the compose network, where nothing listens, and the proxy would answer 502 to every request. Settings: HEGEMONY_REGISTRY_HOST, HEGEMONY_REGISTRY_URL, HEGEMONY_REGISTRY_BUCKET, HEGEMONY_REGISTRY_IMAGE_TAG (the zot release), HEGEMONY_REGISTRY_S3_SECURE (true when the S3 endpoint is https), HEGEMONY_REGISTRY_CACHE_RETENTION, HEGEMONY_REGISTRY_BACKEND_URL (where the API reaches zot for the Container Images pages; the overlay derives it from the fixed address). The production hardening guide covers the trust boundaries and multi-host deployments.
Related docs
- Flow step reference — the container handlers, their image fields and the Cache image option
- Production hardening — the platform registry's trust boundaries
- Secrets Management — reference formats and backends
- File Repository Storage and Connections — the same reference-only credential rule for object storage
- Glossary — Registry Credential