Documentation
ReferenceArchitecture

Container Registry

The Tabbify registry is where your built app images live. It speaks the standard OCI Distribution API on /v2, and it has two faces: a public HTTPS edge at https://registry.tabbify.io, and a private mesh peer that the rest of the platform talks to directly. It joins the private mesh tagged registry and answers on its mesh address; peers like the node and the build supervisors push and pull from it through encrypted WireGuard tunnels, with per-tenant isolation enforced at the door.

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 edgehttps://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::1 shape every supervisor and service peer gets), and binds [<peer-ula>]:5000. In the live deployment that address is:

    [fd5a:1f00:0:3::1]: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>::1 addresses 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 the fd5a:1f00:0:N::1 range.

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

Phase 1 enforces a hard rule: every repository path must 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   (Phase 1)

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 registry is a generic OCI artifact store, so it also accepts non-image artifacts under the same /v2 API via oras:

oras push \
  --artifact-type application/vnd.tabbify.wasm.component.v1 \
  registry.tabbify.io/myteam/hello:v1 \
  hello.wasm:application/wasm

Either way the <tenant>/ namespace rule still applies — you can only write under your own tenant prefix.

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.

Where things stand: Zot supervision, mesh join, the streaming /v2 proxy, token auth, per-tenant namespaces, the registry.tabbify.io edge, and both storage modes are live on AWS and verified end-to-end over the mesh. Leave REGISTRY_AUTH_URL unset for anonymous dev mode — no token, no namespace enforcement, local only. The catalog endpoint and ACLs beyond per-tenant are Phase 2; signed images and garbage collection are Phase 3.

See also The Node API, The deploy pipeline, and the CLI reference.