How Container Manages Image Storage and Layers Locally: A Deep Dive into Apple's OCI Implementation
Container manages image storage and layers locally through a three-tier architecture comprising ContentStore for raw blobs, SnapshotStore for unpacked filesystems, and ImageStore for metadata orchestration, all following OCI specifications.
The apple/container repository implements an OCI-compatible container runtime that stores images in a layered, content-addressable format. Understanding how container manages image storage and layers locally reveals a sophisticated system designed for deduplication, lazy loading, and efficient garbage collection. The implementation splits responsibilities across three cooperating Swift services that persist data to JSON-backed filesystem stores.
The Three-Store Architecture
The storage layer is partitioned into specialized components, each handling a distinct aspect of image management. Separation of concerns allows for efficient deduplication and on-demand filesystem preparation.
ContentStore: Raw Blob Storage
The ContentStore handles the raw binary objects that constitute an image. According to the source code in Sources/Services/ContainerImagesService/Server/ContentStoreService.swift, this store saves compressed layer tarballs, configuration JSON, and manifests under the content directory.
Each blob is addressed by its SHA-256 digest and written to <root>/content/<digest>, where <root> typically resolves to ~/.container. The save(blob:) method ensures content-addressability by using the digest as the filename, guaranteeing that identical layers from different images share the same physical file.
SnapshotStore: Filesystem Layer Management
The SnapshotStore, implemented in Sources/Services/ContainerImagesService/Server/SnapshotStore.swift, manages unpacked filesystem snapshots. When a layer is needed for execution, the store lazily extracts the corresponding blob into <root>/snapshots/<digest>.
This component tracks reference counts to determine when a snapshot can be safely removed. The unpack(image:platform:) method handles extraction, while get(for:image:platform:) retrieves existing snapshots or triggers unpacking on demand.
ImageStore: Metadata Orchestration
The ImageStore resides in Sources/Services/ContainerImagesService/Server/ImagesService.swift and maintains higher-level metadata. It records image manifests, tags, and references, mapping human-readable names like docker.io/library/alpine:latest to lists of layer digests.
Key operations include save(references:out:platform:) for recording new images and delete(reference:performCleanup:) for removal. The store persists manifest data under <root>/images/<name>.json using the FilesystemEntityStore generic persistence layer defined in Sources/ContainerPersistence/EntityStore.swift.
The Image Pull Workflow
When executing container image pull, the system coordinates all three stores to populate local storage while maintaining OCI compliance.
-
Fetch manifests and descriptors – The CLI retrieves the manifest JSON from the remote registry, which contains a list of layer digests and the configuration digest.
-
Store blobs – Each layer digest is passed to
ContentStore.save(blob:), which writes the compressed tarball to the content directory using its digest as the filename. -
Create snapshots –
SnapshotStore.unpack(image:platform:)lazily extracts layers only when the image is run, creating directories under<root>/snapshots/<digest>. Reference counting ensures shared layers are tracked correctly. -
Record the image –
ImageStore.save(references:out:platform:)writes a JSON manifest that binds the image name to its constituent layer digests and configuration.
Runtime Snapshot Management
During container run, the runtime requests the image manifest from ImageStore. For each layer digest in the manifest, the runtime calls SnapshotStore.get(for:image:platform:). If the snapshot exists, it is reused; otherwise, the store fetches the blob from ContentStore and unpacks it.
The snapshots are mounted in order, with lower layers first and the top layer last, into the VM's rootfs. Because snapshots are immutable and content-addressed, they can be safely shared between concurrent containers, significantly reducing disk usage.
Garbage Collection and Pruning
The container image prune command traverses ImageStore to identify unreferenced tags, then invokes ImageStore.delete(reference:performCleanup:). This routine removes the manifest entry and notifies ContentStore to delete blobs whose reference counts drop to zero.
Orphaned snapshots are reclaimed by SnapshotStore.clean(keepingSnapshotsFor:), which removes snapshot directories no longer associated with any stored image. The EntityStore persistence layer ensures all JSON writes are atomic and logged for debugging purposes.
Content-Addressed Deduplication
Since all layers are content-addressed by digest, two different images sharing a base layer reference the exact same blob file in <root>/content/ and the same snapshot directory in <root>/snapshots/. This automatic deduplication is enforced by reference-counting logic within the stores, requiring no additional CLI bookkeeping.
The ImageResource model defined in Sources/ContainerResource/Image/ImageResource.swift encapsulates the relationship between names, tags, and digests, ensuring that storage operations remain consistent across the entire system.
Summary
- Three-tier storage separates blob storage (
ContentStore), filesystem snapshots (SnapshotStore), and metadata management (ImageStore) for clean separation of concerns. - Content-addressability ensures layers are stored once under
<root>/content/<digest>and shared across images via reference counting. - Lazy unpacking defers layer extraction until runtime via
SnapshotStore.unpack(), optimizing disk usage for pulled images that are never run. - Atomic persistence is provided by
FilesystemEntityStoreinEntityStore.swift, which writes JSON files atomically with debug logging. - Automatic deduplication occurs naturally because layers are identified by SHA-256 digest, eliminating redundant storage of shared base images.
Frequently Asked Questions
Where does Container store image layers on disk?
Container stores image layers under the content directory within its data root (typically ~/.container). Each compressed layer blob is saved as a file named by its SHA-256 digest at <root>/content/<digest>. Unpacked filesystem snapshots reside in parallel under <root>/snapshots/<digest>, while image metadata JSON files are kept in <root>/images/.
How does Container handle layer deduplication between different images?
Container achieves automatic deduplication through content-addressable storage. Since ContentStore.save(blob:) uses the SHA-256 digest as the filename, identical layers from different images write to the same path and are stored only once. The SnapshotStore maintains reference counts on unpacked layers, ensuring that shared layers remain available until all referencing images are deleted.
What happens when I run container image prune?
The prune command triggers ImageStore.delete(reference:performCleanup:) to remove unreferenced manifests. It then notifies ContentStore to delete blobs with zero references. Finally, SnapshotStore.clean(keepingSnapshotsFor:) removes orphaned snapshot directories whose layers are no longer needed by any stored image, reclaiming disk space from extracted filesystems.
Is the storage layout OCI-compatible?
Yes, the storage layout follows OCI image specifications. The ContentStore maintains blobs addressed by digest, while ImageStore preserves manifests and configuration JSON in a structure that mirrors the OCI image layout. This design allows for interoperability with standard OCI tools and registries.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →