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

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, 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 decodes the JSON or YAML configuration that lives beside each plugin binary (conventionally named 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, 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 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 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

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

Starting a Plugin Manually

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

Querying Service Names

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

Source: 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 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 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. 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.

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 →