入口文件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 响应体。整体调用链自外向内HTTP 请求 └─ otelhttp.NewHandler OpenTelemetry 包装span METHOD path └─ Server.makeHTTPHandler 入口注入 ctx、调用中间件链、错误统一处理 └─ Server.handlerWithGlobalMiddlewares VersionMiddleware → ExperimentalMiddleware → … → 业务 handler └─ route.Handler() 真正的业务函数例如 getVolumesList2. 核心类型与常量2.1versionMatcher版本前缀const versionMatcher /v{version:[0-9.]}这是整个 API 路由的版本协商核心。{version:[0-9.]}是gorilla/mux的变量捕获捕获到的值会出现在vars[version]里mux把它注入到request的 context通过mux.Vars(r)后续VersionMiddleware取出该值与minAPIVersion/defaultAPIVersion比对。最终暴露的 URL 形如GET /v1.42/volumes ← 带版本前缀客户端显式协商版本 GET /volumes ← 不带版本前缀使用默认版本非常精简只持有一个全局中间件链。router.Router不直接挂在Server上而是在CreateMux时作为参数传入。这种设计让Server与具体有哪些业务路由解耦——上层 daemon 负责按需装配 router 集合。2.3statusClientClosedRequest 499非标准 HTTP 状态码来自 NGINX 习惯用于客户端在响应返回前断开连接的场景。仅写入响应以供 OTEL/metrics 看到并写一条 info 日志不会真正发给客户端连接已断。3. 路由注册CreateMuxfunc (s *Server) CreateMux(ctx context.Context, routers ...router.Router) *mux.Router3.1 注册流程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 / 404m.HandleFunc(versionMatcher/{path:.*}, notFoundHandler) m.NotFoundHandler notFoundHandler m.MethodNotAllowedHandler notFoundHandler所有未匹配的路径包括带版本前缀但路径不存在的请求统一返回HTTP/1.1 404 Not Found Content-Type: application/json {message:page not found}MethodNotAllowedHandler也被设置为notFoundHandler—— 这是有意为之默认mux在方法不匹配时会返回 405但 Docker 选择把它一并归到 404避免泄漏端点存在但方法不对的信息。3.3router.Router接口每个资源域container、image、volume…都实现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...)GETNewPostRoute(...)POSTNewPutRoute(...)PUTNewDeleteRoute(...)DELETENewOptionsRoute(...)OPTIONSNewHeadRoute(...)HEADNewRoute(method, path, handler, opts...)任意opts ...RouteWrapper是路由级装饰器注意与下面的全局 middleware 不同典型用法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 路由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.25PUT/volumes/{name}、/v{version}/volumes/{name}最低 API 1.424. 中间件链4.1Middleware接口type Middleware interface { WrapHandler(APIFunc) APIFunc }任何结构体只要实现WrapHandler就是一个中间件。注意它的方向是洋葱式的WrapHandler(next)返回的新 handler 在调next前后做自己的事。4.2 装配顺序// 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.1VersionMiddlewaremiddleware/version.go最关键的中间件承担三件事写响应头Server: Docker/serverVersion (GOOS)、Api-Version: defaultAPIVersion、Ostype: GOOS。版本协商从vars[version]取客户端请求的版本为空时用defaultAPIVersion兜底。版本范围检查低于minAPIVersion或高于defaultAPIVersion都返回versionUnsupportedError带InvalidParameter()标记 → 400。把版本塞进 ctxctx context.WithValue(ctx, APIVersionKey{}, apiVersion)业务 handler 通过httputils.VersionFromContext(ctx)读取按版本走不同分支典型场景API 1.44 之前隐藏某些字段。构造函数会做完整性校验NewVersionMiddleware(serverVersion, defaultAPIVersion, minAPIVersion) // 要求minAPIVersion ≤ defaultAPIVersion // 且都在 [config.MinAPIVersion, config.MaxAPIVersion] 区间内4.3.2ExperimentalMiddlewaremiddleware/experimental.go只做一件事在每个响应上加Docker-Experimental: true|false头。客户端可以据此判断当前 daemon 是否开启了实验特性。4.3.3DebugRequestMiddlewaremiddleware/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// router/experimental.go func Experimental(r Route) Route把任意 route 标记为实验性。被标记后默认走experimentalHandler直接返回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 注入 contextua : 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标记triggerapi便于 telemetry 区分 API 触发的事件和其他触发源。5.2 串联中间件并执行handlerFunc : s.handlerWithGlobalMiddlewares(handler) vars : mux.Vars(r) if err : handlerFunc(ctx, w, r, vars); err ! nil { ... }5.3 客户端断连分支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 错误转换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)→ 404errdefs.InvalidParameter(err)→ 400gRPC 错误和 distribution registry 错误也有专门分支。API 1.24 走纯文本Docker 已经不再支持 1.24 以下但万一有极老客户端连进来给它返回 JSON 会显示成一坨原始 JSON 文本体验更差。注释里写Lets 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步骤组件动作1otelhttp创建 spanGET /volumes2mux路由匹配命中/v{version:[0-9.]}/volumes→volumeRouter.getVolumesList3makeHTTPHandler注入 ctxUA、baggage4VersionMiddleware校验 1.42 ∈ [min, default]写Server/Api-Version/Ostype响应头把1.42塞进 ctx5ExperimentalMiddleware写Docker-Experimental: false或 true6DebugRequestMiddleware仅 debug 级别打印请求字段7getVolumesList解析 form、调用backend.List、按版本决定是否查 swarm 集群卷、合并、WriteJSON8返回span 收尾状态码 200如果是请求/volumes无版本前缀步骤 4 中vars[version] 自动落到defaultAPIVersion其余流程一致。7. 时序图注册阶段daemon.NewDaemon │ ├─ 创建各个 backendcontainer/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()里追加一行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 的兼容性硬约束。9. 文件索引文件作用daemon/server/server.go入口Server、CreateMux、makeHTTPHandlerdaemon/server/middleware.gohandlerWithGlobalMiddlewares中间件链装配daemon/server/middleware/middleware.goMiddleware接口daemon/server/middleware/version.go版本协商 响应头注入daemon/server/middleware/experimental.goDocker-Experimental响应头daemon/server/middleware/debug.godebug 日志中间件 敏感字段屏蔽daemon/server/router/router.goRouter/Route接口daemon/server/router/local.golocalRouteNewGetRoute等便捷构造 WithMinimumAPIVersiondaemon/server/router/experimental.goExperimental()路由装饰器501 切换daemon/server/httputils/httputils.goAPIFunc类型、APIVersionKey、JSON 读写工具daemon/server/httpstatus/status.goFromErrorerror → HTTP status 映射daemon/server/router/{container,image,volume,system,network,swarm,...}各资源域具体路由