How the Container CLI Communicates with the API Server: gRPC over Unix Domain Sockets

The container CLI communicates with the API server via gRPC over a Unix domain socket, using the ContainerAPIClient library to marshal protobuf requests through HTTP/2 without requiring custom HTTP logic.

The apple/container repository implements a container runtime architecture where the command-line interface (CLI) and the API server (container-apiserver) run as independent Swift executables on the same host. This design relies on a type-safe Remote Procedure Call (RPC) mechanism that keeps the CLI lightweight while delegating container operations to the server process.

The Communication Architecture

The CLI and API server communicate through a local Unix domain socket rather than TCP ports or REST endpoints. This architecture provides several technical advantages:

  • Transport Layer: HTTP/2 gRPC via Unix domain socket (/var/run/container.sock by default)
  • Client Library: ContainerAPIClient containing auto-generated gRPC stubs
  • Serialization: Protocol Buffers (protobuf) for message exchange
  • Network Stack: SwiftNIO via GRPCSwift (grpc-swift-2) for asynchronous I/O

When you execute a command like container start, the CLI creates a client from the generated stubs and sends RPC calls over the socket. The server receives these protobuf messages, executes the corresponding container operation, and returns serialized responses.

Client-Side Implementation

Entry Point and Command Routing

The CLI entry point resides in Sources/CLI/ContainerCLI.swift, where the @main struct creates an Application using ArgumentParser. This application registers all sub-commands in Sources/ContainerCommands/Application.swift (generated by ArgumentParser), mapping each CLI verb to its corresponding implementation.

For example, when you run container start, the command resolves to ContainerStart.swift in Sources/ContainerCommands/Container/.

Establishing the gRPC Connection

Each sub-command instantiates a client using the ContainerAPIClient library. In Sources/ContainerCommands/Container/ContainerStart.swift, the implementation follows this pattern:

import ContainerAPIClient

let client = try ContainerAPIClient.makeClient(socketPath: "/var/run/container.sock")
let request = ContainersService_StartRequest(containerID: id)
let response = try await client.containers.start(request)

The makeClient method internally constructs a GRPCChannel defined in Sources/ContainerAPIClient/GRPCChannel.swift:

let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
let channel = try GRPCChannelPool.withUnixDomainSocket(path: socketPath, group: group)

This channel uses SwiftNIO (NIOCore) to manage the Unix socket connection, while GRPCSwift handles the HTTP/2 framing and gRPC protocol specifics.

Generated Service Stubs

The actual RPC methods are defined in Sources/Services/ContainerAPIService/Client/ContainersService.swift, which contains the auto-generated ContainersServiceClient class. This stub is generated from the protobuf definitions (e.g., containers.proto) and provides type-safe methods like start(), list(), and run() that the CLI calls directly.

Server-Side Implementation

Binding to the Unix Socket

The API server initialization occurs in Sources/APIServer/APIServer+Start.swift. The APIServer.main() method constructs a GRPCServer and binds it to the Unix socket path:

// In Sources/APIServer/APIServer+Start.swift
let server = GRPCServer(group: group, serviceProviders: [ContainersServiceProvider()])
try server.bind(unixDomainSocketPath: "/var/run/container.sock")

Handling Incoming Requests

The server implementation in Sources/Services/ContainerAPIService/Server/ContainersService.swift registers the same protobuf service interface. When the CLI sends an RPC, the server receives the deserialized protobuf message, executes the container operation using the runtime engine, and serializes the response back to the client over the same HTTP/2 connection.

Auxiliary HTTP Communication

While container operations use gRPC, file transfers such as image pulls and kernel downloads use a separate HTTP client. The ContainerAPIClient.FileDownloader class (defined in Sources/ContainerAPIClient/FileDownloader.swift) utilizes AsyncHTTPClient to perform standard HTTP GET/PUT operations. These calls are independent of the gRPC channel and do not travel through the Unix socket to the API server.

Configuration and Socket Path

By default, the CLI and server communicate through /var/run/container.sock. You can specify an alternative path using the --socket flag:


# Start the server on a custom socket

container-apiserver --socket /tmp/custom.sock &

# CLI automatically uses the same socket path

container --socket /tmp/custom.sock list

The entire exchange remains within the host kernel's networking stack, avoiding TCP overhead and providing lower latency than network-based communication.

Summary

  • The container CLI uses the ContainerAPIClient library to communicate with the API server via gRPC over HTTP/2.
  • Transport occurs over a Unix domain socket (/var/run/container.sock by default) using SwiftNIO and GRPCSwift.
  • Service definitions are generated from Protocol Buffer files, creating type-safe client stubs in Sources/Services/ContainerAPIService/Client/.
  • The server binds to the same socket in Sources/APIServer/APIServer+Start.swift and handles requests in Sources/Services/ContainerAPIService/Server/.
  • File download operations (images, kernels) use AsyncHTTPClient separately from the gRPC channel.

Frequently Asked Questions

Does the container CLI use REST or HTTP directly?

No. According to the apple/container source code, the CLI does not use REST or raw HTTP for container operations. Instead, it uses gRPC (HTTP/2) via the ContainerAPIClient library, with messages serialized using Protocol Buffers. The only HTTP usage occurs in FileDownloader for downloading image layers and kernel tarballs.

What socket path does the container CLI use by default?

The default Unix domain socket path is /var/run/container.sock. This path is used unless overridden by the --socket command-line flag when starting container-apiserver or running CLI commands. The socket path must match between the server and client for communication to succeed.

How are the RPC methods defined between the CLI and server?

RPC methods are defined in protobuf files (e.g., containers.proto) and compiled into Swift using the gRPC Swift plugin. The generated client stubs reside in Sources/Services/ContainerAPIService/Client/ContainersService.swift, while server implementations are in Sources/Services/ContainerAPIService/Server/ContainersService.swift. Both sides share the same protobuf message definitions ensuring type safety.

Why does the Apple Container project use gRPC instead of REST?

The project uses gRPC to provide strongly-typed, contract-based APIs with automatic code generation for both client and server. This eliminates manual HTTP endpoint management and JSON parsing, reducing bugs and maintenance overhead. The HTTP/2 transport also supports efficient multiplexing over the single Unix domain socket connection.

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 →