Open-Source Wikis

/

containerd

/

Background

/

Plugins and proxy plugins

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:

  1. 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.
  2. External proxy plugins — a separate process exposing one of the gRPC service contracts (snapshot, content, diff, sandbox). The operator declares them in config.toml and 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 RequiredPlugins in 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 overview
  • cmd/containerd/server/server.go — the actual code that registers and loads plugins
  • vendor/github.com/containerd/plugin/registry/registry.go — the topological sort
  • vendor/github.com/containerd/plugin/plugin.go — the Registration/InitContext types

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Plugins and proxy plugins – containerd wiki | Factory