CLI tool
containers/storage avatar
containers/storage

containers/storage is the Go layer store behind Podman, Buildah and Skopeo

Container Storage Library

645 stars274 forksGoApache-2.0

At a glance

What is it?
containers/storage is the Go library that gives Podman, Buildah and Skopeo their shared model of layers, images and containers on disk, with copy-on-write layers, names resolved to IDs, metadata and big data blobs. The module path has moved to go.podman.io/storage and new development happens in the container-libs monorepo, so new code should not start here.
Who is it for?
Adopt containers/storage only if you are maintaining one of the tools in that ecosystem, or if you have an existing dependency on github.com/containers/storage and need to keep it working, since the library itself now points you at go.podman.io/storage and the container-libs monorepo.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 168 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The library that decides what a container image is on disk

containers/storage is a Go library which aims to provide methods for storing filesystem layers, container images and containers, and a containers-storage CLI wrapper is included for manual and scripting use.

The library is the storage layer beneath a well known family of tools. The repository topics name buildah, containers, podman and skopeo, and those are the consumers of this API in practice. Podman needs somewhere to keep image layers and running container filesystems, Buildah needs to build images without a daemon, and Skopeo needs to copy images around, and all three of them agree on a common on-disk model so that an image built by one can be run by another.

That agreement is the point. If Podman and Buildah each had their own storage layout, every image would be tied to the tool that built it. Sharing one library means the format is a de facto standard among these tools, and it is why the design decisions in this library are conservative: they are about compatibility and durability rather than novelty.

The project is Apache-2.0 licensed, the primary language is Go, and the go.mod file declares Go 1.23.0 with a warning that the go and toolchain versions should match exactly to prevent unwanted auto-updates, and that there should generally be no toolchain directive present.

The repository is substantial and shows the shape of a mature storage system: separate files for layers, images and containers at the top level, a drivers directory for the pluggable backends, a types directory, internal packages, a cmd directory for the CLI, tests, hack scripts and a vendored dependency tree.

Layers, images and containers are three different objects

The core of the design is the distinction between the three item types, and the README defines each of them precisely.

A layer is a copy-on-write filesystem which is notionally stored as a set of changes relative to its parent layer, if it has one. A given layer can only have one parent, but any layer can be the parent of multiple layers, and layers which are parents of other layers should be treated as read-only.

An image is a reference to a particular layer, its top layer, along with other information which the library can manage for the convenience of its caller. That information typically includes configuration templates for running a binary contained within the image's layers, and may include cryptographic signatures. Multiple images can reference the same layer, since the differences between two images may not be in their layer contents.

A container is a read-write layer which is a child of an image's top layer, along with information the library manages for the caller, typically configuration information for running that specific container. Multiple containers can be derived from a single image.

The parent rule is the one constraint worth internalising. A layer has at most one parent, which gives the layer graph a tree shape, and the read-only guidance for parent layers is what makes sharing safe. If you try to make a layer the parent of two children and then modify it, both children change underneath you.

The fact that an image is mostly a pointer plus metadata is what makes tagging cheap. Renaming or adding a signature to an image does not copy the layers, and that is why the library can afford to store configuration templates and signatures alongside the content rather than baking them into it.

Names are resolved to 32 character hexadecimal IDs

Layers, images and containers are represented primarily by 32 character hexadecimal IDs, but items of each kind can also have one or more arbitrary names attached to them, which the library will automatically resolve to IDs when they are passed into API calls which expect IDs.

This is a small design decision with a large usability consequence. A caller can work with human names like a tag or a container name, and the resolution happens inside the library, so a caller that always uses IDs does not have to change and a caller that uses names does not have to build an index. It also means a name is not a first class identifier: two different names can point at the same object, and a name can be moved to a different object, so a program that treats a name as a stable handle is relying on a convention the library does not guarantee.

Alongside the object types, the library stores two kinds of payload. There is metadata for each of the three types, which is expected to be a small piece of data since it is cached in memory and stored along with the library's own bookkeeping information. And there is big data for images and containers, defined as a named chunk of larger data which is only in memory when it is being read from or written to its own disk file.

The split between metadata and big data is the key to the caching model. Metadata is small enough to keep in memory, and big data is large enough that keeping it there would be wasteful, so it lives in its own file and is streamed. A caller that assumes a chunk of big data is a byte slice in memory will be surprised.

Storage drivers, the config file and the CLI wrapper

The library is not tied to one filesystem. The drivers directory at the top of the repository holds the backend implementations, and the configuration is a separate file, storage.conf, with a storage.conf-freebsd variant alongside it for the platform whose conventions differ. The README also mentions operations which use VMs, and states that they expect to launch them using vagrant, defaulting to its libvirt provider, with the boxes also available for the virtualbox provider, selected by setting $VAGRANT_PROVIDER to virtualbox before kicking off the build.

That is a detail aimed at contributors rather than users, but it tells you the project supports a path where the storage lives inside a virtual machine rather than directly on the host, which matters for macOS and Windows where the host filesystem cannot provide the semantics a Linux container needs.

The CLI wrapper is the other thing worth knowing about. The README states it is included for manual and scripting use, and the Makefile has a target for building it. The Makefile's own description of the build is precise: containers-storage is the target for building using gc on the host, and there is a separate gccgo target that builds with gccgo through a wrapper script. The relevant command is:

bash
	$(GO) build -compiler gc $(BUILDFLAGS) ./cmd/containers-storage

The README gives the shorter form for the same thing, using make binary, since the binary target depends on the containers-storage target.

The build has more moving parts than a typical Go library. The Makefile auto-detects build tags through hack/btrfs_tag.sh and hack/libsubid_tag.sh, and the repository vendors its dependencies, with a vendor target and a vendor-in-container target for regenerating the tree. The default target runs a full local-binary, docs, local-validate and local-cross sequence, and the cross build covers a long target list including arm, darwin, freebsd, ppc64, riscv64, s390x and mips variants.

The move to go.podman.io/storage and the monorepo

The most important thing in the README is a warning at the very top, and any article about this project that ignores it would be misleading.

The warning says the package was moved and that references should be updated to use go.podman.io/storage instead. It says new development of this project happens at github.com/containers/container-libs, and it links a blog post about the upcoming migration of three containers repositories to a monorepo.

The module file in the repository says the same thing in a comment: the first line reads Deprecated: Use go.podman.io/storage instead. The module path itself is still github.com/containers/storage, and the go.mod file also carries the warning about the go and toolchain versions matching exactly, with no toolchain directive, to prevent unwanted auto-updates.

What this means practically is that this repository is a compatibility shell. Existing code that imports github.com/containers/storage can keep doing so, and the library still works, but any new work belongs in the monorepo or against the new module path. The three repositories being consolidated are the storage library and the related tooling, and the point of the migration is to release them together with one version scheme rather than separately.

The version history here shows the shape of that. The tags listed are v1.51.3 from 2026-05-08, and then v1.59.1 from 2025-08-04 and v1.59.0 from 2025-07-14. The numbers are not monotonic in date, which is a reminder that a library whose versioning has been reorganised across repositories is not a place to guess version semantics. The last push was on 2026-05-08, and the repository is not archived.

Where containers/storage is the wrong thing to reach for

There are two distinct wrong-tool cases, and they come from different directions.

The first is a greenfield integration. If you are writing something new that needs to store container images, starting against github.com/containers/storage means writing against a module path the project itself marks as deprecated. The two costs of that are a rename later and a dependency on a repository that is not where development happens. The Go module proxy and the new path make the move cheap, but it is still a move.

The second is expecting this to be a general purpose image store. The library implements a specific model, the one the Podman family agrees on, with layers having one parent, images as references to a top layer, and containers as read-write children. That model is a deliberate choice about how the filesystem is shared, and it is not the same contract as talking to a registry. The registry protocol is about manifests and blobs, the storage model is about local filesystem state, and confusing the two leads to designs where an image can be pulled but not run.

The alternative approach, and the one the ecosystem is converging on, is to use the new module path and the monorepo directly. The difference in approach is organisational rather than technical: the same library, the same storage format and the same drivers, but released and versioned as one unit with the tools that depend on it, so a storage change and a Podman change arrive in the same release rather than in two independent ones.

For a team already deep in this ecosystem, that consistency is the real argument. A storage layout change that ships independently of the tools that read it is a class of breakage, and keeping everything in one repository with one version line is how the project eliminates it.

Editorial conclusion

Adopt containers/storage only if you are maintaining one of the tools in that ecosystem, or if you have an existing dependency on github.com/containers/storage and need to keep it working, since the library itself now points you at go.podman.io/storage and the container-libs monorepo. Do not start a new integration against this module path, and do not treat it as a general purpose container store you can point at arbitrary Docker images, because the model it implements is the one Podman, Buildah and Skopeo agree on rather than a public registry contract. Verify first that your import path matches the module you resolve, that the storage driver configuration in storage.conf matches the filesystem your host provides, and that the version you pin, with v1.51.3 the most recent tag listed here, behaves the way the tools you depend on expect.

Frequently asked questions

What is the containers/storage Go library used for?

It provides methods for storing filesystem layers, container images and containers, and a containers-storage CLI wrapper for manual and scripting use. It is the storage layer used by the Podman, Buildah and Skopeo family of tools, so an image built by one can be run by another.

What is the difference between a layer, an image and a container in containers/storage?

A layer is a copy-on-write filesystem stored as changes relative to its parent layer, and a layer has at most one parent. An image is a reference to a particular layer, its top layer, plus information the library manages such as configuration templates and possibly cryptographic signatures. A container is a read-write layer which is a child of an image's top layer, plus configuration for running that container.

Has containers/storage been moved to a new module path?

Yes. The README carries a warning that the package was moved and that references should be updated to use go.podman.io/storage, and the go.mod file is marked Deprecated. New development happens in the container-libs monorepo at github.com/containers/container-libs, with a blog post explaining the migration of three containers repositories.

How do I build the containers-storage CLI?

The README says to use make binary, and the Makefile defines the binary target in terms of the containers-storage target, which runs $(GO) build -compiler gc $(BUILDFLAGS) ./cmd/containers-storage. There is also a separate gccgo target that builds with gccgo through a wrapper script.

How does the library tell layers, images and containers apart?

They are represented primarily by 32 character hexadecimal IDs, but each item can also have one or more arbitrary names attached, and the library automatically resolves those names to IDs when they are passed into API calls which expect IDs.

Official sources

  1. containers/storage on GitHub
  2. Issues
  3. License: Apache-2.0
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/containers-storage.svg)](https://hysenlabs.com/projects/containers-storage)

Was this useful?