Stevedore
A library-first, daemonless OCI toolkit for Elixir — everything you can do to a container image except run it.
A stevedore is the dockworker who loads, unloads, stows, and inspects shipping containers, and never sails the ship. That is exactly the line this library draws:
Stevedore handles OCI artifacts at rest — as bytes. Running them (namespaces, mounts, cgroups) is in motion, and out of scope.
Everything Stevedore does operates on images-as-data: fetch, inspect, copy, mirror, build, modify, analyze, sign, verify, and serve. None of it needs a kernel, root, or a container runtime — which is the whole point. It stays portable and embeddable by any application that deals with images, whether or not that application can (or wants to) run them.
It takes its feature surface from Skopeo (copy, inspect, sync, sign) and extends it with the create/modify/analyze surface of crane and ORAS, plus an opt-in registry server.
Status
Pre-1.0, under active development. The full at-rest toolkit (registry client, copy + transports, registry server, build/mutate, analyze, sign/verify/referrers, CLI + deploy) is implemented and tested; the public API may still shift before 1.0.
A taste
# Mirror a multi-arch image from one registry to another (digests preserved, blobs skipped/mounted):
Stevedore.copy("docker://alpine:3.20", "docker://ghcr.io/me/alpine:3.20", all: true)
# Build an image from a directory tree — declaratively, no Dockerfile, no `RUN`:
{:ok, image} = Stevedore.Build.from_dir("./rootfs", %{entrypoint: ["/bin/app"]})
Stevedore.copy(image, "oci:./out:1.0")
# Read what's inside, in memory and without root (whiteout-aware):
{:ok, sbom} = Stevedore.Analyze.sbom(image)
# Run a real /v2 registry — nothing starts until you ask:
Stevedore.start_link(store: "/var/lib/stevedore", port: 5000)
…or from the shell:
mix stevedore.copy docker://alpine:3.20 oci:./alpine:3.20
mix stevedore.inspect docker://alpine:3.20
mix stevedore.deploy docker://alpine:3.20 ./public --server nginx --config registry.conf
Design principles
- Weightless by default. Adding
:stevedorestarts no processes and pulls no heavy dependencies. The HTTP client (req), the registry server (plug/bandit), and zstd (ezstd) are optional — you opt into them only for the modes you use. - Daemonless, no database. It talks to registries and on-disk layouts directly; for on-disk transports the filesystem is the source of truth.
- Native. Crypto, digests, signing, and archives use
:crypto/:public_key/:zlib. It never shells out toskopeo,cosign,openssl,gpg, ortar. - Digest-preserving. Manifests and blobs move as raw bytes, so content digests stay stable end to end. Conversions that must re-serialize (e.g. OCI ↔ Docker v2s2) say so.
- Pure core, pluggable shells. Format logic is pure functions over structs; storage,
transports, serving, and the CLI sit behind behaviours.
copyis the primitive everything composes from: any transport → any transport.
What it can do
| Area | Highlights |
|---|---|
| Fetch | docker:// Distribution v2 client: bearer-token auth, multi-arch select, digest-verified blobs, CDN-redirect token-leak protection |
| Copy | one primitive across transports — docker://, oci:, oci-archive:, docker-archive:, dir:, static: — with multi-arch/platform/format control, blob-skip and cross-repo mount |
| Build / modify | assemble images from layers or a directory; append, retag, rewrite config, annotate, rebase, flatten — all digest-correct |
| Analyze | whiteout-aware merged filesystem, per-layer entries, diffs, file reads, best-effort SBOM |
| Sign / verify | cosign-compatible signatures (native ECDSA), policy verification, OCI 1.1 subject/Referrers |
| Serve / deploy | a writable /v2 registry (Bandit), or a static tree + generated nginx/caddy config a dumb web server can host |
Installation
Add stevedore to your dependencies:
def deps do
[
{:stevedore, "~> 0.1"}
]
end
The base build is dependency-free. Opt into modes as needed:
{:stevedore, "~> 0.1"},
{:req, "~> 0.5"}, # the docker:// registry client
{:plug, "~> 1.16"}, # \
{:bandit, "~> 1.5"}, # } the standalone /v2 registry server
{:ezstd, "~> 1.1"} # zstd-compressed layers
Target Elixir is ~> 1.18 (uses the built-in JSON module — no jason).
Documentation
- Cookbook — task-oriented recipes (mirror, build from a directory, sign & verify, serve a registry, …), each a complete copy-paste block with its intent and expected result. Start here to do something.
- References — the OCI/Docker/sigstore specifications Stevedore implements, mapped section-by-section to the modules that implement them.
- Testing — the test strategy: the hermetic core,
the
:external/:conformance/:interoptag taxonomy, external tools used as oracles (never inlib/), how to run each slice, and the CI job map.
Every module carries a @moduledoc and every public function a @doc + @spec (with iex>
doctests). API docs are generated with ExDoc and, once
published, will be at https://hexdocs.pm/stevedore.
Specifications
License
Stevedore draws directly on the design and conventions of the cloud-native container ecosystem — Skopeo, cosign, crane, and ORAS — which are themselves Apache-2.0. Matching that license keeps Stevedore compatible with the projects it learns from and interoperates with, and the Apache license's explicit patent grant is the norm for OCI tooling. Copyright 2026 oshlabs.