# How the Container Runtime Plugin System Works on Linux in apple/container

> Explore the apple/container plugin system on Linux. Discover its modular architecture for stand-alone binaries, configuration, and systemd service management.

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

---

**The container runtime plugin system in apple/container uses a modular architecture where stand-alone binaries are discovered via `PluginLoader`, configured through `PluginConfig`, and managed as systemd services on Linux.**

The apple/container repository implements a lightweight, extensible plugin framework for container runtime management. On Linux environments, this system discovers, loads, and registers plugins through a dedicated Swift module that bridges plugin binaries with the host systemd initialization system. The architecture treats each plugin as a stand-alone daemon service with declarative configuration and deterministic service naming.

## Core Components of the Container Runtime Plugin System

The plugin framework centers on four Swift types defined in the `ContainerPlugin` module. These components handle everything from filesystem discovery to process execution.

### Plugin Value Type

In [`Sources/ContainerPlugin/Plugin.swift`](https://github.com/apple/container/blob/main/Sources/ContainerPlugin/Plugin.swift), the `Plugin` struct serves as the core value type that wraps a plugin's `binaryURL` (executable path) together with its static `PluginConfig`. This type supplies helper methods for generating launch-daemon labels, Mach-service names, and boot-time handling logic. The struct provides the `exec(args:)` method for process replacement and `getLaunchdLabel(instanceId:)` for service identification.

### PluginConfig Decoding

[`Sources/ContainerPlugin/PluginConfig.swift`](https://github.com/apple/container/blob/main/Sources/ContainerPlugin/PluginConfig.swift) decodes the JSON or YAML configuration that lives beside each plugin binary (conventionally named [`plugin.json`](https://github.com/apple/container/blob/main/plugin.json)). This file describes the available daemon services via the `servicesConfig` object, specifies whether the plugin should auto-start at boot through the `loadAtBoot` boolean, and provides abstract help text for the `container help` command. The configuration defines `DaemonPluginType` values such as `runtime` or `network` to categorize service capabilities.

### PluginFactory Validation

Located in [`Sources/ContainerPlugin/PluginFactory.swift`](https://github.com/apple/container/blob/main/Sources/ContainerPlugin/PluginFactory.swift), the `PluginFactory` protocol and its default implementation validate plugin installation directories. The factory checks for the presence of both an executable binary and a configuration file, then constructs a fully-typed `Plugin` instance. This abstraction allows the runtime to support multiple binary formats or configuration schemas through different factory implementations.

### PluginLoader Discovery

[`Sources/ContainerPlugin/PluginLoader.swift`](https://github.com/apple/container/blob/main/Sources/ContainerPlugin/PluginLoader.swift) orchestrates the discovery process. It maintains a list of plugin search paths (defaulting to `/usr/local/libexec/container/plugin`) and iterates through registered `PluginFactory` instances to instantiate plugins found on disk. The loader provides the `findPlugins()` method that returns an array of `Plugin` objects ready for registration or execution.

## Plugin Discovery and Installation Layout on Linux

The container runtime expects a specific directory structure for plugin installation. Each plugin resides in a dedicated subdirectory under the plugin search path:

```

/usr/local/libexec/container/plugin/<plugin-name>/
├─ bin/<plugin-name>          (executable binary)
└─ plugin.json                (static configuration)

```

At startup, the `PluginLoader` iterates over all configured search directories. For each subdirectory, the loader invokes each registered `PluginFactory` to attempt instantiation. The `DefaultPluginFactory` reads the [`plugin.json`](https://github.com/apple/container/blob/main/plugin.json) file via `PluginConfig` and builds the `Plugin` value only if both the configuration parses successfully and the executable exists at the expected path.

## Configuration Interpretation and systemd Registration

Once loaded, the `PluginConfig` determines how the runtime integrates the plugin with the host system. The `servicesConfig` object contains two critical fields:

- **services** – An array of service entries, each specifying a `type` (e.g., `runtime`, `network`) and optional arguments passed to the binary on launch
- **loadAtBoot** – A boolean indicating whether the plugin should register as a system service

On Linux, the `PluginLoader.registerWithLaunchd(plugin:pluginStateRoot:debug:)` method translates these settings into systemd units. When `shouldBoot` evaluates to `true`, the runtime writes a unit file to `/etc/systemd/system` using the label generated by `Plugin.getLaunchdLabel(instanceId:)`. The label format `com.apple.container.<name>` becomes the systemd unit name, and the runtime executes `systemctl enable` to configure auto-start behavior.

## Plugin Execution and Inter-Process Communication

When the runtime starts a plugin, it calls `Plugin.exec(args:)`. This method rewrites `argv[0]` to the full path of the plugin binary and invokes `execvp` to replace the current process image, mirroring traditional Unix daemon behavior.

For communication, the runtime uses deterministic service names generated by `getMachService(type:)`. Although Mach-services are macOS-specific, apple/container maintains the naming convention (`com.apple.container.<type>.<name>`) on Linux for cross-platform consistency. The runtime locates running plugins via these names, which map to Unix domain sockets or D-bus endpoints on Linux systems.

## Practical Implementation Examples

The following examples demonstrate common operations with the container runtime plugin system, based on the test suites in `Tests/ContainerPluginTests/`.

### Discovering and Registering Plugins

```swift
import ContainerPlugin

let pluginSearchPaths = [
    URL(fileURLWithPath: "/usr/local/libexec/container/plugin")
]

let loader = PluginLoader(
    pluginDirectories: pluginSearchPaths,
    pluginFactories: [DefaultPluginFactory()]
)

let plugins = loader.findPlugins()

for plugin in plugins {
    print("Found plugin: \(plugin.name)")
    if plugin.shouldBoot {
        try loader.registerWithLaunchd(
            plugin: plugin, 
            pluginStateRoot: .someStateRoot, 
            debug: false
        )
    }
}

```

*Source:* [`Tests/ContainerPluginTests/PluginLoaderTest.swift`](https://github.com/apple/container/blob/main/Tests/ContainerPluginTests/PluginLoaderTest.swift)

### Starting a Plugin Manually

```swift
guard let linuxSandbox = plugins.first(where: { $0.name == "linux-sandboxd" }) else {
    fatalError("sandbox plugin missing")
}
try linuxSandbox.exec(args: ["linux-sandboxd", "--config", "/etc/container/sandbox.yaml"])

```

*Source:* [`Sources/ContainerPlugin/Plugin.swift`](https://github.com/apple/container/blob/main/Sources/ContainerPlugin/Plugin.swift)

### Querying Service Names

```swift
if let service = linuxSandbox.getMachService(type: .runtime) {
    print("Runtime service name: \(service)")
    // Outputs: com.apple.container.runtime.linux-sandboxd
}

```

*Source:* [`Sources/ContainerPlugin/Plugin.swift`](https://github.com/apple/container/blob/main/Sources/ContainerPlugin/Plugin.swift)

## Summary

- The **container runtime plugin system** in apple/container treats plugins as stand-alone binaries with JSON configuration, discovered via `PluginLoader` at paths like `/usr/local/libexec/container/plugin/`.
- **PluginConfig** defines daemon services and boot behavior, while **PluginFactory** validates directory structures and instantiates typed `Plugin` objects.
- On Linux, the `registerWithLaunchd` method creates systemd unit files under `/etc/systemd/system`, using labels generated by `Plugin.getLaunchdLabel()`.
- **Plugin.exec(args:)** replaces the current process with the plugin binary using `execvp`, and service communication relies on deterministic naming schemes compatible with both Mach (macOS) and Unix domain sockets (Linux).

## Frequently Asked Questions

### Where does apple/container search for plugins on Linux?

By default, the runtime searches `/usr/local/libexec/container/plugin` and any additional paths specified in the runtime configuration. The `PluginLoader` iterates these directories and uses registered `PluginFactory` instances to validate and instantiate plugins found in subdirectories containing both a `bin/` executable and a [`plugin.json`](https://github.com/apple/container/blob/main/plugin.json) configuration file.

### How does the plugin system integrate with systemd?

When a plugin's configuration specifies `loadAtBoot: true`, the `PluginLoader.registerWithLaunchd()` method generates a systemd unit name from the plugin's launch-daemon label (format: `com.apple.container.<name>`). It writes a unit file to `/etc/systemd/system` and executes `systemctl enable` to configure the plugin for automatic startup, effectively bridging the plugin's declarative configuration with the host init system.

### What configuration format does apple/container expect for plugins?

Each plugin requires a [`plugin.json`](https://github.com/apple/container/blob/main/plugin.json) file (or YAML equivalent) located in the plugin's root directory alongside the `bin/` folder. This file must decode to a `PluginConfig` structure containing a `servicesConfig` object with a `services` array (defining daemon types like `runtime` or `network`) and a boolean `loadAtBoot` field that controls systemd registration behavior.

### How does plugin process execution work in apple/container?

The runtime starts plugins via `Plugin.exec(args:)`, defined in [`Sources/ContainerPlugin/Plugin.swift`](https://github.com/apple/container/blob/main/Sources/ContainerPlugin/Plugin.swift). This method modifies the argument vector to set `argv[0]` to the plugin's full binary path, then calls `execvp` to replace the current process image with the plugin executable. This design ensures plugins run as independent daemons while inheriting the runtime's process context during launch.