# How to Use the Plugin System for Network and Runtime Extensions in apple/container

> Learn to use the apple/container plugin system to add network and runtime extensions. Discover how to register custom binaries with launchd for on-demand services without altering core code.

- Repository: [Apple/container](https://github.com/apple/container)
- Tags: how-to-guide
- Published: 2026-07-02

---

**The `apple/container` plugin system discovers external binaries from host directories, instantiates them as Swift structs conforming to the `Plugin` protocol, and registers them with launchd to provide network or runtime services on demand without modifying core container code.**

The primary keyword is "plugin system for network and runtime extensions in apple/container". This architecture allows developers to extend the macOS container runtime with custom networking backends and alternative execution environments by placing structured bundles in specific filesystem locations.

## Core Plugin Architecture

The plugin system centers on five key components that handle discovery, configuration, and lifecycle management.

### The Plugin Struct

At [`Sources/ContainerPlugin/Plugin.swift`](https://github.com/apple/container/blob/main/Sources/ContainerPlugin/Plugin.swift), the **Plugin** struct represents a single extension. It encapsulates the plugin's name, binary path, launchd label, and service capabilities (`runtime`, `network`, or both). The struct conforms to the `Plugin` protocol and provides methods like `getLaunchdLabel()` which returns the standardized label `com.apple.container.<name>`.

### Configuration and Loading

**PluginConfig** ([`Sources/ContainerPlugin/PluginConfig.swift`](https://github.com/apple/container/blob/main/Sources/ContainerPlugin/PluginConfig.swift)) decodes each plugin's [`plugin.json`](https://github.com/apple/container/blob/main/plugin.json) configuration file. This JSON schema defines the service types, default arguments, and boot behavior. **PluginLoader** ([`Sources/ContainerPlugin/PluginLoader.swift`](https://github.com/apple/container/blob/main/Sources/ContainerPlugin/PluginLoader.swift)) scans designated directories, validates bundle integrity, and instantiates `Plugin` objects. It automatically registers valid plugins with launchd using `PluginLoader.registerWithLaunchd`.

### State Management and Runtime Integration

**PluginStateRoot** ([`Sources/ContainerPlugin/PluginStateRoot.swift`](https://github.com/apple/container/blob/main/Sources/ContainerPlugin/PluginStateRoot.swift)) provisions per-instance state directories for logs and sockets. At runtime, **PluginsHarness** ([`Sources/Services/ContainerAPIService/Server/Plugin/PluginsHarness.swift`](https://github.com/apple/container/blob/main/Sources/Services/ContainerAPIService/Server/Plugin/PluginsHarness.swift)) aggregates all loaded plugins and routes container API requests to the appropriate extension via Mach services.

## Creating a Custom Plugin

Adding a new network or runtime extension requires a specific bundle structure and JSON configuration.

### Bundle Structure

Create a directory with the following layout:

```text
my-runtime/
├── bin/               # executable that implements the service

│   └── my-runtime
└── plugin.json       # configuration file

```

The binary must reside in the `bin/` subdirectory, and the directory name determines the plugin identifier.

### Configuring plugin.json

The configuration file must match the schema parsed by `PluginConfig`. A minimal runtime plugin configuration looks like this:

```json
{
  "type": ["runtime"],
  "defaultArguments": ["--listen", "unix:///var/run/my-runtime.sock"],
  "shouldBoot": false
}

```

Key fields include:
- **type**: Array containing `"runtime"`, `"network"`, or both
- **defaultArguments**: Array of strings passed to the binary on startup
- **shouldBoot**: Boolean indicating whether launchd should start the service at boot (`true`) or on-demand (`false`)

### Installation and Registration

Place the bundle in one of the scanned directories:

- **System-wide**: `/usr/local/libexec/container/plugin/`
- **Per-user**: `$HOME/.local/libexec/container/plugin/`
- **Custom**: Any path specified via the `--plugin-dir` CLI flag

When the container runtime initializes, `PluginLoader` discovers the bundle, validates the executable, and registers the plugin with launchd using the label convention `com.apple.container.<name>`. The test suite in [`Tests/ContainerPluginTests/PluginLoaderTest.swift`](https://github.com/apple/container/blob/main/Tests/ContainerPluginTests/PluginLoaderTest.swift) verifies this discovery and registration workflow.

## Using Network Extensions

Network plugins declare the `"network"` type in their [`plugin.json`](https://github.com/apple/container/blob/main/plugin.json) and expose a Mach service at `com.apple.container.network.<name>`.

### Service Discovery and CLI Usage

List available network plugins:

```bash
container network list

```

Connect a container to a custom network backend:

```bash
container network connect --plugin my-network-plugin <container-id>

```

The plugin receives the request via its Mach service, creates the virtual network interface (often backed by `vmnet`), and returns the endpoint to the container runtime. The plugin binary handles the actual network setup while the core container code manages the integration.

## Using Runtime Extensions

Runtime plugins provide alternative execution environments such as custom sandboxes. They declare `"runtime"` in the type array of [`plugin.json`](https://github.com/apple/container/blob/main/plugin.json).

Invoke a container with a specific runtime plugin:

```bash
container run --runtime sandboxd alpine:latest

```

The runtime passes the container's launch parameters to the plugin binary, which sets up the sandbox environment before execing the container process. This allows integration with macOS security frameworks without modifying the containerd-style core runtime.

## Complete Example: Hydra Plugin

The repository's test fixtures include a reference implementation. The **hydra** plugin bundle demonstrates a dual-service extension:

```text
hydra/
├── bin/
│   └── hydra
└── plugin.json

```

With [`plugin.json`](https://github.com/apple/container/blob/main/plugin.json) configured as:

```json
{
  "type": ["runtime", "network"],
  "defaultArguments": ["start", "with", "params"],
  "shouldBoot": true
}

```

This configuration registers two Mach services: `com.apple.container.runtime.hydra` and `com.apple.container.network.hydra`. The `shouldBoot: true` setting ensures launchd starts the service at boot time rather than on-demand. The test file [`Tests/ContainerPluginTests/PluginTest.swift`](https://github.com/apple/container/blob/main/Tests/ContainerPluginTests/PluginTest.swift) validates that the `Plugin` struct correctly generates launchd labels and Mach service names for this bundle.

## Summary

- **Plugin discovery** occurs via `PluginLoader` scanning `/usr/local/libexec/container/plugin/` and user directories, or paths specified with `--plugin-dir`.
- **Bundle requirements** include a `bin/` directory containing the executable and a [`plugin.json`](https://github.com/apple/container/blob/main/plugin.json) file defining the service type and arguments.
- **Registration** happens automatically through `PluginLoader.registerWithLaunchd`, creating launchd plists with labels following the `com.apple.container.<name>` convention.
- **Network plugins** expose Mach services at `com.apple.container.network.<name>` and integrate with `container network` commands.
- **Runtime plugins** activate via the `--runtime` flag and handle container execution environment setup.
- **State persistence** is managed through `PluginStateRoot`, providing per-instance directories for logs and sockets.

## Frequently Asked Questions

### How does apple/container discover new plugins?

The `PluginLoader` class recursively scans predefined directories and any paths specified via the `--plugin-dir` CLI flag. It validates each directory containing a [`plugin.json`](https://github.com/apple/container/blob/main/plugin.json) file and an executable in `bin/`, then instantiates a `Plugin` struct. Discovery is tested in [`Tests/ContainerPluginTests/PluginLoaderTest.swift`](https://github.com/apple/container/blob/main/Tests/ContainerPluginTests/PluginLoaderTest.swift).

### What is the difference between runtime and network plugin types?

**Runtime** plugins provide alternative execution environments and are invoked via the `--runtime` flag when starting containers. **Network** plugins manage virtual network interfaces and are accessed through `container network` commands. A single plugin can declare both types in the `type` array of [`plugin.json`](https://github.com/apple/container/blob/main/plugin.json) to provide hybrid services.

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

`PluginLoader` automatically generates launchd property lists for each discovered plugin using the label format `com.apple.container.<name>`. If `shouldBoot` is true in [`plugin.json`](https://github.com/apple/container/blob/main/plugin.json), launchd starts the service at system boot; otherwise, it uses launchd's on-demand activation when the Mach service is first accessed.

### Can plugins store persistent state?

Yes. The `PluginStateRoot` class provides each plugin instance with a dedicated state directory for storing logs, sockets, and other persistent data. This directory is managed separately from the plugin bundle and persists across container restarts.