当前位置: 首页 > news >正文

Docker-API路由架构详解

一句话定位

daemon/server/router/<resource>/是 Docker daemon 对外暴露 HTTP API 的入口
每一条docker psdocker rundocker 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 命令

container/

/containers/*/exec/*/commit

容器生命周期、exec、文件拷贝

docker run/ps/stop/logs/exec/cp/commit

image/

/images/*

镜像 list/inspect/pull/push/tag/delete

docker images/pull/push/tag/rmi

network/

/networks/*

网络创建、连接、断开

docker network ls/create/rm/connect

volume/

/volumes/*

数据卷 CRUD

docker volume ls/create/rm

build/

/build/build/prune/build/cancel

镜像构建

docker build

system/

/_ping/info/version/events/system/df/auth

系统/守护进程信息

docker info/version/events/login

swarm/

/swarm/*/nodes/*/services/*/tasks/*/secrets/*/configs/*

Swarm 集群

docker swarm/node/service

session/

/session/*

buildkit 长连接会话(已废弃,向后兼容)

buildkit 内部

plugin/

/plugins/*

插件管理

docker plugin

distribution/

/distribution/*

镜像仓库元信息(manifest、blob)

docker manifest

checkpoint/

/containers/{id}/checkpoints/*

容器检查点(实验性)

docker checkpoint

grpc/

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

每个资源都有一个<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), // ... } }

几个要点:

  1. router.NewXxxRoute(method, path, handler)是工厂方法,XXX = Get/Post/Put/Delete/Head/Options。
  1. 路径用 gorilla/mux 语法{name:.*}是路径参数,.*是允许的字符正则。匹配后vars["name"]可以取出值。
  1. router.WithMinimumAPIVersion("1.25")是路由级装饰器:API 版本不够的客户端访问会被拒绝(见第 4 节)。
  1. 路由表就是一张静态查找表,没注册的 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.Serverdaemon/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 请求:

  1. POST /v1.42/containers/createbody=容器配置 → 拿到容器 ID
  1. POST /v1.42/containers/{id}/start→ 启动

Step 2:路由匹配

两个 URL 都在container/container.go:67的路由表里:

  • /containers/createc.postContainersCreate
  • /containers/{name:.*}/startc.postContainersStart

Step 3:handler 解析(container/container_routes.go

postContainersCreate大致流程:

  1. httputils.ReadJSON(r, &body)解析 JSON
  1. runconfig.DecodeContainerConfig把 JSON 转成container.Config+hostconfig.HostConfig
  1. 校验:不能同时detach=truestdin_open=true
  1. c.backend.ContainerCreate(ctx, config)→ 走到daemon.create.go::ContainerCreate
  1. 返回{"Id": "...", "Warnings": [...]}

postContainersStart流程:

  1. vars["name"]取容器名/ID
  1. c.backend.ContainerStart(ctx, name, checkpoint, checkpointDir)
  1. 成功返回 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/containers/json

getContainersJSON

docker ps

GET/containers/{name}/json

getContainersByName

docker inspect

GET/containers/{name}/logs

getContainersLogs

docker logs

GET/containers/{name}/stats

getContainersStats

docker stats

GET/containers/{name}/top

getContainersTop

docker top

GET/containers/{name}/changes

getContainersChanges

docker diff

GET/containers/{name}/export

getContainersExport

docker export

GET/containers/{name}/archive

getContainersArchive

docker cp(下载)

POST/containers/create

postContainersCreate

docker create

POST/containers/{name}/start

postContainersStart

docker start

POST/containers/{name}/stop

postContainersStop

docker stop

POST/containers/{name}/kill

postContainersKill

docker kill

POST/containers/{name}/restart

postContainersRestart

docker restart

POST/containers/{name}/pause

postContainersPause

docker pause

POST/containers/{name}/wait

postContainersWait

docker wait

POST/containers/{name}/rename

postContainerRename

docker rename

POST/containers/{name}/update

postContainerUpdate

docker update

POST/containers/{name}/exec

postContainerExecCreate

docker exec(创建)

POST/exec/{id}/start

postContainerExecStart

docker exec(启动)

POST/containers/{name}/attach

postContainersAttach

docker attach

POST/containers/prune

postContainersPrune(v1.25+)

docker container prune

POST/commit

postCommit

docker commit

PUT/containers/{name}/archive

putContainersArchive

docker cp(上传)

DELETE/containers/{name}

deleteContainers

docker rm

image(image/image.go

端点

docker 命令

GET/images/json

docker images

GET/images/{name}/json

docker inspect <image>

GET/images/{name}/history

docker history

GET/images/search

docker search

GET/images/get/images/{name}/get

docker save

POST/images/create

docker pull

POST/images/load

docker load

POST/images/{name}/push

docker push

POST/images/{name}/tag

docker tag

POST/images/prune(v1.25+)

docker image prune

DELETE/images/{name}

docker rmi

network(network/network.go

端点

docker 命令

GET/networks

docker network ls

GET/networks/{id}

docker network inspect

POST/networks/create

docker network create

POST/networks/{id}/connect

docker network connect

POST/networks/{id}/disconnect

docker network disconnect

POST/networks/prune(v1.25+)

docker network prune

DELETE/networks/{id}

docker network rm

volume(volume/volume.go

端点

docker 命令

GET/volumes

docker volume ls

GET/volumes/{name}

docker volume inspect

POST/volumes/create

docker volume create

POST/volumes/prune(v1.25+)

docker volume prune

PUT/volumes/{name}(v1.42+)

docker volume update

DELETE/volumes/{name}

docker volume rm

system(system/system.go

端点

docker 命令

GET/_ping、HEAD/_ping

健康检查(CLI 启动时探测)

GET/info

docker info

GET/version

docker version

GET/events

docker events

GET/system/df

docker system df

POST/auth

docker login

OPTIONS/{anyroute:.*}

CORS 预检

build(build/build.go

端点

docker 命令

POST/build

docker build

POST/build/prune(v1.31+)

docker builder prune

POST/build/cancel

取消进行中的 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
  • wsContainersAttachgolang.org/x/net/websocket升级连接
  • postContainersAttach直接 hijack TCP 连接

这部分属于进阶话题,新手先理解同步 JSON 端点即可。

Q6:为什么有些资源有两个 backend(Backend+ClusterBackend)?

volumenetworksystemswarm:单机功能走Backend
Swarm 集群相关功能走ClusterBackend。在非 Swarm 节点上ClusterBackend是 nil,
handler 内部判 nil 决定走单机还是集群分支。

http://www.jsqmd.com/news/1151339/

相关文章:

  • LangChain ModelCallLimitMiddleware与ToolCallLimitMiddleware:限制模型调用与防止Agent工具无限调用
  • SerpBase vs ScraperAPI:SERP 专家和通用爬虫的差别
  • 小卖部老板的纸条
  • 护照阅读器在景区内配套服务:从入住到游玩的体验升级 对于包含住宿的大型度假区或文旅综合体,护照阅读器也让酒店入住等环节变得高效。
  • AI 改了代码,你怎么验收?用 Git Diff 生成一份变更审查清单
  • 免费数字人平台推荐:哪些是真免费,哪些后续最贵
  • 3分钟搞定Mac微信防撤回:WeChatIntercept终极配置指南
  • 法国AI初创公司ZML发布免费推理软件,支持多种AI芯片
  • 百考通智能任务书贴合你的选题,拒绝空话套话
  • 03.01.01.ComfyUI Ubuntu:环境搭建篇(集成 调用ComfyUI的API 使用Pythin 中间件方式)
  • VMamba VSSM 架构解析:4阶段VSS Block堆叠与SS2D模块的3步实现
  • 通过Semi Design思考前端组件AI化
  • 地月引力地震理论应用——四川高风险年份筛选
  • 测试转大模型:从排查路径看工程价值
  • DC SUPERMAN 超人美国商标维权全解析|S 盾图形 TRO 禁令应对教程
  • 一、三步搞定:在Creo/ProE任意位置自定义坐标系并导出
  • SolidWorks_装配体设计19_装配体动画与运动
  • Windows命令调用的神经反射弧:Run-Command智能启动器
  • 【单片机毕业设计】基于 51/STM32 单片机的温度监测声光报警系统设计与实现,基于 51/STM32 单片机的数码管温度采集报警装置开发(022801)
  • 杰理之旋转编码器【篇】
  • 2026智能写作工具深度测评|教育学习、文案创作、论文写作首选工具盘点
  • AGENTS.md / CLAUDE.md 到底写什么?给 AI 编程工具一份仓库说明书
  • HCIP--BGP基础实验
  • Seaborn vs Matplotlib vs Plotly:3种热力图方案性能与效果深度对比
  • JDK 1.8 已经装好,JDK 17 安装
  • 微信公众号原生Servlet开发零基础实战教程(附公众号主体变更迁移公证书办理流程及完整后端运行代码)
  • VSP One:走向统一的数据基础设施
  • 记一次使用 cloudflared tunnel 内网穿透
  • PCL 无序点云的快速三角剖分
  • 【单片机毕业设计】基于 51/STM32 单片机的超声波测距声光报警系统设计,基于 STM32/51 单片机的近距离测距预警装置开发(022901)