入口文件:daemon/server/server.go
适用范围:Docker/Moby daemon 暴露的 RESTful HTTP API(即 dockerd 监听的 TCP/Unix socket 接口)
1. 总览
daemon/server/server.go 定义了 Docker daemon HTTP API 的核心 Server 结构体。它本身不绑定监听地址(监听由 daemon 上层负责,比如 unix socket / tcp / fd),它的职责是:
- 接收一组
router.Router(每个 router 负责一个资源域,例如 container、image、volume、system、network、swarm 等)。
- 把它们的所有
Route注册到gorilla/mux路由器 ,同时自动为每条路由生成两个 URL:一个带版本前缀/v{version}/...,一个不带版本前缀/...。
- 在路由和最终业务 handler 之间串联一组全局中间件(version、experimental、debug 等)。
- 统一错误处理 :handler 返回
error,由 server 转换成 HTTP status code 和 JSON 响应体。
整体调用链(自外向内):
Go
HTTP 请求
└─ otelhttp.NewHandler (OpenTelemetry 包装,span = "METHOD path")
└─ Server.makeHTTPHandler (入口:注入 ctx、调用中间件链、错误统一处理)
└─ Server.handlerWithGlobalMiddlewares (VersionMiddleware → ExperimentalMiddleware → ... → 业务 handler)
└─ route.Handler() (真正的业务函数,例如 getVolumesList)
2. 核心类型与常量
2.1 versionMatcher(版本前缀)
Go
const versionMatcher = "/v{version:[0-9.]+}"
这是整个 API 路由的版本协商核心。{version:[0-9.]+} 是 gorilla/mux 的变量捕获:
- 捕获到的值会出现在
vars["version"]里;
mux把它注入到request的 context(通过mux.Vars(r));
- 后续
VersionMiddleware取出该值,与minAPIVersion/defaultAPIVersion比对。
最终暴露的 URL 形如:
Go
GET /v1.42/volumes ← 带版本前缀(客户端显式协商版本)
GET /volumes ← 不带版本前缀(使用默认版本)
非常精简,只持有一个全局中间件链。router.Router 不直接挂在 Server 上,而是在 CreateMux 时作为参数传入。这种设计让 Server 与"具体有哪些业务路由"解耦------上层 daemon 负责按需装配 router 集合。
2.3 statusClientClosedRequest = 499
非标准 HTTP 状态码(来自 NGINX 习惯),用于客户端在响应返回前断开连接的场景。仅写入响应以供 OTEL/metrics 看到,并写一条 info 日志,不会真正发给客户端(连接已断)。
3. 路由注册:CreateMux
Go
func (s *Server) CreateMux(ctx context.Context, routers ...router.Router) *mux.Router
3.1 注册流程
Go
for each apiRouter in routers:
for each r in apiRouter.Routes():
f := s.makeHTTPHandler(r)
m.Path(versionMatcher + r.Path()).Methods(r.Method()).Handler(f) // /v1.42/...
m.Path(r.Path()).Methods(r.Method()).Handler(f) // /...
要点:
- 每条路由会被注册两次 ------一次带版本前缀,一次不带。这让客户端既可以显式指定版本(
curl /v1.41/info),也可以省略(curl /info)走默认版本。
- 注册是可中断的 :每条路由注册前都会检查
ctx.Err(),便于 daemon 关闭时提前结束初始化。
- 逐条 debug 日志 :注册过程在 debug 级别日志下打印每一条
method/path,便于排查"路由没生效"类问题。
3.2 兜底路由(405 / 404)
Go
m.HandleFunc(versionMatcher+"/{path:.*}", notFoundHandler)
m.NotFoundHandler = notFoundHandler
m.MethodNotAllowedHandler = notFoundHandler
所有未匹配的路径(包括带版本前缀但路径不存在的请求)统一返回:
Go
HTTP/1.1 404 Not Found
Content-Type: application/json
{"message":"page not found"}
MethodNotAllowedHandler 也被设置为 notFoundHandler ------ 这是有意为之:默认 mux 在方法不匹配时会返回 405,但 Docker 选择把它一并归到 404,避免泄漏"端点存在但方法不对"的信息。
3.3 router.Router 接口
每个资源域(container、image、volume...)都实现:
Go
type Router interface {
Routes() []Route
}
type Route interface {
Handler() httputils.APIFunc // func(ctx,w,r,vars) error
Method() string // GET / POST / PUT / DELETE / OPTIONS / HEAD
Path() string // 例如 "/volumes/{name:.*}"
}
构造路由的便捷函数(见 router/local.go):
|--------------------------------------------|---------|
| 函数 | HTTP 方法 |
| NewGetRoute(path, handler, opts...) | GET |
| NewPostRoute(...) | POST |
| NewPutRoute(...) | PUT |
| NewDeleteRoute(...) | DELETE |
| NewOptionsRoute(...) | OPTIONS |
| NewHeadRoute(...) | HEAD |
| NewRoute(method, path, handler, opts...) | 任意 |
opts ...RouteWrapper 是路由级装饰器(注意:与下面的全局 middleware 不同),典型用法:
Go
router.NewPostRoute("/volumes/prune", v.postVolumesPrune, router.WithMinimumAPIVersion("1.25"))
router.NewPutRoute("/volumes/{name:.*}", v.putVolumesUpdate, router.WithMinimumAPIVersion("1.42"))
WithMinimumAPIVersion 在 handler 外再套一层"版本不够直接 400"的壳,使旧客户端看不到新端点。注意它返回 400 而不是 404------因为业务 handler 普遍用 404 表示"对象不存在"(例如 GET /volumes/foo 找不到该 volume),如果端点本身也 404,客户端无法区分。
3.4 一个具体例子:volume 路由
Go
func (v *volumeRouter) initRoutes() {
v.routes = []router.Route{
router.NewGetRoute("/volumes", v.getVolumesList),
router.NewGetRoute("/volumes/{name:.*}", v.getVolumeByName),
router.NewPostRoute("/volumes/create", v.postVolumesCreate),
router.NewPostRoute("/volumes/prune", v.postVolumesPrune, router.WithMinimumAPIVersion("1.25")),
router.NewPutRoute("/volumes/{name:.*}", v.putVolumesUpdate, router.WithMinimumAPIVersion("1.42")),
router.NewDeleteRoute("/volumes/{name:.*}", v.deleteVolumes),
}
}
经过 CreateMux 注册后,最终生效的 URL(节选):
|------|------------------------------------------------|-------------|
| 方法 | URL | 备注 |
| GET | /volumes、/v{version}/volumes | 列出卷 |
| GET | /volumes/{name}、/v{version}/volumes/{name} | 查单个卷 |
| POST | /volumes/prune、/v{version}/volumes/prune | 最低 API 1.25 |
| PUT | /volumes/{name}、/v{version}/volumes/{name} | 最低 API 1.42 |
4. 中间件链
4.1 Middleware 接口
Go
type Middleware interface {
WrapHandler(APIFunc) APIFunc
}
任何结构体只要实现 WrapHandler 就是一个中间件。注意它的方向是"洋葱式"的:WrapHandler(next) 返回的新 handler 在调 next 前后做自己的事。
4.2 装配顺序
Go
// daemon/server/middleware.go
func (s *Server) handlerWithGlobalMiddlewares(handler httputils.APIFunc) httputils.APIFunc {
next := handler
for _, m := range s.middlewares {
next = m.WrapHandler(next)
}
if log.GetLevel() >= log.DebugLevel {
next = middleware.DebugRequestMiddleware(next)
}
return next
}
关键注释原文:
The order of the middlewares is backwards, meaning that the first in the list will be evaluated last.
也就是说,s.middlewares[0]是最外层 (最先看到请求、最后看到响应),s.middlewares[len-1] 是最内层(紧贴业务 handler)。装配时是反向 wrap,所以代码上看是顺序 append,运行时是栈式调用。
如果日志级别 ≥ Debug,会再额外套一层 DebugRequestMiddleware(最内层,靠近业务)。
4.3 三个内置中间件
4.3.1 VersionMiddleware(middleware/version.go)
最关键的中间件,承担三件事:
- 写响应头 :
Server: Docker/<serverVersion> (<GOOS>)、Api-Version: <defaultAPIVersion>、Ostype: <GOOS>。
- 版本协商 :从
vars["version"]取客户端请求的版本;为空时用defaultAPIVersion兜底。
- 版本范围检查 :低于
minAPIVersion或高于defaultAPIVersion都返回versionUnsupportedError(带InvalidParameter()标记 → 400)。
- 把版本塞进 ctx :
ctx = context.WithValue(ctx, APIVersionKey{}, apiVersion),业务 handler 通过httputils.VersionFromContext(ctx)读取,按版本走不同分支(典型场景:API 1.44 之前隐藏某些字段)。
构造函数会做完整性校验:
Go
NewVersionMiddleware(serverVersion, defaultAPIVersion, minAPIVersion)
// 要求:minAPIVersion ≤ defaultAPIVersion
// 且都在 [config.MinAPIVersion, config.MaxAPIVersion] 区间内
4.3.2 ExperimentalMiddleware(middleware/experimental.go)
只做一件事:在每个响应上加 Docker-Experimental: true|false 头。客户端可以据此判断当前 daemon 是否开启了实验特性。
4.3.3 DebugRequestMiddleware(middleware/debug.go)
仅在 debug 日志级别生效。会:
- 打印每条请求的
method、request-url、vars;
- 对 POST 请求,若 Content-Type 是 JSON 且 body ≤ 4KB,会读取并解析 body,屏蔽敏感字段后 (
password、secret、data、jointoken、signingcakey、unlockkey)输出到form-data字段;
- handler 出错时把
error-response和status也加进日志字段。
安全提示:敏感字段屏蔽是基于字段名的"白名单 + 大小写不敏感"匹配。新增加密字段的 API 端点必须同步更新 scrub 列表,否则明文会出现在 debug 日志里。
4.4 路由级装饰器 vs 全局中间件
容易混淆,这里区分一下:
|------|-----------------------------------------|------------------------------------------------------------------|
| 维度 | 全局 Middleware | 路由级 RouteWrapper(如 WithMinimumAPIVersion) |
| 定义位置 | middleware/*.go,实现 Middleware 接口 | router/local.go、router/experimental.go,是 func(Route) Route |
| 装配方式 | Server.UseMiddleware(...),对所有路由生效 | router.NewPostRoute(path, h, opts...),只对这一条路由生效 |
| 执行时机 | 在 handlerWithGlobalMiddlewares 里串联 | 在路由构造时就把原 handler 包了一层 |
| 运行顺序 | 全局中间件链 → 路由级 wrapper → 真正业务 handler | 见左 |
4.5 实验性路由:Experimental
Go
// router/experimental.go
func Experimental(r Route) Route
把任意 route 标记为实验性。被标记后默认走 experimentalHandler,直接返回:
Go
This experimental feature is disabled by default. Start the Docker daemon in experimental mode in order to enable it.
且实现了 NotImplemented() 接口,会被 httpstatus.FromError 映射为 501 Not Implemented 。daemon 启动时如果开了实验特性,会调用 Enable() 把 handler 切回真正的业务函数。
5. 请求处理:makeHTTPHandler
这是每条路由真正的 http.HandlerFunc。它把 gorilla/mux 风格的 (w, r) 适配到 Docker 内部的 APIFunc(ctx, w, r, vars) 风格,并完成四件事:
5.1 注入 context
Go
ua := r.Header.Get("User-Agent")
ctx := baggage.ContextWithBaggage(
dockerversion.WithUpstreamUserAgent(r.Context(), ua),
otelutil.MustNewBaggage(
otelutil.MustNewMemberRaw(otelutil.TriggerKey, "api"),
),
)
r = r.WithContext(ctx)
- 把
User-Agent写进 ctx(用于追踪"是谁发起的 API 调用",区分 CLI / 客户端库);
- 添加 OTel baggage,标记
trigger=api,便于 telemetry 区分 API 触发的事件和其他触发源。
5.2 串联中间件并执行
Go
handlerFunc := s.handlerWithGlobalMiddlewares(handler)
vars := mux.Vars(r)
if err := handlerFunc(ctx, w, r, vars); err != nil { ... }
5.3 客户端断连分支
Go
if r.Context().Err() != nil {
w.WriteHeader(statusClientClosedRequest) // 499, 仅用于 metrics/OTEL
log.G(ctx).WithFields(...).Info("request cancelled by client")
return
}
这是优先级最高的分支:只要请求 context 已经取消(客户端走了),就别再尝试写 JSON 错误体。强行写会污染日志(写失败)、浪费带宽(虽然已断)。这里只写状态码(给中间件的 metrics 看)和一条 info 日志。
5.4 错误转换
Go
statusCode := httpstatus.FromError(err)
if v := vars["version"]; v != "" && versions.LessThan(v, "1.24") {
http.Error(w, err.Error(), statusCode) // 老客户端:纯文本
} else {
httputils.WriteJSON(w, statusCode, &common.ErrorResponse{
Message: err.Error(), // 新客户端:JSON
})
}
if statusCode >= 500 {
log.G(ctx).Errorf("Handler for %s %s returned error", ...)
}
三个细节:
httpstatus.FromError根据 error 实现的标记接口(NotFound()、InvalidParameter()、Conflict()等)映射到 HTTP status。比如errdefs.NotFound(err)→ 404,errdefs.InvalidParameter(err)→ 400,gRPC 错误和 distribution registry 错误也有专门分支。
- API < 1.24 走纯文本:Docker 已经不再支持 1.24 以下,但万一有极老客户端连进来,给它返回 JSON 会显示成一坨原始 JSON 文本,体验更差。注释里写"Let's be nice"------对已经不支持的客户端仍尽量给可读错误。
- 5xx 才记 error 日志:4xx 是客户端问题(参数错、没权限、对象不存在),算正常业务流量,不污染错误日志。
5.5 OTel 包装
整个 http.HandlerFunc 最外层被 otelhttp.NewHandler(..., operation) 包裹,operation = "METHOD path"(例如 "GET /volumes")。这一层负责创建 span、记录耗时和状态码,与业务逻辑解耦。
6. 典型请求生命周期示例
请求:GET /v1.42/volumes HTTP/1.1
|----|--------------------------------------|--------------------------------------------------------------------------------------|
| 步骤 | 组件 | 动作 |
| 1 | otelhttp | 创建 span GET /volumes |
| 2 | mux 路由匹配 | 命中 /v{version:[0-9.]+}/volumes → volumeRouter.getVolumesList |
| 3 | makeHTTPHandler | 注入 ctx(UA、baggage) |
| 4 | VersionMiddleware | 校验 1.42 ∈ min, default;写 Server / Api-Version / Ostype 响应头;把 1.42 塞进 ctx |
| 5 | ExperimentalMiddleware | 写 Docker-Experimental: false(或 true) |
| 6 | DebugRequestMiddleware(仅 debug 级别) | 打印请求字段 |
| 7 | getVolumesList | 解析 form、调用 backend.List、按版本决定是否查 swarm 集群卷、合并、WriteJSON |
| 8 | 返回 | span 收尾,状态码 200 |
如果是请求 /volumes(无版本前缀),步骤 4 中 vars["version"] == "",自动落到 defaultAPIVersion,其余流程一致。
7. 时序图:注册阶段
Go
daemon.NewDaemon
│
├─ 创建各个 backend(container/image/volume/network/swarm/...)
│
├─ 各 router.NewRouter(backend, cluster, ...) ──→ []router.Router
│
├─ server.Server{}.UseMiddleware(versionMiddleware)
├─ server.Server{}.UseMiddleware(experimentalMiddleware)
│
└─ mux := srv.CreateMux(ctx, allRouters...)
│
├─ for each router, for each route:
│ makeHTTPHandler(route) ← 包装 otelhttp + 中间件链
│ mux.Path("/v{version:[0-9.]+}" + path).Methods(method).Handler(f)
│ mux.Path(path).Methods(method).Handler(f)
│
├─ NotFoundHandler / MethodNotAllowedHandler → JSON 404
└─ return mux (由上层 daemon 绑定到 http.Server 监听)
8. 扩展指南
8.1 新增一条 API 路由
- 找到对应资源域的 router(如
daemon/server/router/volume/volume.go)。
- 在
initRoutes()里追加一行:
Go
router.NewPostRoute("/volumes/{name}/foo", v.postVolumeFoo,
router.WithMinimumAPIVersion("1.45"))
3.在同包内实现postVolumeFoo(ctx, w, r, vars) error,返回错误时用errdefs包裹以获得正确的 HTTP status。
4无需修改server.go------CreateMux会自动注册带版本前缀和不带前缀两个 URL。
8.2 新增一个全局中间件
1在daemon/server/middleware/下新建文件,定义实现Middleware接口的结构体。
2在 daemon 启动时调用srv.UseMiddleware(yourMiddleware)。
3注意装配顺序:先 append 的中间件在最外层。VersionMiddleware通常应该最外层(确保所有响应都带版本头),所以它一般第一个被 append。
8.3 新增一个 API 版本
参见daemon/config的MinAPIVersion/MaxAPIVersion。新增版本时要:
1在每个端点上按需追加WithMinimumAPIVersion("x.yy")或在 handler 内用httputils.VersionFromContext(ctx)做版本分支(隐藏新字段或新行为)。
2更新VersionMiddleware的构造参数,使其覆盖新版本。
3不要破坏旧版本的行为------这是 Docker API 的兼容性硬约束。
- 文件索引
|------------------------------------------------------------------------|----------------------------------------------------|
| 文件 | 作用 |
| daemon/server/server.go | 入口:Server、CreateMux、makeHTTPHandler |
| daemon/server/middleware.go | handlerWithGlobalMiddlewares(中间件链装配) |
| daemon/server/middleware/middleware.go | Middleware接口 |
| daemon/server/middleware/version.go | 版本协商 + 响应头注入 |
| daemon/server/middleware/experimental.go | Docker-Experimental响应头 |
| daemon/server/middleware/debug.go | debug 日志中间件 + 敏感字段屏蔽 |
| daemon/server/router/router.go | Router/Route接口 |
| daemon/server/router/local.go | localRoute+NewGetRoute等便捷构造 +WithMinimumAPIVersion |
| daemon/server/router/experimental.go | Experimental()路由装饰器(501 切换) |
| daemon/server/httputils/httputils.go | APIFunc类型、APIVersionKey、JSON 读写工具 |
| daemon/server/httpstatus/status.go | FromError:error → HTTP status 映射 |
| daemon/server/router/{container,image,volume,system,network,swarm,...} | 各资源域具体路由 |