Container Registry
The Tabbify registry stores OCI images produced by remote builds. It speaks the
OCI Distribution API on /v2, with an authenticated HTTPS edge at
https://registry.tabbify.io and an internal mesh endpoint used by platform
components. Production mesh traffic to the registry follows the managed relay
path, with tenant namespace checks at the registry boundary.
You almost never touch the registry by hand. When you git push, Tabbify clones your repo, builds the Dockerfile into an OCI image on a build supervisor, and pushes that image here for you. The registry is plumbing under the deploy pipeline — this page documents how it works.
How it is built
The registry is a thin Rust wrapper around Zot. The wrapper runs as PID 1, supervises a Zot child on loopback 127.0.0.1:5001 (anonymous, never exposed), joins the mesh tagged registry with a sticky identity, and reverse-proxies /v2 on its assigned mesh address at port 5000. Zot does the storage; the wrapper does auth, namespace enforcement, and mesh integration. Large layers stream through in both directions — nothing is buffered in memory.
Addressing
The registry is reachable two ways:
- Public edge —
https://registry.tabbify.io. CloudFront terminates TLS and forwards to the registry over the mesh. This is the only user-facing address; nothing on your machine needs to know about mesh internals. - Mesh peer — when the registry joins the mesh it receives a standard peer ULA (the same
fd5a:1f00:0:N::1shape used by platform peers) and binds[<peer-ula>]:5000.
The peer ULA is not a user endpoint — only other mesh peers (the node, the build supervisors) dial it. The identity that pins it is persisted under REGISTRY_DATA_DIR, so the address stays stable across restarts.
Don't confuse this with the
fd5a:1f02:<hash>::1addresses you'll see elsewhere. Those are per-app ULAs that supervisors host for deployed apps — the registry is a service peer, not an app, so it lives in thefd5a:1f00:0:N::1range.
Mesh peers serve plain HTTP over the encrypted WireGuard tunnel, so a Docker daemon that talks to the registry by its mesh address must list that address in insecure-registries (/etc/docker/daemon.json). Clients that go through registry.tabbify.io get ordinary HTTPS and need no such config.
Authentication
When REGISTRY_AUTH_URL is set, every /v2 request is gated. The wrapper validates your Tabbify API token against auth and mints a short-lived (300s) HS256 registry token signed by a per-process secret. The hot path verifies that token locally — no per-request call to auth.
This is the standard OCI bearer flow. Log in with your Tabbify API token as the password (the username is ignored):
docker login registry.tabbify.io -u x -p <TABBIFY_TOKEN>
Under the hood: a first /v2 hit returns 401 with a challenge pointing at /auth/token; the client exchanges your token there, gets back {token, access_token, expires_in: 300}, and retries with Authorization: Bearer <registry_token>. The wrapper calls auth's POST /v1/validate and reads the network claim (falling back to subject) as your tenant.
Per-tenant namespaces
Current namespace enforcement requires every repository path to start with
<tenant>/. A token for tenant acme can touch /v2/acme/... and nothing else.
namespace::check("acme", "/v2/acme/app/manifests/v1") -> Allow
namespace::check("acme", "/v2/globex/app/manifests/v1") -> Deny (403)
namespace::check("acme", "/v2/") -> Allow (version check)
namespace::check("acme", "/v2/_catalog") -> Deny
Pushing and pulling by hand
In the normal flow you never push manually — git push triggers a build that pushes for you. But the registry is a standard OCI store, so you can talk to it directly through the public edge. Log in first (see above), then:
docker tag busybox registry.tabbify.io/myteam/demo:v1
docker push registry.tabbify.io/myteam/demo:v1
The <tenant>/ namespace rule applies to manual access too: you can only write
under your own tenant prefix. Registry acceptance of an OCI artifact does not
make it an executable Tabbify workload; the public runtime deploys CPU container
images through Firecracker.
Storage
REGISTRY_STORAGE picks the backend: local (filesystem under REGISTRY_DATA_DIR/zot-cache, the always-green dev default) or s3 (set REGISTRY_S3_BUCKET and REGISTRY_S3_REGION). On AWS, credentials come from the EC2 instance role — no static keys in env. Both modes keep a local boltdb dedupe cache.
Running it
The managed registry is part of the platform — you don't deploy it yourself to use Tabbify. This is what the container looks like if you run it locally or as part of a self-hosted stack:
docker run -d \
--device /dev/net/tun --cap-add NET_ADMIN \
-e REGISTRY_STORAGE=local \
-e REGISTRY_AUTH_URL=http://auth:8080 \
-v tbf-registry:/var/lib/tabbify-registry \
tabbify-registry
In production the image reference comes from a compose variable (TABBIFY_REGISTRY_IMAGE) that resolves to a private ECR image at deploy time, and cloud-init pulls it with the EC2 instance role — no static credentials. For local or development use, the published ghcr.io/tabbify-io/tabbify-registry image works the same way.
Mesh membership needs the TUN device and NET_ADMIN. Mounting a volume at REGISTRY_DATA_DIR (default /var/lib/tabbify-registry) is mandatory, not optional: it holds the persisted mesh-identity.json. Without it the registry generates a fresh keypair — and therefore a fresh mesh address — on every restart, which breaks routing for every peer that was talking to the old address. The volume is what keeps the registry's fd5a:1f00:0:N::1 address stable so the node and supervisors can keep finding it.
The current deployment path uses Zot supervision, mesh join, the streaming
/v2 proxy, token auth, tenant namespaces, and the HTTPS registry edge. Leaving
REGISTRY_AUTH_URL unset creates an anonymous local-development mode with no
tenant enforcement; do not expose that mode publicly.
See also The Node API, The deploy pipeline, and the CLI reference.