一句话定位

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/create body=容器配置 → 拿到容器 ID
  1. 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 大致流程:

  1. httputils.ReadJSON(r, &body) 解析 JSON
  1. 调 runconfig.DecodeContainerConfig 把 JSON 转成 container.Config + hostconfig.HostConfig
  1. 校验:不能同时 detach=true 和 stdin_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
  • wsContainersAttach 用 golang.org/x/net/websocket 升级连接
  • postContainersAttach 直接 hijack TCP 连接

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

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

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

更多推荐