README
¶
spod
A daemonless, rootless OCI container engine and embeddable Go library,
Apache-2.0 licensed. Images pull straight from registries via
go-containerregistry; Linux containers run through runc/libcontainer.
No daemon, no root service, no separately installed runtime.
Quick Start
Install the latest release binary and run a container — no toolchain needed on the machine (the release page carries one file per architecture):
curl -fsSL https://github.com/stewlab/spod/releases/latest/download/spod-linux-amd64.tar.gz | tar -xz -C /tmp
install -m 755 /tmp/spod ~/.local/bin/spod
spod --version
spod pull docker.io/library/alpine:3.22
spod run --rm -it alpine:3.22 /bin/sh
Containers run on Linux — the engine needs the kernel (user namespaces,
runc/libcontainer). The darwin, windows, and freebsd builds manage images
and the store (pull, import, export, prune) and report an error for the
container lifecycle. Archives are spod-<os>-<arch>.tar.gz (.zip for
windows) plus a checksums.txt; curl-installed binaries are not
quarantined by macOS Gatekeeper. Building from source, including
cross-compiling, is below.
Building from Source
Build in a container — no Go toolchain or C compiler needed on the host:
podman build --target binary --output type=local,dest=bin . # emits bin/spod
mkdir -p ~/.local/bin && install -m 755 bin/spod ~/.local/bin/spod
spod --version
podman build --tag spod . # builds the distribution image
podman build --target test . # runs the test suite
install (rather than cp) replaces the file by rename, so the install
succeeds even while a previous spod process is still running from the old
binary — cp fails with "Text file busy" in that situation.
The binary is fully static (CGO against musl), -trimpath, stripped
with -s -w. --target binary exports just the binary into bin/; the
default build finishes at the minimal Alpine image packaging the engine
for distribution. --target test runs the suite in-container; a failing
test fails the build.
Once one spod binary exists, it can build the next one inside its own container:
spod run --rm -v "$PWD:/src" -w /src golang:1.25-alpine \
sh -c 'apk add --no-cache git gcc musl-dev linux-headers &&
GOWORK=off CGO_ENABLED=1 go build -trimpath -tags netgo,osusergo \
-ldflags "-s -w -linkmode external -extldflags -static" -o bin/spod ./cmd/spod'
install -m 755 bin/spod ~/.local/bin/spod
spod --version (one line) and spod version (full build table) name the build: the tag when built from one (a
go install github.com/stewlab/spod/cmd/spod@v0.1.0 binary included),
dev-<commit> for checkout builds — -dirty when the checkout carries
uncommitted changes — and dev when no git metadata is available.
Build on the host instead if you have Go 1.25+, a C compiler, and a static libc:
GOWORK=off CGO_ENABLED=1 go build -trimpath -tags 'netgo osusergo' \
-ldflags '-s -w -linkmode external -extldflags "-static"' -o bin/spod ./cmd/spod
GOWORK=off go test ./...
install -m 755 bin/spod ~/.local/bin/spod
Cross-compiling
The Linux targets cross-compile in the container: GOARCH selects amd64
(default), arm64, riscv64, 386, arm — zig cc carries musl, no per-target
toolchain, no emulation; Alpine's package pin holds the Zig version.
podman build --build-arg GOARCH=arm64 --target binary --output type=local,dest=bin .
podman build --build-arg GOARCH=riscv64 --target binary --output type=local,dest=bin .
The darwin, windows, and freebsd targets build with plain Go and CGO off
— no Apple SDK needed: image pulls, store management, and import/export
work, while the container lifecycle reports ErrUnsupported. The
release workflow ships all of them.
Testing the full lifecycle
Tests that run real containers are opt-in; this exercises the full
lifecycle against docker.io/library/alpine:3.22, while plain tests stay
offline:
SPOD_REAL_LIFECYCLE_TEST=1 GOWORK=off go test ./pkg/engine/ -run TestRealLifecycleAlpine
Commands
spod pull docker.io/library/alpine:3.22
spod run --rm -it -v "$PWD:/workspace" -w /workspace alpine:3.22 /bin/sh
spod run -d --name demo alpine:3.22 sleep infinity
spod exec -it demo /bin/sh
spod attach demo
spod inspect --format '{{.Status}}|{{.Image}}' demo
spod ps
spod stop demo && spod start demo
spod logs --tail 20 demo
spod rm demo
spod rename demo demo2
spod images
spod rmi alpine:3.22
spod volume ls && spod volume rm demo-home
spod prune # or: spod prune --force
spod export demo -o demo-rootfs.tar
spod import probe/base:latest demo-rootfs.tar
spod import-oci alpine:3.22 alpine-saved.tar
exportsnapshots a container's root filesystem, changes included;importturns an archive into a stored image, making captured filesystems reproducible starting points.import-ocistores a registry image from an archive — podman or dockersaveoutput, either OCI layout or docker-save format — which is how images land in the store from another engine's storage without a download.renamechanges a container's name in place, mount definitions included;volume ls/volume rmmanage named volumes.pruneremoves stored images no container uses. It lists what would go and asks first (default No); scripts pass--force. Images still in use are never offered, and removals land in the store's trash either way.attachstreams a detached container's live output and, when it was created withrun -di, forwards stdin. Detached TTY is not supported; useexec -itfor a terminal.- Storage follows the platform data directory (
~/.local/share/spod/storageon Linux). Override withSPOD_ROOTor--root; overrides must be absolute. Removed resources move to the store'strash/directory rather than being deleted outright.
Embedding
pkg/engine exposes image and lifecycle operations; pkg/cli provides a
Cobra command tree. Every embedding executable must call engine.Init()
as its first operation in main — libcontainer re-executes the binary to
bootstrap containers. cli.StyleHelp(cmd) applies the shared help styling
to any command tree.
func main() {
engine.Init()
// ...application setup and commands
}
Contributing & Releases
Development happens on short-lived feature branches off main, merged
through pull requests gated by CI. The conventions live in
CONTRIBUTING.md; the maintainer release runbook — how tags become
releases and how versions are stamped — is RELEASING.md.
Boundaries
- Host networking only; isolated network modes are rejected, not silently
mapped to host. The host's
/etc/resolv.confand/etc/hostsare bind- mounted read-only so name resolution works; a container mounting either path itself overrides that. - No image builds, overlayfs, privileged mode, cgroup limits, SELinux
relabeling, or NVIDIA CDI.
:z/:Zmount suffixes are accepted without relabeling. - Bind mounts hand containers host files, devices, and sockets with the invoking user's privileges; this is not a sandbox for untrusted code.
- Image ownership is normalized to the invoking user. Single-UID mapping covers container UID/GID 0; other image users need subordinate mappings.
- Images and volumes held by containers refuse removal.
- The container lifecycle requires Linux with CGO; on other platforms —
and on Linux builds without CGO — image and store operations work and
lifecycle methods report
ErrUnsupported.