ferro-oci-server
Embeddable server-side primitives for the
OCI Distribution Specification v1.1.
Manifest / Blob / Tag / Referrers handlers as an Axum router, a
chunked-upload state machine, the spec §6.2 error envelope, and a
metadata-plane trait that keeps the storage layer abstract — plus a
ready-to-run ferro-oci-server binary with Kubernetes health probes.
✅ Passes the official OCI Distribution Spec v1.1 conformance suite: 75/75 specs (Push, Pull, Content Discovery, Content Management). See
tests/conformance/RESULTS.mdfor the real run andtests/conformance/run_conformance.shto reproduce.
The Rust ecosystem has had
oci-client(formerlyoci-distribution) andoci-specfor years. What's been missing is the server. The dominant open OCI registries — Harbor, zot, and thedistribution/distributionreference impl — are all Go. There has been no embeddable Rust answer for the OCI Distribution Spec server side. This crate is a starting point for one.
🟢 Stable (
v1.0.0). The public API is committed under strict semver: breaking changes require a major bump. The server passes the official OCI Distribution Spec v1.1 conformance suite: 75/75 specs, 0 failures (harness intests/conformance/).
Part of the Ferro ecosystem. Extracted from FerroRepo, a private Rust artifact repository.
What this crate does
router/handlers— Axum router for/v2/**:GET /v2/— version check (200 OK)GET|HEAD|DELETE /v2/{name}/blobs/{digest}— blob planePOST|PATCH|PUT /v2/{name}/blobs/uploads/{uuid?}— chunked + monolithic upload state machineGET|HEAD|PUT|DELETE /v2/{name}/manifests/{reference}— manifest plane (digest or tag references)GET /v2/{name}/tags/list— tag listing with paginationGET /v2/_catalog— repository listing with paginationGET /v2/{name}/referrers/{digest}— referrer index per spec §6.7
error—OciErrorplusOciErrorCodeenum that renders the spec's error JSON envelope ({ "errors": [...] })manifest—ImageManifest,ImageIndex,Descriptorserde typesmedia_types— Media-type classification (Docker v2 vs OCI v1 manifests, configs, layers)reference— image-name + tag + digest parsing with spec-compliant validationregistry—RegistryMetatrait (manifest + tag + upload + referrer book-keeping). Reference implInMemoryRegistryMetausingparking_lot::RwLock+BTreeMap.upload—UploadState,ContentRangeparser
What this crate does not do
- External metadata database — only
InMemoryRegistryMetaships. A SQLite / Postgres backend is on the roadmap; the trait is stable enough that you can implement your own today. Note that the filesystem deployment does durably persist metadata to ametadata.jsonsnapshot underFERRO_OCI_STORAGE_DIR(see below), so manifests/tags/referrers survive a restart even without an external database. - Authentication — handlers are open. Layer your auth in the Axum middleware stack above this router.
- Sigstore / SLSA / TUF / cosign — the OCI server stores and serves manifests but does not sign or verify. Those live in separate crates (not yet published).
Run the server
A ready-to-run binary ships with the crate. It is configured entirely through the environment:
| Variable | Default | Meaning |
|---|---|---|
FERRO_OCI_LISTEN |
0.0.0.0:8080 |
host:port to bind |
FERRO_OCI_STORAGE_DIR |
(in-memory) | filesystem dir for blob bytes and metadata; unset → RAM (ephemeral) |
RUST_LOG |
info |
tracing-subscriber env filter |
When FERRO_OCI_STORAGE_DIR is set the registry metadata plane
(manifests, tag aliases, referrer index) is durably mirrored to a
metadata.json snapshot in that directory (write-through on every
mutation, atomic temp-file + rename) and reloaded on boot, so blobs and
their manifests/tags survive a restart together. A missing or corrupt
snapshot is tolerated — the registry starts empty and logs a warning.
In-flight upload sessions are intentionally not persisted (a restart
aborts them). With FERRO_OCI_STORAGE_DIR unset, both blobs and
metadata are in-memory only and lost on exit.
FERRO_OCI_LISTEN=127.0.0.1:5000 \
FERRO_OCI_STORAGE_DIR=/var/lib/oci-registry \
It exposes Kubernetes-style probe endpoints alongside the /v2/**
surface and shuts down gracefully on SIGTERM/SIGINT:
GET /live— liveness (200 OK, bodyOK)GET /healthz— health (200 OK, JSON{"status":"ok"})GET /ready— readiness (200 OK, bodyOK)
Embed it in your own server
use Arc;
use Router;
use FsBlobStore;
use ;
# async
Then docker push localhost:5000/myimage:latest should work
against the running server.
Conformance
The crate passes the official upstream
opencontainers/distribution-spec conformance test suite
end-to-end — 75/75 specs across all four workflow categories
(Push, Pull, Content Discovery, Content Management). The real run and
its honest changelog (including the two server bugs the suite caught)
are recorded in tests/conformance/RESULTS.md;
tests/conformance/run_conformance.sh
boots the server, runs the suite (Go toolchain or prebuilt Docker
image), and writes a JUnit + HTML report, so you can reproduce it in
CI or on a workstation.
In addition, tests/conformance_smoke.rs exercises the full
in-process request walk for every endpoint pair (start upload →
chunked PATCH → finalize PUT → blob HEAD/GET → manifest PUT-by-tag →
manifest GET-by-digest → referrers GET → tag list → catalog → delete)
and the error variants in spec §6.2.
An external metadata database (SQLite / Postgres) and an
authentication trait remain on the roadmap. The filesystem deployment
already persists metadata durably via a metadata.json snapshot (see
"Run the server").
Status
| Aspect | Status |
|---|---|
| API stability | stable (v1.x) — strict semver from 1.0.0 |
| Conformance | official OCI Distribution v1.1 suite: 75/75 specs, 0 failures |
| Manifest / blob / tag / catalog / referrers handlers | working |
| Chunked uploads | working |
Runnable server binary + K8s probes (/live, /healthz, /ready) |
working |
| OCI v1.1 conformance suite | 75/75 specs pass (all 4 workflows) |
| Metadata durability | filesystem deployment persists to metadata.json; external DB backend (SQLite/Postgres) on roadmap |
| Authentication | scaffold — layer your own middleware |
| MSRV | rustc 1.88 |
Used in production by
- FerroRepo (private) — Rust artifact repository.
License
Apache-2.0. See LICENSE.