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
PluginLoaderat paths like/usr/local/libexec/container/plugin/. - PluginConfig defines daemon services and boot behavior, while PluginFactory validates directory structures and instantiates typed
Pluginobjects. - On Linux, the
registerWithLaunchdmethod creates systemd unit files under/etc/systemd/system, using labels generated byPlugin.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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →