containerd/containerd
Plugins and proxy plugins
Why containerd has its idiosyncratic plugin model and what the boundary between built-in and external plugins looks like.
The dual model
containerd supports two ways to extend the daemon:
- Built-in (compile-time) plugins — registered via
init()in a package the daemon imports. Cheap to add at containerd's own development time, slow to roll out everywhere because every consumer needs a containerd build. - External proxy plugins — a separate process exposing one of the gRPC service contracts (snapshot, content, diff, sandbox). The operator declares them in
config.tomland containerd dials them like any other plugin.
The docs/PLUGINS.md document explains the operator-facing view; this page is about the internals.
Why both
- Built-ins are the right answer for things that every containerd needs (the bbolt metadata store, the events exchange, the gRPC server). Compiling them in keeps the deployment story simple.
- Proxy plugins are the right answer for storage backends that are user-specific (a cloud-provider snapshotter, a custom content cache, a microVM sandbox). They can be released independently from containerd.
Built-in registration
// in plugins/snapshots/native/native.go
func init() {
registry.Register(&plugin.Registration{
Type: plugins.SnapshotPlugin,
ID: "native",
InitFn: func(ic *plugin.InitContext) (any, error) {
ic.Meta.Platforms = append(ic.Meta.Platforms, platforms.DefaultSpec())
return native.NewSnapshotter(ic.Properties[plugins.PropertyRootDir])
},
})
}The blank import in cmd/containerd/builtins/builtins.go is the only thing that brings the package into the binary. To disable a built-in, list its URI in disabled_plugins.
Proxy plugin registration
[proxy_plugins]
[proxy_plugins.mysnap]
type = "snapshot"
address = "/run/mysnap.sock"
platform = "linux/amd64"Translated by server.LoadPlugins (in cmd/containerd/server/server.go) into the equivalent of:
registry.Register(&plugin.Registration{
Type: plugins.SnapshotPlugin,
ID: "mysnap",
InitFn: func(ic *plugin.InitContext) (any, error) {
conn, err := grpc.NewClient(dialer.DialAddress(address), gopts...)
if err != nil { return nil, err }
return ssproxy.NewSnapshotter(ssapi.NewSnapshotsClient(conn), "mysnap"), nil
},
})The proxy wrapper (core/snapshots/proxy/, core/content/proxy/, etc.) implements the Go interface for the plugin type by delegating each method to a gRPC call.
Why only some types are proxy-able
server.LoadPlugins has a fixed switch:
switch pp.Type {
case "snapshot": ...
case "content": ...
case "sandbox": ...
case "diff": ...
default: log.G(ctx).Warn("unknown proxy plugin type")
}The other plugin types (runtime v2, CRI, GC, events, …) either don't make sense out-of-process or have their own external mechanism (e.g. runtime v2 already runs in a separate process — the shim).
Streaming proxy
A more recent addition: for plugins that need to push events back into containerd (warning service, future event-producing proxies), ec04e4f63 "Add streaming proxy" introduced a streaming-RPC variant where the proxy keeps a long-lived stream open. The shim manager's reattach logic uses the same primitive.
The dependency graph
registry.Graph produces a topological order. Each Registration.Requires lists the plugin types the plugin needs at init time. A snapshotter typically Requires the metadata store; the metadata store Requires the content store; the content store has no Requires. This is what guarantees things initialize in a sensible order.
Failure isolation
- An optional plugin's failure is logged and the daemon continues.
- A required plugin's failure aborts startup. This is what
RequiredPluginsin the config is for. - A plugin that has called
RegisterReadiness()is considered "promised to come up" and is treated as required afterwards.
Reading what's loaded
ctr plugins ls (or the introspection gRPC service in plugins/services/introspection/) shows the live state. Each entry has its InitErr filled in if the plugin failed to load — invaluable when debugging custom configurations.
References
docs/PLUGINS.md— operator-focused overviewcmd/containerd/server/server.go— the actual code that registers and loads pluginsvendor/github.com/containerd/plugin/registry/registry.go— the topological sortvendor/github.com/containerd/plugin/plugin.go— theRegistration/InitContexttypes
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.