一句话定位daemon/server/router/resource/是 Docker daemon 对外暴露 HTTP API 的入口。每一条docker ps、docker run、docker pull命令最终都会变成一个 HTTP 请求落到这里的某条路由上。dockerd 的对外接口不是 gRPC、不是自定义协议就是最朴素的 HTTP/JSON RESTful API。所有路由都按「资源」拆成独立目录每个目录里管自己的事。全景图一条 docker 命令的旅程docker CLI dockerdHTTP server daemon 业务层 ──────────── ───────────────────────── ────────────── docker run nginx │ │ HTTP POST /v1.42/containers/create bodyJSON ▼ ┌──────────────────┐ │ gorilla/mux 路由 │ ← 把 URL Method 匹配到 handler └──────────────────┘ │ │ 匹配到 container.NewRouter 注册的 /containers/create ▼ ┌──────────────────┐ │ Route 三元组 │ (methodPOST, path/containers/create, handlerpostContainersCreate) └──────────────────┘ │ ▼ ┌──────────────────────────────────┐ │ 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 命令container//containers/*、/exec/*、/commit容器生命周期、exec、文件拷贝docker run/ps/stop/logs/exec/cp/commitimage//images/*镜像 list/inspect/pull/push/tag/deletedocker images/pull/push/tag/rminetwork//networks/*网络创建、连接、断开docker network ls/create/rm/connectvolume//volumes/*数据卷 CRUDdocker volume ls/create/rmbuild//build、/build/prune、/build/cancel镜像构建docker buildsystem//_ping、/info、/version、/events、/system/df、/auth系统/守护进程信息docker info/version/events/loginswarm//swarm/*、/nodes/*、/services/*、/tasks/*、/secrets/*、/configs/*Swarm 集群docker swarm/node/servicesession//session/*buildkit 长连接会话已废弃向后兼容buildkit 内部plugin//plugins/*插件管理docker plugindistribution//distribution/*镜像仓库元信息manifest、blobdocker manifestcheckpoint//containers/{id}/checkpoints/*容器检查点实验性docker checkpointgrpc/gRPC 路由已废弃的 gRPC builder 接口无对外命令debug//debug/*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每个资源都有一个resourceRouter结构体持有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.gorouter不直接调用*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 handlerresource_routes.gohandler 统一签名来自httputils.APIFuncfunc (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→ 404errdefs.InvalidParameter→ 400errdefs.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客户端无法区分两种情况。统一用 400InvalidParameter客户端能正确分支处理。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:989func 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.Serverdaemon/server/server.goserver 把每条 route 注册到底层gorilla/mux再加版本前缀/v{version}/再叠上中间件链请求进来 → CORS 中间件 → Debug 中间件 → 实验性中间件 → 版本协商中间件注入版本号到 ctx → 鉴权中间件authorization plugin → gorilla/mux 路由匹配 → RouteWrapper 链如 WithMinimumAPIVersion → handler6. 实战跟着docker run nginx走一遍这是把所有知识点串起来的最佳练习。Step 1CLI 端docker run nginx→ 实际发两个HTTP 请求POST /v1.42/containers/createbody容器配置 → 拿到容器 IDPOST /v1.42/containers/{id}/start→ 启动Step 2路由匹配两个 URL 都在container/container.go:67的路由表里/containers/create→c.postContainersCreate/containers/{name:.*}/start→c.postContainersStartStep 3handler 解析container/container_routes.gopostContainersCreate大致流程httputils.ReadJSON(r, body)解析 JSON调runconfig.DecodeContainerConfig把 JSON 转成container.Confighostconfig.HostConfig校验不能同时detachtrue和stdin_opentrue调c.backend.ContainerCreate(ctx, config)→ 走到daemon.create.go::ContainerCreate返回{Id: ..., Warnings: [...]}postContainersStart流程vars[name]取容器名/ID调c.backend.ContainerStart(ctx, name, checkpoint, checkpointDir)成功返回 204Step 4daemon 业务层daemon.create.go/daemon.start.go才是真正干活的地方——读镜像、写容器元数据、建 namespace、调 containerd 等。这一层已经和 HTTP 无关。Step 5响应回写handler 把结果 JSON 序列化 →w.Write(...)→ docker CLI 收到 → 打印容器 ID 给用户。7. 各资源路由速查表下面是各资源的核心端点一览方便你拿到一条 docker 命令时立刻定位代码。containercontainer/container.go端点handlerdocker 命令GET/containers/jsongetContainersJSONdocker psGET/containers/{name}/jsongetContainersByNamedocker inspectGET/containers/{name}/logsgetContainersLogsdocker logsGET/containers/{name}/statsgetContainersStatsdocker statsGET/containers/{name}/topgetContainersTopdocker topGET/containers/{name}/changesgetContainersChangesdocker diffGET/containers/{name}/exportgetContainersExportdocker exportGET/containers/{name}/archivegetContainersArchivedocker cp下载POST/containers/createpostContainersCreatedocker createPOST/containers/{name}/startpostContainersStartdocker startPOST/containers/{name}/stoppostContainersStopdocker stopPOST/containers/{name}/killpostContainersKilldocker killPOST/containers/{name}/restartpostContainersRestartdocker restartPOST/containers/{name}/pausepostContainersPausedocker pausePOST/containers/{name}/waitpostContainersWaitdocker waitPOST/containers/{name}/renamepostContainerRenamedocker renamePOST/containers/{name}/updatepostContainerUpdatedocker updatePOST/containers/{name}/execpostContainerExecCreatedocker exec创建POST/exec/{id}/startpostContainerExecStartdocker exec启动POST/containers/{name}/attachpostContainersAttachdocker attachPOST/containers/prunepostContainersPrunev1.25docker container prunePOST/commitpostCommitdocker commitPUT/containers/{name}/archiveputContainersArchivedocker cp上传DELETE/containers/{name}deleteContainersdocker rmimageimage/image.go端点docker 命令GET/images/jsondocker imagesGET/images/{name}/jsondocker inspect imageGET/images/{name}/historydocker historyGET/images/searchdocker searchGET/images/get、/images/{name}/getdocker savePOST/images/createdocker pullPOST/images/loaddocker loadPOST/images/{name}/pushdocker pushPOST/images/{name}/tagdocker tagPOST/images/prunev1.25docker image pruneDELETE/images/{name}docker rminetworknetwork/network.go端点docker 命令GET/networksdocker network lsGET/networks/{id}docker network inspectPOST/networks/createdocker network createPOST/networks/{id}/connectdocker network connectPOST/networks/{id}/disconnectdocker network disconnectPOST/networks/prunev1.25docker network pruneDELETE/networks/{id}docker network rmvolumevolume/volume.go端点docker 命令GET/volumesdocker volume lsGET/volumes/{name}docker volume inspectPOST/volumes/createdocker volume createPOST/volumes/prunev1.25docker volume prunePUT/volumes/{name}v1.42docker volume updateDELETE/volumes/{name}docker volume rmsystemsystem/system.go端点docker 命令GET/_ping、HEAD/_ping健康检查CLI 启动时探测GET/infodocker infoGET/versiondocker versionGET/eventsdocker eventsGET/system/dfdocker system dfPOST/authdocker loginOPTIONS/{anyroute:.*}CORS 预检buildbuild/build.go端点docker 命令POST/builddocker buildPOST/build/prunev1.31docker builder prunePOST/build/cancel取消进行中的 build8. 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抢走。Q2handler 报错怎么变成 HTTP 状态码看daemon/server/httpstatus/它根据 error 实现的接口NotFound()、InvalidParameter()、Conflict()等映射到 404/400/409。错误类型定义在errdefs包。Q3URL 前缀的/v1.42/是谁加的不是各资源 router 自己加是 server 层用 subrouter 自动拼上/v{version}/。版本号由VersionMiddleware解析客户端请求头/URL 后注入 ctxhandler 通过httputils.VersionFromContext(ctx)取。Q4router 之间会复用 backend 吗会。比如container.NewRouter(opts.daemon)、image.NewRouter(opts.daemon, ...)、network.NewRouter(opts.daemon, ...)传的都是同一个*daemon.Daemon只是各自从 Backend 接口里挑自己需要的方法。Q5handler 里的 streaminglogs、attach、events怎么实现通过 hijack HTTP 连接 / chunked transfer / websocket。比如getContainersLogs把 backend 返回的-chan *LogMessage逐条写到 ResponseWriterwsContainersAttach用golang.org/x/net/websocket升级连接postContainersAttach直接 hijack TCP 连接这部分属于进阶话题新手先理解同步 JSON 端点即可。Q6为什么有些资源有两个 backendBackendClusterBackend如volume、network、system、swarm单机功能走BackendSwarm 集群相关功能走ClusterBackend。在非 Swarm 节点上ClusterBackend是 nilhandler 内部判 nil 决定走单机还是集群分支。