Moby — API Surface#

API types#

  • REST/HTTP — primary API (Docker Engine API, ~100 endpoints)
  • gRPC over HTTP/2 — BuildKit build and session APIs (multiplexed over same listener)
  • Librarygithub.com/moby/moby/client as an independent Go module
  • Plugin (extension) — v2 plugin HTTP protocol over Unix socket; v1 legacy plugin protocol
  • Events stream — SSE-style JSON/NDJSON stream (GET /events)
  • Hijacked TCP — container attach/exec (protocol upgrade from HTTP to raw TCP)

REST/HTTP API#

Router#

  • Router: gorilla/mux (versioned path prefix /v{version}/)
  • Route registration: per-resource Router structs, each implementing Router interface (Routes() []Route). Routes are registered by calling Server.CreateMux(routers...) at startup, which iterates over all routers and registers each route with the shared mux.

Middleware chain#

Applied in order (outermost → innermost) for every request:

  1. ExperimentalMiddleware — sets Docker-Experimental: true/false response header
  2. VersionMiddleware — validates API version in URL prefix against min/max bounds; injects version into context.Context; rejects requests outside configured range with a structured error
  3. AuthZ Middleware — if authz plugins configured: serializes full request (method, URI, headers, body up to 4 MiB) as JSON and forwards to each authz plugin over Unix socket; blocks if any plugin returns Allow: false; also inspects response and can block/modify it

Each middleware implements:

type Middleware interface {
    WrapHandler(func(ctx, w, r, vars) error) func(ctx, w, r, vars) error
}

API versioning#

  • Current max version: 1.54 (set in daemon/config/config.go)
  • Absolute minimum: 1.24 (cannot be raised above this by config)
  • Configurable default minimum: 1.40 (operator can lower to 1.24 via min-api-version in daemon.json)
  • Per-route version gates: router.WithMinimumAPIVersion("1.25") options function wraps a handler; requests below the required version get a structured version-mismatch error
  • Experimental routes: router.Experimental option; returns 501 Not Implemented if daemon not in experimental mode
  • Version negotiation: client library auto-negotiates by calling GET /version and downgrading to min(server, client)

Authentication#

  • Transport security: TLS mutual auth (cert+key, CA verification) for TCP listeners
  • Request auth: delegated entirely to external authz plugins; the daemon has no built-in auth (no Bearer tokens, no API keys). The typical deployment places the daemon behind a TLS socket accessible only to trusted processes.

Transport#

Multiple listeners can run simultaneously:

  • unix:///var/run/docker.sock (default, no TLS)
  • tcp://host:port (optionally with TLS)
  • Windows named pipe (npipe://)
  • HTTP/1.1, HTTP/2, and h2c (HTTP/2 cleartext) all supported on all listeners via golang.org/x/net/http2

Key endpoints (by router)#

System (system.Router)#

MethodPathDescription
GET/_pingHealth check; returns API version headers
GET/versionEngine version, API version, OS/arch
GET/infoDaemon configuration, stats (single-flight protected)
GET/eventsServer-sent event stream (JSON lines / NDJSON)
GET/system/dfDisk usage breakdown
POST/authRegistry login credential validation

Containers (container.Router)#

MethodPathDescription
GET/containers/jsonList containers
GET/containers/{name}/jsonInspect container
POST/containers/createCreate container
POST/containers/{name}/startStart container
POST/containers/{name}/stopStop container
POST/containers/{name}/restartRestart container
POST/containers/{name}/killSend signal
POST/containers/{name}/pausePause (SIGSTOP)
POST/containers/{name}/unpauseUnpause
POST/containers/{name}/execCreate exec instance
POST/exec/{name}/startStart exec (hijacks connection)
POST/containers/{name}/attachAttach stdin/stdout/stderr (hijacks connection)
GET/containers/{name}/attach/wsAttach via WebSocket
GET/containers/{name}/logsStream or tail logs
GET/containers/{name}/statsStream resource stats
GET/containers/{name}/topProcess list (ps)
POST/containers/{name}/updateUpdate resource limits
POST/containers/{name}/renameRename container
POST/containers/{name}/waitWait for state change
DELETE/containers/{name}Remove container
POST/containers/pruneRemove stopped containers (API 1.25+)
GET/PUT/HEAD/containers/{name}/archiveCopy files in/out (tar)
POST/commitCreate image from container

Images (image.Router)#

MethodPathDescription
GET/images/jsonList images
GET/images/{name}/jsonInspect image
GET/images/{name}/historyLayer history
POST/images/createPull image (fromImage) or import (fromSrc)
POST/images/{name}/pushPush image to registry
POST/images/{name}/tagTag image
GET/images/searchSearch Docker Hub
GET/images/{name}/getExport image as tar
GET/images/getExport multiple images
POST/images/loadImport image from tar
DELETE/images/{name}Remove image
POST/images/pruneRemove unused images (API 1.25+)

Build (build.Router)#

MethodPathDescription
POST/buildBuild image (v1 classic builder or BuildKit)
POST/build/prunePrune build cache (API 1.31+)
POST/build/cancelCancel active build

Networks (network.Router)#

MethodPathDescription
GET/networksList networks
GET/networks/{id}Inspect network
POST/networks/createCreate network
POST/networks/{id}/connectConnect container to network
POST/networks/{id}/disconnectDisconnect container
DELETE/networks/{id}Remove network
POST/networks/pruneRemove unused networks

Volumes (volume.Router)#

MethodPathDescription
GET/volumesList volumes
GET/volumes/{name}Inspect volume
POST/volumes/createCreate volume
PUT/volumes/{name}Update volume (API 1.42+, Swarm cluster volumes)
DELETE/volumes/{name}Remove volume
POST/volumes/pruneRemove unused volumes

Swarm (swarm.Router)#

MethodPathDescription
POST/swarm/initInitialize Swarm
POST/swarm/joinJoin Swarm
POST/swarm/leaveLeave Swarm
GET/swarmInspect Swarm
POST/swarm/updateUpdate Swarm config
POST/swarm/unlock / GET /swarm/unlockkeyManager unlock
CRUD/services/{id}Service management
CRUD/nodes/{id}Node management
GET/tasks/{id}Task inspect/list/logs
CRUD/secrets/{id}Secret management (API 1.25+)
CRUD/configs/{id}Config management (API 1.30+)

Plugins (plugin.Router)#

MethodPathDescription
GET/pluginsList plugins
GET/plugins/{name}/jsonInspect plugin
POST/plugins/pullInstall plugin from registry
POST/plugins/createCreate plugin from tar
POST/plugins/{name}/enableEnable plugin
POST/plugins/{name}/disableDisable plugin
POST/plugins/{name}/pushPush plugin to registry
POST/plugins/{name}/upgradeUpgrade plugin (API 1.26+)
DELETE/plugins/{name}Remove plugin

Checkpoints (Experimental, checkpoint.Router)#

MethodPathDescription
GET/containers/{name}/checkpointsList checkpoints
POST/containers/{name}/checkpointsCreate checkpoint (CRIU)
DELETE/containers/{name}/checkpoints/{checkpoint}Delete checkpoint

Debug (debug.Router)#

PathDescription
/debug/varsexpvar — Go runtime metrics as JSON
/debug/pprof/pprof index
/debug/pprof/profileCPU profile
/debug/pprof/heap, /goroutine, etc.Standard pprof endpoints

Session / Distribution#

  • POST /session — BuildKit session multiplexing (upgrades to h2c; gRPC streams for cache, secrets, auth, SSH forwarding)
  • GET /distribution/{name}/json — OCI manifest/platform info from registry (API 1.30+)

gRPC API#

BuildKit (via HTTP/2 upgrade)#

Moby does not expose a standalone gRPC port. Instead, gRPC is multiplexed over the existing HTTP/2 listeners using two mechanisms:

  1. POST /grpc (deprecated grpc.Router) — receives an HTTP/1.1 request, upgrades to HTTP/2 in-process, then dispatches to the gRPC server. Deprecated because it is no longer necessary.
  2. Native h2c — since HTTP/2 is now enabled on all listeners, gRPC clients can connect directly using standard HTTP/2 (h2c for Unix socket, TLS for TCP).

gRPC services registered:

  • moby.buildkit.v1.Control — BuildKit build control (solve, status, prune, build history)
  • opentelemetry.proto.collector.trace.v1.TraceService — client-side traces forwarded to daemon for inclusion in build history

Proto files: None in moby/moby itself; proto definitions live in the moby/buildkit dependency. Moby delegates to buildkitd.NewController() for service implementation.

gRPC interceptors:

  • grpcerrors.UnaryServerInterceptor / StreamServerInterceptor — normalize gRPC errors
  • otelgrpc.StatsHandler — OpenTelemetry tracing
  • Custom unaryInterceptor — logs errors at DEBUG level, skips tracing for OTLP export calls

Plugin / Extension System#

Moby has two distinct plugin systems with very different designs:

v2 Managed Plugins (primary)#

How it works:

  1. Plugin is an OCI image distributed via any Docker registry
  2. docker plugin install pulls the image, unpacks it to a content-addressed rootfs
  3. Plugin config (config.json) declares: plugin type (volume driver, network driver, authz, log driver), required capabilities, settings
  4. On enable, daemon/pkg/plugin/Manager creates an OCI spec (plugin.InitSpec) and runs the plugin as an isolated container via containerd
  5. Plugin process binds to a Unix socket at /run/docker/plugins/<id>.sock
  6. Daemon communicates with the plugin over this socket using moby.plugins.http/v1 protocol

Plugin protocol (moby.plugins.http/v1):

  • HTTP over Unix domain socket
  • Handshake: POST /Plugin.Activate → plugin returns {"Implements": ["VolumeDriver"]} (or NetworkDriver, authz, LoggingDriver, etc.)
  • Each subsystem has its own RPC endpoints (e.g., VolumeDriver.Create, VolumeDriver.Mount, NetworkDriver.CreateNetwork, etc.)
  • No OpenAPI/gRPC — plain JSON-over-HTTP

Extension points:

  • Volume drivers: VolumeDriver.{Create,Remove,Mount,Unmount,Path,List,Get}
  • Network drivers: NetworkDriver.{CreateNetwork,DeleteNetwork,CreateEndpoint,Join,Leave}
  • IPAM drivers: IpamDriver.{GetCapabilities,GetDefaultAddressSpaces,RequestPool,ReleasePool,RequestAddress,ReleaseAddress}
  • Authorization: authz — receives full Request/Response JSON; returns Allow bool, Msg string
  • Log drivers: LogDriver.{StartLogging,StopLogging,Capabilities,ReadLogs}
  • Image/build secrets (via BuildKit session)

v1 Legacy Plugins (deprecated)#

  • Discovered from /run/docker/plugins/*.sock or spec files in /etc/docker/plugins/, /usr/lib/docker/plugins/
  • Not containerized — run as external processes the user starts manually
  • Same HTTP protocol as v2, but lifecycle is unmanaged
  • Still supported via pkg/plugins package

AuthZ Plugin System#

  • Loaded from v1/v2 plugin store
  • pkg/authorization.Middleware wraps every HTTP handler
  • Each request: daemon serializes {User, UserAuthNMethod, RequestMethod, RequestURI, RequestBody} → JSON → POST to each authz plugin
  • Each response: daemon serializes {RequestBody, ResponseBody, ResponseStatusCode} → POST to each authz plugin
  • Combined result: all plugins must allow (AND logic)
  • Body capped at 4 MiB to prevent memory exhaustion
  • Supports response body modification (plugins can filter privileged content)

Library API (github.com/moby/moby/client)#

A separately versioned Go module providing a complete typed client for the Docker Engine API.

API style#

  • Method-per-operation — one function per API call (e.g., ContainerStart, ImagePull, NetworkCreate)
  • Options structs — each method takes an options struct (e.g., ContainerStartOptions, ImagePullOptions) with explicit field names, not variadic options
  • Functional options for client constructionclient.New(client.FromEnv, client.WithAPIVersionFromEnv())
  • Result structs — methods return typed result structs (e.g., ContainerCreateResult, ImagePullResponse)
  • Streaming results — streaming endpoints return io.ReadCloser or channel-based types (e.g., EventsResult{Messages <-chan, Err <-chan})

Interface decomposition#

APIClient is composed from 16 sub-interfaces:

APIClient
├── stableAPIClient
│   ├── ContainerAPIClient     — 22 methods (CRUD + lifecycle + I/O)
│   ├── ExecAPIClient          — 5 methods
│   ├── ImageAPIClient         — 11 methods
│   ├── ImageBuildAPIClient    — 3 methods
│   ├── NetworkAPIClient       — 7 methods
│   ├── VolumeAPIClient        — 6 methods
│   ├── PluginAPIClient        — 10 methods
│   ├── SystemAPIClient        — 5 methods (events, info, ping, df, auth)
│   ├── NodeAPIClient          — 4 methods (Swarm)
│   ├── ServiceAPIClient       — 6 methods (Swarm)
│   ├── TaskAPIClient          — 3 methods (Swarm)
│   ├── SwarmAPIClient         — 7 methods
│   ├── SecretAPIClient        — 5 methods
│   ├── ConfigAPIClient        — 5 methods
│   ├── DistributionAPIClient  — 1 method
│   └── RegistrySearchClient   — 1 method
└── CheckpointAPIClient        — 3 methods (experimental)

Client configuration#

// Functional options:
client.New(
    client.FromEnv,                         // DOCKER_HOST, DOCKER_API_VERSION, DOCKER_CERT_PATH
    client.WithHost("unix:///var/run/docker.sock"),
    client.WithAPIVersion("1.41"),          // disable negotiation
    client.WithHTTPClient(customHTTP),
    client.WithTLSClientConfig(cacert, cert, key),
    client.WithResponseHook(hook),          // intercept raw responses
)

API version negotiation#

  • Default: client calls GET /version, compares server version to client max version, uses min of both
  • Override: WithAPIVersion or DOCKER_API_VERSION env var disables negotiation
  • Per-request: client injects X-Docker-API-Version in URL path, not header

Backward compatibility#

  • The client module has its own go.mod at repositories/moby/client/go.mod
  • It depends on github.com/moby/moby/api (types module) but not on the daemon
  • No explicit stability/semver policy visible in code, but the interface is stable across Docker CLI releases

Connection hijacking#

Attach, exec-start, and DialHijack endpoints upgrade the HTTP connection to a raw bidirectional TCP stream. The HijackDialer interface exposes this:

DialHijack(ctx context.Context, url, proto string, meta map[string][]string) (net.Conn, error)

This is used for container attach, exec streams, and BuildKit session streams.


API types / serialization#

  • Body format: JSON (standard encoding/json)
  • Streaming: NDJSON (newline-delimited JSON lines) for events and build output
  • Error format: {"message": "error text"} with HTTP status codes mapped to semantic error types via errdefs package (IsNotFound, IsConflict, etc.)
  • Types module: github.com/moby/moby/api — a standalone Go module containing all request/response structs, no daemon dependencies. This is the contract shared between daemon and all clients.
  • Swagger spec: api/swagger.yaml — OpenAPI 2.0 spec with version-tagged variants in api/docs/v1.{26,27,32,44,50,52,53}.yaml. Used for documentation generation and validation.
  • Content negotiation: GET /events supports application/json, application/x-ndjson, application/json-seq via Accept header.

Notable API design decisions#

1. Router-per-Resource + Backend Interface separation#

Each resource type (container, image, network, etc.) has:

  • Its own Router struct with route registration
  • Its own Backend interface (defined within the router package)
  • Handlers receive only the Backend interface, not *daemon.Daemon

This enforces a soft API boundary even within the monolith. New endpoints can only call methods on the resource-specific Backend, not arbitrary daemon internals. The result is a codebase where adding a new API endpoint requires choosing which Backend interface to extend, making the contract explicit.

2. Per-route version gating via option functions#

Routes are annotated with version constraints using Go option functions at declaration time:

router.NewPostRoute("/containers/prune", c.postContainersPrune, router.WithMinimumAPIVersion("1.25"))
router.NewGetRoute("/containers/{name}/checkpoints", cr.getContainerCheckpoints, router.Experimental)

This is cleaner than runtime version checks scattered in handler bodies and makes the API version matrix visible in initRoutes().

3. BuildKit session multiplexing#

BuildKit’s session API (used for cache mounts, SSH agent forwarding, secrets, OIDC tokens) is multiplexed via a single POST /session endpoint that upgrades to HTTP/2 and then carries multiple gRPC streams. This allows a single daemon HTTP listener to serve both Docker API REST and BuildKit gRPC without separate ports or processes.

4. Plugin protocol designed for filesystem discovery#

v1/v2 plugins are discovered via filesystem conventions (/run/docker/plugins/*.sock, /etc/docker/plugins/*.spec) and activated lazily. The GET /Plugin.Activate handshake returns a capability list that determines which subsystem the plugin implements. This means adding a new plugin type doesn’t require daemon changes — only a new spec file and a process that speaks the HTTP protocol.

5. AuthZ as response-intercepting middleware#

Unlike most authorization systems that only check requests, Moby’s authz middleware also passes responses through all authz plugins. This allows plugins to strip sensitive data from responses (e.g., redact environment variables containing secrets from GET /containers/{id}/json) — a capability unique to Moby’s design.

6. Separate api/ module for type contracts#

The api/types/ module (github.com/moby/moby/api) has its own go.mod and contains only type definitions — no HTTP code, no daemon code. This means tools generating clients from OpenAPI specs, or projects that need type compatibility, can depend on just the types module without pulling in the entire daemon. This is an intentional decoupling for ecosystem health.