How Apple Container Manages Storage: Filesystem Persistence Architecture

Apple Container uses a file‑system‑backed persistence layer where volumes, image snapshots, and metadata are stored as regular files and JSON documents on macOS, managed by VolumesService, SnapshotStore, and FilesystemEntityStore.

The apple/container repository provides a macOS-native container runtime that avoids complex storage drivers by leveraging the host file system for all persistence. Understanding how apple container manages storage reveals a synchronous, file-based architecture where virtual disks appear as ordinary files that standard macOS tools can inspect and manage.

Core Storage Components

Apple Container organizes disk state into four distinct categories, each handled by dedicated services in the codebase.

Volumes and Block Storage

User-defined data volumes are stored as block device images accompanied by JSON configuration files. According to the source code, VolumesService (in Sources/Services/ContainerAPIService/Server/Volumes/VolumesService.swift) creates a directory structure under resourceRoot/volumes/<volume-name>/ containing:

  • volume.img: The actual block file representing the virtual disk
  • entity.json: Metadata describing the volume name, driver, labels, and size quota

Disk usage is calculated using FileManager.default.allocatedSize(of:) on the block file, allowing the system to report precise consumption through volumeDiskUsage(name:) and aggregate totals via calculateDiskUsage().

Image Snapshots

Unpacked OCI image layers reside as EXT4 file systems in the snapshots/<digest>/ directory. The SnapshotStore (in Sources/Services/ContainerImagesService/Server/SnapshotStore.swift) manages these by:

  • Writing snapshot (the block file) and snapshot-info (JSON description) after unpacking
  • Maintaining an "ingest" staging area for atomic moves
  • Running clean(keepingSnapshotsFor:) to remove unreferenced snapshots and return reclaimed byte counts

Metadata Persistence

All entity descriptions are persisted via FilesystemEntityStore (in Sources/ContainerPersistence/EntityStore.swift). This store:

  • Loads all entity.json files at startup into an in-memory index
  • Provides synchronous CRUD operations used by both VolumesService and NetworksService
  • Ensures consistency between the file system state and the runtime's view of resources

Storage Quotas

Per-container limits are declared in ContainerConfiguration.swift (Sources/ContainerResource/Container/ContainerConfiguration.swift) via the storage: UInt64? field. When starting a container, the runtime validates the requested size against a minimum of 1 MiB, raising VolumeError.storageError for invalid configurations.

Volume Lifecycle Operations

Creating, inspecting, and deleting volumes follows a predictable file-based workflow.

Creating a Volume

When you run container volume create mydata, the CLI invokes VolumesService.create, which locks the resource and writes the block file and metadata:

// In VolumesService.swift
public func create(name: String, driver: String = "local", …) async throws -> VolumeConfiguration {
    return try await lock.withLock { _ in
        try await self._create(name: name, driver: driver, )
    }
}

The method atomically creates volume.img and entity.json through the FilesystemEntityStore.

Inspecting Disk Usage

To check a volume's size or system-wide storage:

container volume inspect mydata
container system df      # shows total and reclaimable storage

The underlying implementation uses FileManager.default.allocatedSize:

let volumePath = self.volumePath(for: name)
return FileManager.default.allocatedSize(of: URL(fileURLWithPath: volumePath))

Deleting Volumes

VolumesService.delete removes both the block file and its corresponding entity.json, while FilesystemEntityStore drops the entry from its in-memory index, immediately reclaiming disk space.

Image Snapshot Management

The snapshot system handles unpacked container layers with an emphasis on atomic operations and garbage collection.

Unpacking Layers

SnapshotStore.unpack extracts image layers into a temporary directory before atomically moving them to snapshots/<digest>/. This prevents partial writes from corrupting the storage.

Reclaiming Space

To clean up unused snapshots programmatically:

let reclaimed = try await snapshotStore.clean(keepingSnapshotsFor: [image])
print("Reclaimed \(reclaimed) bytes")

This method scans the snapshots/ directory, preserves only digests referenced by retained images, and returns the total bytes freed.

Storage Quota Enforcement

Container-level storage limits are enforced at startup. When using the CLI:

container run --storage 10g myimage

The flag populates the storage field in ContainerConfiguration. The runtime validates this value against the 1 MiB minimum, throwing VolumeError.storageError if the quota is too small.

Summary

Frequently Asked Questions

How does Apple Container track disk usage for volumes?

Apple Container tracks disk usage by calling FileManager.default.allocatedSize(of:) on the volume.img block file. The VolumesService.volumeDiskUsage(name:) method returns this value, while calculateDiskUsage() aggregates sizes across all volumes to report total, active, and reclaimable storage counts.

Where are container image layers stored in Apple Container?

Image layers are stored as unpacked EXT4 file systems in snapshots/<digest>/, containing a snapshot block file and snapshot-info JSON metadata. The SnapshotStore in Sources/Services/ContainerImagesService/Server/SnapshotStore.swift manages these directories, using an "ingest" staging area for atomic writes.

What happens when a volume is deleted in Apple Container?

When VolumesService.delete is invoked, it removes both the volume.img block file and the entity.json metadata from resourceRoot/volumes/<volume-name>/. The FilesystemEntityStore automatically removes the entry from its in-memory index, synchronously reclaiming the disk space on the host file system.

How are storage quotas enforced for individual containers?

Storage quotas are declared in the storage: UInt64? field of ContainerConfiguration (defined in Sources/ContainerResource/Container/ContainerConfiguration.swift). When a container starts, the runtime validates the requested size against a minimum of 1 MiB and raises VolumeError.storageError if the configuration is invalid.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →