bigywater.tech
Docker · 81

Docker Engine API:用 curl 直接对 unix socket 发请求

docker 命令背后到底发了什么?Engine API 是什么样的?这篇不用 docker 命令,直接用 Python 对 Docker 的 unix socket 发 HTTP,完整地走一遍容器的生命周期,并用一个代理看 docker run 在背后发出的请求。

1.40 ~ 1.56
这个 Docker 支持的 API 版本范围,路径里写了超出范围的版本会得到 400
8 字节头
GET /containers/<id>/logs 返回的多路复用流,每一帧前的头(类型 + 长度)
6 个请求
docker run --rm alpine echo 在代理上被记录到的 HTTP 请求数

是 HTTP

实测 · 本机执行输出
  GET /_ping → 200;正文 'OK';响应头 Api-Version = 1.56,Docker-Experimental = false,Ostype = linux
  GET /version → 200;Version=29.8.2 ApiVersion=1.56 MinAPIVersion=1.40 Os=linux Arch=arm64 GoVersion=go1.26.8
  GET /info → 200;59 个顶层字段;NCPU=10 Containers=8 Driver=overlayfs CgroupVersion=2

Docker 守护进程监听一个 unix socket(Mac 上是 ~/.docker/run/docker.sock),说的就是普通 HTTP/1.1 加 JSON。/_ping 探测版本,/version 和 /info 返回守护进程信息。

生命周期

实测 · 本机执行输出
  POST /containers/create → 201;返回 Id = 7dda27b7871e…(共 64 个字符);Warnings = []
  POST /containers/<id>/start → 204(无正文:0 字节)
  POST /containers/<id>/wait → 200;{"StatusCode":0}
  GET /containers/<id>/logs → 200;Content-Type = application/vnd.docker.multiplexed-stream;正文 44 字节;前 16 字节(十六进制):01 00 00 00 00 00 00 0f 68 65 6c 6c 6f 2d 66 72
  按 8 字节帧头拆开:[('stdout', 'hello-from-api'), ('stderr', 'err-from-api')]
  GET /containers/<id>/json → 200;State.Status=exited ExitCode=0
  DELETE /containers/<id> → 204
  再 GET → 404;{'message': 'No such container: 7dda27b7871eed1966aee73a6dfcc1575b6fdd306bd27b4c23f5e3198a287764'}

创建(POST create,JSON 描述容器)、启动(POST start 返回 204)、等待、读日志、删除,都是单独的接口。日志接口返回多路复用流:每帧 8 字节头(类型 1 是 stdout、2 是 stderr,后 4 字节是大端长度)加数据,没有 TTY 时用这种格式区分两个流。

版本

实测 · 本机执行输出
  GET /v1.40/containers/json → 200;返回 8 个容器
  GET /v1.24/containers/json → 400;{"message":"client version 1.24 is too old. Minimum supported API version is 1.40, please upgrade your client 
  GET /v1.12/containers/json → 400;client version 1.12 is too old. Minimum supported API version is 1.40, please upgrade your client to a newer v
  GET /v1.54/containers/json → 200;返回 8 个容器
  GET /v9.99/containers/json → 400;{"message":"client version 9.99 is too new. Maximum supported API version is 1.56"}

API 版本写在路径前缀 /v1.xx/;服务端支持一个范围,超出就 400 并说明范围。不带版本前缀会怎样,我没有测。

docker CLI

实测 · 本机执行输出
  docker -H unix://<代理> run --rm alpine echo via-proxy:输出 'via-proxy'
  代理记录到 3 个连接、共 6 个请求(按连接分组列出,不严格按时间):
    HEAD /_ping HTTP/1.1
    HEAD /_ping HTTP/1.1
    POST /v1.56/containers/create HTTP/1.1
    POST /v1.56/containers/<64位ID>/wait?condition=removed HTTP/1.1
    POST /v1.56/containers/<64位ID>/attach?stderr=1&stdout=1&stream=1 HTTP/1.1
    POST /v1.56/containers/<64位ID>/start HTTP/1.1

docker run 在 API 层面就是 create + attach + start,加上 wait?condition=removed 等待 --rm 的容器被删除;它也先用 HEAD /_ping 探测。按我的理解,能访问这个 socket 就等于能控制整个 Docker,所以不要随便挂载到容器里(本文没有演示)。

分步演示

Engine API
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Docker Desktop(Engine 29.8.2,容器跑在 Docker Desktop 自带的 Linux 虚拟机里,内核与宿主机的 macOS 不同);镜像为 linux/arm64;需要看虚拟机内部时用特权容器 + nsenter;计时用 time / date +%s%N,数字每次运行会有小幅抖动。API 请求用 Python 的 http.client 经 unix socket 发出;代理是一个 Python 线程,把 CLI 的连接转发到真 socket 并记录请求行;容器 ID 在输出中缩写;实验后已删除容器
    • 没有验证:TLS / TCP 方式的 API、认证与授权插件、流式接口(events、attach 的 hijack)的完整协议、把 socket 挂载进容器的风险、Windows 命名管道
    京ICP备2026067502号-1京公网安备11010502063487号