Apple Container Lifecycle Management: How the System Orchestrates Create, Run, and Delete Operations

Apple Container implements a full-cycle management model that orchestrates container creation, bootstrap, execution, and deletion through a dedicated container-apiserver and XPC-based helper processes.

Apple Container is Apple's open-source container runtime that manages Linux containers on macOS using lightweight virtualization. The lifecycle management for containers follows a distinct create-bootstrap-run-stop-delete flow coordinated by the container-apiserver launch agent and its XPC helpers. This architecture ensures that each container operates in isolation while sharing system resources efficiently.

The Eight-Phase Container Lifecycle

Apple Container divides container management into eight distinct phases, from system initialization to resource teardown. Each phase is handled asynchronously via Swift's async/await and isolated in separate processes to prevent failures from cascading between containers.

Phase 1: System Start

Before any container runs, the system must initialize its infrastructure. The container system start command launches the container-apiserver launch agent, which spawns three critical XPC helpers:

  • container-core-images – Manages the image store and OCI image pulling
  • container-network-vmnet – Handles virtual network configuration via the vmnet framework
  • container-runtime-linux – One instance per container that creates lightweight Linux VMs

This initialization is documented in docs/technical-overview.md and implemented in Sources/APIServer/APIServer+Start.swift.

Phase 2: Create

When you execute container run, the ContainerCLI (Sources/CLI/ContainerCLI.swift) parses arguments and delegates to ContainerRun.run in Sources/ContainerCommands/Container/ContainerRun.swift. This method:

  1. Generates a unique container ID via Utility.createContainerID(name:)
  2. Builds a container configuration from command-line flags (image, resources, network)
  3. Calls client.create(configuration:options:) to register the container with the apiserver

The container exists in a configured but not yet running state after this phase.

Phase 3: Bootstrap (Start)

After creation, the CLI immediately bootstraps the container. In ContainerRun.run, the code opens a stdio conduit using ProcessIO.create and calls:

let process = try await client.bootstrap(
    id: id, 
    stdio: io.stdio, 
    dynamicEnv: [:]
)

This XPC request asks the container-runtime-linux helper to start the init process inside a lightweight VM that backs the container. The helper returns a ContainerProcess object representing the running init process.

Phase 4: Run and Attach

The runtime behavior depends on the execution mode:

  • Detached mode (--detach): The CLI calls process.start() asynchronously, prints the container ID, and exits. The container continues running in the background.
  • Interactive mode: The CLI forwards stdin/stdout/stderr to the init process via io.handleProcess, blocking until the process exits.

This logic appears in ContainerRun.swift lines 54-62, where the code branches based on the detach flag.

Phase 5: Stop and Signal Handling

The runtime monitors Unix signals through a SignalThreshold mechanism. If the process receives three SIGINT or SIGTERM events, the system forces a hard exit. When the init process exits naturally, ContainerRun captures the exit code and returns it as the CLI exit status.

Signal handling occurs in the same ContainerRun.run method, ensuring that foreground containers properly propagate exit codes to the shell.

Phase 6: Cleanup on Failure

If any lifecycle step throws an exception, the CLI executes error-path cleanup. The ContainerRun.run method includes a defer or catch block that calls:

client.delete(id: containerId)

This ensures the system does not retain stale entries for partially-created containers, preventing resource leaks and ID conflicts.

Phase 7: Delete (Explicit)

When you run container rm <id>, the CLI invokes client.delete(id:) from the ContainerClient module. This RPC triggers the apiserver to:

  1. Tear down the lightweight VM
  2. Remove the sandbox directory
  3. Free the container ID in the persistence layer

The deletion is handled by the ContainerClient.delete implementation in the ContainerAPIClient package.

Phase 8: System Stop

The container system stop command terminates the entire ecosystem. The apiserver shuts down all helper processes (image store, network daemon, and per-container runtimes), releasing every VM resource. This gracefully concludes the lifecycle management cycle.

Architecture Components Supporting Lifecycle Management

Several key components collaborate to manage container lifecycles:

ContainerCLI (Sources/CLI/ContainerCLI.swift) The entry point that parses command-line arguments and forwards them to the Application framework.

Application A thin wrapper around the container-system configuration and the XPC client that coordinates between CLI commands and the apiserver.

ContainerClient (ContainerAPIClient package) Sends asynchronous RPCs (create, bootstrap, delete, stop) over XPC to the apiserver, abstracting the inter-process communication complexity.

container-apiserver A launch-agent (Sources/APIServer/APIServer.swift) that owns the in-process state (ContainerSystemConfig) and marshals requests to the appropriate XPC helpers.

container-runtime-linux Per-container XPC helper that creates lightweight Linux VMs, starts init processes, and forwards I/O between the host and guest.

container-core-images and container-network-vmnet Shared XPC helpers managing the image store and virtual network interface, respectively.

ContainerPersistence Maintains on-disk JSON/YAML representations of containers, networks, and system configuration (e.g., Sources/ContainerPersistence/ContainerSystemConfig.swift).

Practical Lifecycle Commands

Below are the shell commands that trigger each lifecycle phase, with their underlying Swift implementations.

Start the Container System

container system start

Spawns the apiserver and XPC helpers as described in the technical overview.

Create and Run a Container


# Detached execution

container run --detach --name myapp ghcr.io/example/app:latest

# Interactive execution

container run -it --name myapp ghcr.io/example/app:latest /bin/bash

Underlying implementation:

let id = Utility.createContainerID(name: "myapp")
try await client.create(configuration: cfg, options: .init(autoRemove: false))
let process = try await client.bootstrap(id: id, stdio: io.stdio, dynamicEnv: [:])
try await process.start()

Execute Commands in Running Containers

container exec -ti myapp /bin/bash

Opens a new process inside the existing VM using the same XPC client connection.

Stop and Remove Containers

container stop myapp    # Sends graceful stop signal

container rm myapp      # Removes configuration and VM resources

Translates to:

try await client.stop(id: "myapp")
try await client.delete(id: "myapp")

Shut Down the System

container system stop

Terminates the apiserver and all XPC helpers, releasing every lightweight VM.

Summary

  • Apple Container implements an eight-phase lifecycle covering system start, create, bootstrap, run, stop, cleanup, delete, and system stop.
  • The container-apiserver launch agent coordinates all lifecycle operations through XPC helpers for images, networking, and runtime.
  • Container IDs are generated via Utility.createContainerID and managed persistently through ContainerSystemConfig.
  • Lifecycle operations are asynchronous (using Swift async/await) and process-isolated, ensuring failures don't cascade between containers.
  • The ContainerRun.run method in Sources/ContainerCommands/Container/ContainerRun.swift implements the core create-bootstrap-run logic with automatic cleanup on failure.

Frequently Asked Questions

How does Apple Container generate unique container IDs?

Apple Container generates unique identifiers through the Utility.createContainerID(name:) function, which creates a unique string based on the provided name and internal entropy. This ID is used throughout the lifecycle in Sources/ContainerCommands/Container/ContainerRun.swift and persists in the ContainerPersistence layer until explicitly deleted.

What happens if a container fails to start during the bootstrap phase?

If the bootstrap phase fails or any preceding step throws an exception, the CLI executes automatic cleanup by calling client.delete(id:) to remove the partially-created container. This prevents the system from retaining stale entries and ensures that failed containers don't consume resources or block future ID allocations.

How does Apple Container handle process signals during shutdown?

The runtime implements a SignalThreshold monitor that tracks SIGINT and SIGTERM events. After receiving three consecutive signals, the system forces a hard exit of the container process. For graceful shutdowns, the runtime captures the init process exit code and propagates it to the CLI, ensuring proper shell exit status reporting.

What is the difference between container stop and container delete?

Stop (container stop) sends a signal to the running init process via the runtime helper, gracefully terminating the container process while preserving the container configuration and VM state for potential restart. Delete (container rm) invokes the apiserver's delete RPC, which tears down the VM, removes the sandbox directory, and frees the container ID from persistence, completely removing all traces of the container from the system.

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 →