# How the CLI Client Communicates with the Background API Server in Apple Container

> Discover how the CLI client communicates with the background API server in Apple Container using Unix-domain sockets and gRPC with the ContainerAPIClient library.

- Repository: [Apple/container](https://github.com/apple/container)
- Tags: internals
- Published: 2026-06-18

---

**The CLI client communicates with the background API server via Unix-domain sockets using gRPC-compatible protobuf messages through the ContainerAPIClient library.**

The `apple/container` repository implements a client-server architecture that separates the lightweight command-line interface from the heavy-lifting container runtime daemon. Understanding how the CLI client communicates with the background API server reveals a high-performance transport layer built on local sockets and strongly typed RPC calls. This design keeps the CLI thin while delegating complex container operations to the persistent daemon process.

## Transport Layer: Unix-Domain Sockets

The communication channel relies exclusively on **Unix-domain sockets** for local inter-process communication. When the API server starts, it creates a socket file at a platform-specific path: `/var/run/container/...` on macOS and `/run/container/...` on Linux (as implemented in `Sources/APIServer/APIServer+Start.swift`).

The server uses SwiftNIO to bind to this socket. In `APIServer+Start.swift`, the bootstrap process configures a `MultiThreadedEventLoopGroup` and binds the server to the Unix-domain socket path:

```swift
// APIServer+Start.swift – server side socket setup
import NIO
import NIOTransportServices

func startAPIServer() throws {
    let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
    let server = ServerBootstrap(group: group)
        .childChannelInitializer { channel in
            return channel.pipeline.addHandlers([
                GRPCServerHandler(serviceProviders: [ContainerServiceProvider()]),
                // …
            ])
        }
    let channel = try server.bind(unixDomainSocketPath: "/run/container/api.sock").wait()
    try channel.closeFuture.wait()
}

```

This setup creates a persistent listening socket that accepts connections from the CLI client.

## Protocol Layer: gRPC-Compatible Protobuf Messages

All requests and responses are encoded as **gRPC-compatible protobuf messages**. The project uses the same `.proto` definitions on both client and server sides, with generated Swift stubs providing the type-safe interface.

The generated client stubs live in [`Sources/ContainerAPIClient/ContainerAPI.grpc.swift`](https://github.com/apple/container/blob/main/Sources/ContainerAPIClient/ContainerAPI.grpc.swift) and are imported via the `ContainerAPIClient` module. These stubs define methods such as `createContainer()` and `listImages()` that correspond directly to the RPC service definitions.

## Client-Side Implementation

The CLI entry point resides in [`Sources/CLI/ContainerCLI.swift`](https://github.com/apple/container/blob/main/Sources/CLI/ContainerCLI.swift), which imports the `ContainerAPIClient` module and delegates to `Application.main()`:

```swift
// ContainerCLI.swift – entry point
import ContainerAPIClient               // ← brings in the client stubs
...
public static func main() async throws {
    try await Application.main()        // parses args, validates, runs
}

```

Individual commands instantiate the client with a specific socket path and timeout configuration. For example, in [`Sources/ContainerCommands/Container/ContainerList.swift`](https://github.com/apple/container/blob/main/Sources/ContainerCommands/Container/ContainerList.swift), the command creates a `ContainerAPIClient.Client` and invokes the appropriate RPC method:

```swift
// Example: listing containers (ContainerCommands/Container/ContainerList.swift)
import ContainerAPIClient

func listContainers() async throws {
    let client = ContainerAPIClient.Client(
        socketPath: "/run/container/api.sock",
        timeout: .seconds(30)
    )
    let request = ContainerAPI_ListContainersRequest()
    let response = try await client.listContainers(request)
    // format and print `response.containers`
}

```

The client internally uses SwiftNIO's `Channel` to write the protobuf payload to the socket and reads the response back asynchronously.

## Server-Side Implementation

The daemon runs the `APIServer` class defined in [`Sources/APIServer/APIServer.swift`](https://github.com/apple/container/blob/main/Sources/APIServer/APIServer.swift). It binds a `NIOAsyncChannel` to the same Unix-domain socket used by the client and registers generated gRPC service handlers.

The server dispatches incoming RPC calls to specific service implementations such as `ContainerService` (defined in [`Sources/APIServer/ContainerService.swift`](https://github.com/apple/container/blob/main/Sources/APIServer/ContainerService.swift)) and `MachineService`. Each handler deserializes the protobuf request, executes the corresponding container operation, and serializes the response back to the client over the socket.

## Lifecycle and Daemon Management

When the CLI starts, `Application.main()` (defined in [`ContainerCLI.swift`](https://github.com/apple/container/blob/main/ContainerCLI.swift)) checks for a running daemon. If no API server is detected, the CLI automatically launches the `containerd` daemon in the background, waits for the socket file to become available, and then proceeds to issue RPC calls.

This ensures that users can execute commands like `container run` or `container image list` without manually starting the service, while maintaining the architectural separation between the CLI and the runtime.

## Summary

- **Unix-domain sockets** provide the transport layer between CLI and daemon, located at `/var/run/container/` on macOS and `/run/container/` on Linux.
- **gRPC-compatible protobuf messages** encode all RPC calls through generated Swift stubs in the `ContainerAPIClient` module.
- The CLI imports `ContainerAPIClient` (as seen in [`ContainerCLI.swift`](https://github.com/apple/container/blob/main/ContainerCLI.swift)) and instantiates `ContainerAPIClient.Client` with socket path and timeout parameters.
- The server implementation in `APIServer+Start.swift` uses SwiftNIO's `ServerBootstrap` to bind to the Unix-domain socket and register service handlers.
- **Automatic daemon lifecycle management** ensures the API server is running before the CLI attempts communication, launching `containerd` if necessary.

## Frequently Asked Questions

### What transport protocol does the Apple Container CLI use to communicate with the API server?

The CLI uses **Unix-domain sockets** for local inter-process communication rather than TCP/IP. This provides lower latency and better security for local daemon communication, with socket files located at platform-specific paths under `/var/run/container/` on macOS and `/run/container/` on Linux.

### How does the CLI locate the API server socket?

The CLI is configured with explicit socket paths passed to `ContainerAPIClient.Client` initialization (for example, `socketPath: "/run/container/api.sock"`). When the CLI starts, it checks for the existence of this socket file to determine if the daemon is already running, as implemented in the `Application.main()` logic within [`ContainerCLI.swift`](https://github.com/apple/container/blob/main/ContainerCLI.swift).

### What happens if the daemon is not running when the CLI starts?

If `Application.main()` detects that the API server is not running, the CLI automatically launches the `containerd` daemon in the background. It then waits for the Unix-domain socket to become available before proceeding to establish the gRPC connection and execute the requested command.

### Are the gRPC stubs generated from proto files in the Apple Container project?

Yes, the client-side code uses generated Swift stubs (located in [`Sources/ContainerAPIClient/ContainerAPI.grpc.swift`](https://github.com/apple/container/blob/main/Sources/ContainerAPIClient/ContainerAPI.grpc.swift)) that are produced from the same `.proto` definitions used by the server. These generated files provide type-safe methods like `listContainers()` and `createContainer()` that the CLI imports via `import ContainerAPIClient`.