Docker-API路由架构详解
一句话定位
daemon/server/router/<resource>/ 是 Docker daemon 对外暴露 HTTP API 的入口。
每一条 docker ps、docker run、docker pull 命令,最终都会变成一个 HTTP 请求,落到这里的某条路由上。
dockerd 的对外接口不是 gRPC、不是自定义协议,就是最朴素的 HTTP/JSON RESTful API。
所有路由都按「资源」拆成独立目录,每个目录里管自己的事。
全景图:一条 docker 命令的旅程
docker CLI dockerd(HTTP server) daemon 业务层
──────────── ───────────────────────── ──────────────
docker run nginx
│
│ HTTP POST /v1.42/containers/create body=JSON
▼
┌──────────────────┐
│ gorilla/mux 路由 │ ← 把 URL + Method 匹配到 handler
└──────────────────┘
│
│ 匹配到 container.NewRouter 注册的 /containers/create
▼
┌──────────────────┐
│ Route 三元组 │ (method=POST, path="/containers/create", handler=postContainersCreate)
└──────────────────┘
│
▼
┌──────────────────────────────────┐
│ container_routes.go │ 解析 JSON → 校验 → 调 backend
│ postContainersCreate(...) │
└──────────────────────────────────┘
│
│ backend.ContainerCreate(ctx, config)
▼
┌──────────────────────────────────┐
│ daemon.create.go │ 真正的容器创建逻辑
│ ContainerCreate → containerCreate│ 生成容器 ID、写元数据
└──────────────────────────────────┘
│
▼
返回 {Id, Warnings} → handler 序列化为 JSON → HTTP 响应
记住这张图,整篇文档都在解释它。
2. 目录结构:一资源一目录
打开 daemon/server/router/,你会看到一组按资源命名的子目录:
|
目录 |
HTTP 前缀 |
负责什么 |
对应 docker 命令 |
|
|
|
容器生命周期、exec、文件拷贝 |
|
|
|
|
镜像 list/inspect/pull/push/tag/delete |
|
|
|
|
网络创建、连接、断开 |
|
|
|
|
数据卷 CRUD |
|
|
|
|
镜像构建 |
|
|
|
|
系统/守护进程信息 |
|
|
|
|
Swarm 集群 |
|
|
|
|
buildkit 长连接会话(已废弃,向后兼容) |
buildkit 内部 |
|
|
|
插件管理 |
|
|
|
|
镜像仓库元信息(manifest、blob) |
|
|
|
|
容器检查点(实验性) |
|
|
|
gRPC 路由 |
已废弃的 gRPC builder 接口 |
(无对外命令) |
|
|
|
pprof 调试端点 |
调试用 |
约定:每个 <resource>/ 目录里就是三件套——
container/
├── <resource>.go # 路由表:声明所有 (method, path, handler) 三元组
├── backend.go # Backend 接口:定义 router 需要 daemon 提供哪些方法
├── <resource>_routes.go # handler 实现:解析 HTTP 请求 → 调 backend → 写响应
└── *_test.go # 测试
个别资源(如 system/)把路由表直接写在 <resource>.go 里没拆 initRoutes,但本质一样。
3. 三件套详解
3.1 路由表:<resource>.go
每个资源都有一个 <resource>Router 结构体,持有:
backend Backend:业务接口(不直接持有 daemon,做解耦)
routes []router.Route:路由列表
以 container/container.go:46 为例:
type containerRouter struct {
backend Backend
routes []router.Route
}
func NewRouter(b Backend) router.Router {
r := &containerRouter{backend: b}
r.initRoutes()
return r
}
func (c *containerRouter) initRoutes() {
c.routes = []router.Route{
router.NewGetRoute ("/containers/json", c.getContainersJSON),
router.NewPostRoute("/containers/create", c.postContainersCreate),
router.NewPostRoute("/containers/{name:.*}/start", c.postContainersStart),
router.NewPostRoute("/containers/prune", c.postContainersPrune,
router.WithMinimumAPIVersion("1.25")), // 1.25 之后才有
router.NewDeleteRoute("/containers/{name:.*}", c.deleteContainers),
// ...
}
}
几个要点:
router.NewXxxRoute(method, path, handler)是工厂方法,XXX = Get/Post/Put/Delete/Head/Options。
- 路径用 gorilla/mux 语法:
{name:.*}是路径参数,.*是允许的字符正则。匹配后vars["name"]可以取出值。
router.WithMinimumAPIVersion("1.25")是路由级装饰器:API 版本不够的客户端访问会被拒绝(见第 4 节)。
- 路由表就是一张静态查找表,没注册的 URL 直接 404。注册顺序在某些场景下有意义(见第 8 节 FAQ)。
3.2 Backend 接口:backend.go
router 不直接调用*daemon.Daemon,而是依赖一个 Go interface。
这样做的好处:router 可以脱离真实 daemon 单测,将来换实现也不影响 HTTP 层。
container/backend.go:78 把这个 interface 拆得很细:
type Backend interface {
commitBackend // CreateImageFromContainer(...)
execBackend // ContainerExecCreate/Start/Inspect(...)
copyBackend // ContainerArchivePath/Export/ExtractToDir(...)
stateBackend // ContainerCreate/Start/Stop/Kill/Pause/...
monitorBackend // ContainerInspect/Logs/Stats/Top/Containers(...)
attachBackend // ContainerAttach(...)
systemBackend // ContainerPrune(...)
sysInfoProvider // RawSysInfo()
}
每个子接口对应一类功能。*daemon.Daemon 实现了全部方法,所以传给 NewRouter(opts.daemon) 没问题。
新手关键认知:「router 是薄壳,daemon 是厚核心」。HTTP handler 普遍只有 10~30 行,复杂度全在 daemon 业务层。
3.3 handler:<resource>_routes.go
handler 统一签名(来自 httputils.APIFunc):
func (c *containerRouter) postContainersStart(
ctx context.Context,
w http.ResponseWriter,
r *http.Request,
vars map[string]string, // gorilla/mux 提取的路径参数
) error
统一签名的好处:上层可以套统一的中间件(鉴权、版本检查、错误转换、日志、tracing)。
handler 通用模式(4 步):
func (c *containerRouter) postContainersStart(ctx, w, r, vars) error {
// 1. 解析请求(路径参数 / form / JSON body)
name := vars["name"]
// 2. 可选校验(参数合法性)
// ...
// 3. 调 backend(业务全在 daemon 里)
if err := c.backend.ContainerStart(ctx, name, "", ""); err != nil {
return err // ← 注意:直接 return error,不写 HTTP status code
}
// 4. 写响应
w.WriteHeader(http.StatusNoContent) // 204
return nil
}
关键设计:handler 只 return error,不写 status code。
error 由上层中间件根据类型自动转成 HTTP 状态码:
errdefs.NotFound→ 404
errdefs.InvalidParameter→ 400
errdefs.Conflict→ 409
- 其他 → 500
这样 handler 专注业务,错误处理统一。详见 daemon/server/httpstatus/。
4. 路由核心:local.go 拆解
daemon/server/router/local.go 是路由系统的最小积木。全文不到 120 行,必须看懂。
4.1 Route 接口与 localRoute 实现
router.go 定义接口:
type Route interface {
Handler() httputils.APIFunc // 处理函数
Method() string // HTTP 方法
Path() string // URL 模式
}
local.go 给出唯一实现:
type localRoute struct {
method string
path string
handler httputils.APIFunc
}
(method, path, handler) 三元组——所有 HTTP 框架里最经典的模型。
4.2 NewRoute:工厂 + 装饰器
func NewRoute(method, path string, handler httputils.APIFunc, opts ...RouteWrapper) Route {
var r Route = localRoute{method, path, handler}
for _, o := range opts {
r = o(r) // ← 逐层包裹
}
return r
}
opts ...RouteWrapper 是**装饰器模式(middleware)**的精髓:
传 WithMinimumAPIVersion("1.25"),就给 handler 套一层「检查版本」的壳;
将来还能套「实验性开关」「权限校验」等。
NewGetRoute/NewPostRoute/... 都是对 NewRoute 的薄封装,只固定了 HTTP method。
4.3 WithMinimumAPIVersion:版本协商核心
Docker API 有版本概念(v1.41、v1.42、……)。客户端访问 /v1.42/containers/...,server 怎么知道?
func WithMinimumAPIVersion(minAPIVersion string) RouteWrapper {
return func(route Route) Route {
return localRoute{
method: route.Method(),
path: route.Path(),
handler: func(ctx, w, r, vars) error {
v := httputils.VersionFromContext(ctx)
if v != "" && versions.LessThan(v, minAPIVersion) {
return versionError(...) // → 400 InvalidParameter
}
return route.Handler()(ctx, w, r, vars)
},
}
}
}
为什么版本不够返回 400 而不是 404?
源码注释里讲得很清楚(也是新手最容易踩的坑):
业务逻辑广泛用 404 表示「对象不存在」(比如 GET /volumes/foo 找不到就 404)。
如果「端点不存在」也返回 404,客户端无法区分两种情况。
统一用 400(InvalidParameter),客户端能正确分支处理。
4.4 localRoute 没有实现的:ExperimentalRoute
注意 local.go 里没有「实验性路由」逻辑——它在 experimental.go 里通过类型断言实现:
// daemon/server/router/experimental.go(同目录)
type ExperimentalRoute interface {
Route
Enable()
}
只有 daemon 开启了实验特性,才会调用 Enable() 激活这些路由。具体启用点在
daemon/command/daemon.go:1012 附近的 if opts.daemon.HasExperimental() 分支。
5. 路由注册全流程:从启动到生效
router 自己只是数据结构,谁来把它们装进真正的 HTTP server?看 daemon/command/daemon.go:989:
func initRouters(opts daemonOptions) []router.Router {
routers := []router.Router{
// 注意顺序:checkpoint 必须在 container 之前,否则 DELETE 会被 container 抢匹配
checkpointrouter.NewRouter(opts.daemon),
container.NewRouter(opts.daemon),
image.NewRouter(opts.daemon, opts.daemon.RegistryService()),
systemrouter.NewRouter(opts.daemon, opts.cluster, opts.builder.buildkit, opts.daemon.Features),
volume.NewRouter(opts.daemon.VolumesService(), opts.cluster),
build.NewRouter(opts.builder.backend, opts.daemon),
sessionrouter.NewRouter(opts.builder.sessionManager),
swarmrouter.NewRouter(opts.cluster),
pluginrouter.NewRouter(opts.daemon.PluginManager()),
distributionrouter.NewRouter(opts.daemon.ImageBackend()),
network.NewRouter(opts.daemon, opts.cluster),
debugrouter.NewRouter(),
}
if opts.builder.backend != nil {
routers = append(routers, grpcrouter.NewRouter(opts.builder.backend))
}
if opts.daemon.HasExperimental() {
// 启用所有 ExperimentalRoute
}
return routers
}
之后这些 router 被丢给 apiserver.Server(daemon/server/server.go),server 把每条 route 注册到底层 gorilla/mux,再加版本前缀 /v{version}/,再叠上中间件链:
请求进来
→ CORS 中间件
→ Debug 中间件
→ 实验性中间件
→ 版本协商中间件(注入版本号到 ctx)
→ 鉴权中间件(authorization plugin)
→ gorilla/mux 路由匹配
→ RouteWrapper 链(如 WithMinimumAPIVersion)
→ handler
6. 实战:跟着 docker run nginx 走一遍
这是把所有知识点串起来的最佳练习。
Step 1:CLI 端
docker run nginx → 实际发 两个 HTTP 请求:
POST /v1.42/containers/createbody=容器配置 → 拿到容器 ID
POST /v1.42/containers/{id}/start→ 启动
Step 2:路由匹配
两个 URL 都在 container/container.go:67 的路由表里:
/containers/create→c.postContainersCreate
/containers/{name:.*}/start→c.postContainersStart
Step 3:handler 解析(container/container_routes.go)
postContainersCreate 大致流程:
httputils.ReadJSON(r, &body)解析 JSON
- 调
runconfig.DecodeContainerConfig把 JSON 转成container.Config+hostconfig.HostConfig
- 校验:不能同时
detach=true和stdin_open=true
- 调
c.backend.ContainerCreate(ctx, config)→ 走到daemon.create.go::ContainerCreate
- 返回
{"Id": "...", "Warnings": [...]}
postContainersStart 流程:
vars["name"]取容器名/ID
- 调
c.backend.ContainerStart(ctx, name, checkpoint, checkpointDir)
- 成功返回 204
Step 4:daemon 业务层
daemon.create.go / daemon.start.go 才是真正干活的地方——读镜像、写容器元数据、建 namespace、调 containerd 等。这一层已经和 HTTP 无关。
Step 5:响应回写
handler 把结果 JSON 序列化 → w.Write(...) → docker CLI 收到 → 打印容器 ID 给用户。
7. 各资源路由速查表
下面是各资源的核心端点一览,方便你拿到一条 docker 命令时,立刻定位代码。
container(container/container.go)
|
端点 |
handler |
docker 命令 |
|
GET |
|
|
|
GET |
|
|
|
GET |
|
|
|
GET |
|
|
|
GET |
|
|
|
GET |
|
|
|
GET |
|
|
|
GET |
|
|
|
POST |
|
|
|
POST |
|
|
|
POST |
|
|
|
POST |
|
|
|
POST |
|
|
|
POST |
|
|
|
POST |
|
|
|
POST |
|
|
|
POST |
|
|
|
POST |
|
|
|
POST |
|
|
|
POST |
|
|
|
POST |
|
|
|
POST |
|
|
|
PUT |
|
|
|
DELETE |
|
|
image(image/image.go)
|
端点 |
docker 命令 |
|
GET |
|
|
GET |
|
|
GET |
|
|
GET |
|
|
GET |
|
|
POST |
|
|
POST |
|
|
POST |
|
|
POST |
|
|
POST |
|
|
DELETE |
|
network(network/network.go)
|
端点 |
docker 命令 |
|
GET |
|
|
GET |
|
|
POST |
|
|
POST |
|
|
POST |
|
|
POST |
|
|
DELETE |
|
volume(volume/volume.go)
|
端点 |
docker 命令 |
|
GET |
|
|
GET |
|
|
POST |
|
|
POST |
|
|
PUT |
|
|
DELETE |
|
system(system/system.go)
|
端点 |
docker 命令 |
|
GET |
健康检查(CLI 启动时探测) |
|
GET |
|
|
GET |
|
|
GET |
|
|
GET |
|
|
POST |
|
|
OPTIONS |
CORS 预检 |
build(build/build.go)
|
端点 |
docker 命令 |
|
POST |
|
|
POST |
|
|
POST |
取消进行中的 build |
8. FAQ 与进阶话题
Q1:为什么 checkpoint router 必须在 container router 之前注册?
daemon/command/daemon.go:990 注释明说:因为 DELETE /containers/{id}/checkpoints/{checkpoint} 和
DELETE /containers/{name:.*} 路径有重叠。gorilla/mux 按注册顺序匹配,先注册的优先。
如果 container 在前,所有 /containers/... 的 DELETE 都会被 deleteContainers 抢走。
Q2:handler 报错怎么变成 HTTP 状态码?
看 daemon/server/httpstatus/:它根据 error 实现的接口(NotFound()、InvalidParameter()、Conflict() 等)
映射到 404/400/409。错误类型定义在 errdefs 包。
Q3:URL 前缀的 /v1.42/ 是谁加的?
不是各资源 router 自己加,是 server 层用 subrouter 自动拼上 /v{version}/。
版本号由 VersionMiddleware 解析客户端请求头/URL 后注入 ctx,handler 通过 httputils.VersionFromContext(ctx) 取。
Q4:router 之间会复用 backend 吗?
会。比如 container.NewRouter(opts.daemon)、image.NewRouter(opts.daemon, ...)、
network.NewRouter(opts.daemon, ...) 传的都是同一个 *daemon.Daemon,
只是各自从 Backend 接口里挑自己需要的方法。
Q5:handler 里的 streaming(logs、attach、events)怎么实现?
通过 hijack HTTP 连接 / chunked transfer / websocket。比如:
getContainersLogs把 backend 返回的<-chan *LogMessage逐条写到 ResponseWriter
wsContainersAttach用golang.org/x/net/websocket升级连接
postContainersAttach直接 hijack TCP 连接
这部分属于进阶话题,新手先理解同步 JSON 端点即可。
Q6:为什么有些资源有两个 backend(Backend + ClusterBackend)?
如 volume、network、system、swarm:单机功能走 Backend,
Swarm 集群相关功能走 ClusterBackend。在非 Swarm 节点上 ClusterBackend 是 nil,
handler 内部判 nil 决定走单机还是集群分支。
更多推荐

所有评论(0)