CI

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 :stevedore starts 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 to skopeo, cosign, openssl, gpg, or tar.
  • 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. copy is the primitive everything composes from: any transport → any transport.

What it can do

AreaHighlights
Fetchdocker:// Distribution v2 client: bearer-token auth, multi-arch select, digest-verified blobs, CDN-redirect token-leak protection
Copyone primitive across transports — docker://, oci:, oci-archive:, docker-archive:, dir:, static: — with multi-arch/platform/format control, blob-skip and cross-repo mount
Build / modifyassemble images from layers or a directory; append, retag, rewrite config, annotate, rebase, flatten — all digest-correct
Analyzewhiteout-aware merged filesystem, per-layer entries, diffs, file reads, best-effort SBOM
Sign / verifycosign-compatible signatures (native ECDSA), policy verification, OCI 1.1 subject/Referrers
Serve / deploya 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/:interop tag taxonomy, external tools used as oracles (never in lib/), 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

Apache License 2.0.

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.