Docker 基础体系 · 第 61/70 篇。示例以现代 Docker Engine、BuildKit 与 Compose 规范为基础,并明确 Linux 容器边界。

Docker Daemon 配置:data-root、日志、代理、镜像源、TLS 和验证

Docker CLI 只是客户端,真正创建容器、管理镜像、挂载存储和监听 API 的进程是 Docker Daemon,通常称为 dockerd。本文讨论的是 Linux 容器场景下的 Docker Engine Daemon 配置,示例以现代 Docker Engine、BuildKit 和 Compose 规范为基础。

需要先区分三类配置:

  1. Daemon 配置:决定镜像、容器、网络、存储和 API 服务端如何工作。
  2. CLI 或 Context 配置:决定客户端连接哪个 Daemon,以及客户端如何验证服务端。
  3. 容器或构建配置:决定容器进程和构建步骤如何使用代理、日志等能力。

例如:

docker run
   │
   ▼
Docker CLI ── Docker API ──> dockerd ──> containerd / runc
   │                            │
   │                            ├── data-root
   │                            ├── 镜像仓库
   │                            ├── 容器日志
   │                            └── 容器网络与生命周期
   │
   └── Context、DOCKER_HOST、DOCKER_TLS_VERIFY

因此,修改 daemon.json 不等于修改 Docker CLI 配置;给容器设置环境变量,也不等于给 Daemon 设置代理。


一、Daemon 配置文件、启动参数和生效规则

1. Linux 上的主要配置位置

在常见的 rootful Docker Engine 安装中:

/etc/docker/daemon.json

Docker Daemon 通常由 systemd 管理:

systemctl status docker
systemctl cat docker

配置来源可能包括:

  • /etc/docker/daemon.json
  • systemd unit 中的 ExecStart
  • systemd drop-in 文件,例如 /etc/systemd/system/docker.service.d/*.conf
  • 启动参数,例如 dockerd --data-root /srv/docker

需要注意:同一个配置项同时出现在配置文件和命令行参数中时,Daemon 可能拒绝启动,而不是简单地“命令行覆盖配置文件”。例如,data-root 同时在 daemon.jsondockerd --data-root 中配置,常见结果是启动失败并提示重复配置。

修改前先查看真实启动命令:

systemctl cat docker
ps -ef | grep '[d]ockerd'

配置文件可以使用 JSON 校验:

jq empty /etc/docker/daemon.json

jq empty 没有输出且退出码为 0,表示 JSON 语法有效;但这只能证明 JSON 格式正确,不能证明 Docker 支持其中的字段或字段值。

2. 一个可读的基础配置

下面的配置展示了多个常用 Daemon 选项:

{
  "data-root": "/var/lib/docker",
  "log-driver": "local",
  "log-opts": {
    "max-size": "20m",
    "max-file": "5",
    "compress": "true"
  },
  "live-restore": true,
  "features": {
    "buildkit": true
  }
}

这些配置的含义分别是:

  • data-root:Docker 管理数据的根目录。
  • log-driver:新建容器默认使用的日志驱动。
  • log-opts:该日志驱动的默认选项。
  • live-restore:Daemon 重启时,尽可能让运行中的容器继续运行。
  • features.buildkit:使用 BuildKit 作为默认构建后端。现代 Docker Engine 通常已经默认启用 BuildKit,但显式配置仍需结合实际 Engine 版本确认。

修改后通常执行:

sudo systemctl daemon-reload
sudo systemctl restart docker

如果只修改 /etc/docker/daemon.json,通常不需要 daemon-reload;它主要用于 systemd unit 或 drop-in 发生变化的情况。不过统一执行不会有害,关键风险在于 restart docker 会让 Daemon 短暂不可用。

重启前建议先做配置检查:

sudo dockerd --validate --config-file=/etc/docker/daemon.json

--validate 是否可用取决于 Docker Engine 版本;如果当前版本不支持该参数,应以实际版本的 dockerd --help 为准。无论是否有该参数,都应保留旧配置并通过服务日志验证:

sudo cp -a /etc/docker/daemon.json \
  "/etc/docker/daemon.json.$(date +%Y%m%d%H%M%S).bak"

sudo systemctl restart docker
sudo systemctl --no-pager --full status docker
sudo journalctl -u docker -b --no-pager -n 100

二、data-root:Docker 数据到底存在哪里

1. data-root 的定义

data-root 是 Docker Daemon 保存管理数据的根目录。默认通常是:

/var/lib/docker

它不是某个单独的容器目录,而是整个 Docker 数据树的根。典型内容可能包括:

/var/lib/docker/
├── containers/       # 容器元数据及部分日志
├── image/            # 镜像元数据
├── overlay2/         # overlay2 存储驱动数据
├── volumes/          # named volumes
├── network/          # 网络状态
└── buildkit/         # 与构建缓存相关的数据,具体结构随版本变化

这些目录是 Docker 的内部实现细节。不要直接手工删除或修改其中的文件来“清理镜像”,因为元数据与内容层之间存在引用关系,应使用 Docker API 或命令清理。

查看当前实际配置:

docker info --format '
DockerRootDir={{.DockerRootDir}}
StorageDriver={{.Driver}}
LoggingDriver={{.LoggingDriver}}
'

示例输出:

DockerRootDir=/var/lib/docker
StorageDriver=overlay2
LoggingDriver=json-file

这里的 DockerRootDir 是运行中 Daemon 报告的结果,比只查看配置文件更可靠,因为它反映了实际启动参数和最终状态。

2. 为什么修改 data-root 不是简单改路径

Docker 数据由多层内容组成:

镜像层
  └── 可复用的只读内容层
容器可写层
  └── 每个容器独立的写时复制层
卷数据
  └── named volume 的持久化内容
网络和元数据

如果只把空目录配置为新的 data-root,Docker 会看到一个新的数据根:

旧 data-root:有镜像、容器、卷
新 data-root:空

结果不是“自动迁移”,而是新目录中没有原来的对象。旧容器仍然存在于旧目录的数据结构中,但当前 Daemon 不再使用它们。

正确迁移的基本步骤是:

  1. 停止 Docker,避免迁移期间数据继续变化。
  2. 复制整个旧数据根。
  3. 保留权限、硬链接、稀疏文件和扩展属性。
  4. 切换 data-root
  5. 启动并验证对象数量、挂载点和业务。
  6. 观察一段时间后再删除旧目录。

示例:

sudo systemctl stop docker

sudo mkdir -p /srv/docker
sudo rsync -aHAX --numeric-ids --info=progress2 \
  /var/lib/docker/ /srv/docker/

sudo cp -a /etc/docker/daemon.json \
  /etc/docker/daemon.json.before-data-root

编辑 /etc/docker/daemon.json

{
  "data-root": "/srv/docker"
}

然后启动:

sudo systemctl start docker
docker info --format 'DockerRootDir={{.DockerRootDir}}'
docker ps -a
docker image ls
docker volume ls

这里的 rsync -aHAX 不是装饰项:

  • -a 保留常见的权限、时间和目录结构;
  • -H 保留硬链接;
  • -A 保留 ACL;
  • -X 保留扩展属性;
  • --numeric-ids 避免不同机器上的 UID/GID 名称映射造成误解。

实际迁移前应确认目标文件系统支持 Docker 所需的存储驱动。对 overlay2 而言,内核、文件系统和挂载选项必须满足当前 Docker Engine 的要求。把数据目录放在不适合的网络文件系统上,可能导致启动失败、性能异常或存储一致性问题;“能挂载”不等于“适合作为 Docker 存储根”。

3. data-root 与卷、绑定挂载的边界

data-root 包含 Docker 管理的 named volume,但不包含绑定挂载源目录本身。

docker run -d \
  --name app \
  -v app-data:/var/lib/app \
  image:tag

app-data 通常位于:

<data-root>/volumes/app-data/_data

而:

docker run -d \
  --name app \
  -v /srv/app-data:/var/lib/app \
  image:tag

数据实际在宿主机的 /srv/app-data,迁移 data-root 不会迁移它。

这产生一个常见反例:

DockerRootDir 已迁移到 /srv/docker
named volume 已随之迁移
但 /srv/app-data 没有迁移或没有挂载

容器可能能够启动,却读到空目录或错误版本的数据。迁移验证不能只执行 docker ps,还必须验证应用实际读取的数据和卷挂载:

docker inspect app \
  --format '{{json .Mounts}}' | jq

三、容器日志:Daemon 日志和应用日志不是一回事

1. 两种日志通道

Docker 环境中至少有两类日志:

dockerd 自身日志
  └── systemd journal、syslog 或其他服务管理器收集

容器标准输出/标准错误
  └── Docker logging driver 转发或保存

journalctl -u docker 查看的是 Daemon 自身日志,例如镜像拉取失败、存储驱动错误和 API 启动失败。它不一定包含容器应用输出。

容器的 stdoutstderr 是否被 Docker 收集,以及保存到哪里,由容器的 logging driver 决定。

2. 默认日志驱动和已有容器

可以配置新建容器的默认日志驱动:

{
  "log-driver": "local",
  "log-opts": {
    "max-size": "20m",
    "max-file": "5",
    "compress": "true"
  }
}

local 驱动是 Docker 提供的本地日志驱动,支持滚动和压缩,通常比无限增长的 json-file 更适合单机日志保存。json-file 的格式便于 Docker 和一些工具读取,但如果不设置轮转,日志可能持续占满磁盘。

关键边界是:修改 Daemon 的默认日志配置通常只影响之后创建的容器,不会自动重写已有容器的日志配置。

查看某个容器实际使用的驱动:

docker inspect app \
  --format '{{json .HostConfig.LogConfig}}' | jq

可能得到:

{
  "Type": "json-file",
  "Config": {
    "max-file": "3",
    "max-size": "10m"
  }
}

因此,修改默认值后,已有容器通常需要重新创建:

docker compose up -d --force-recreate

这不是普通的 docker restart 能解决的,因为日志驱动属于容器创建时的 HostConfig。

3. json-filelocal 的取舍

json-file 的日志文件通常位于类似:

<data-root>/containers/<container-id>/<container-id>-json.log

不要直接编辑或截断这些内部文件。Docker 需要维护其格式和状态,错误操作可能造成 docker logs 解析失败。

配置轮转:

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "5"
  }
}

这里的近似上限是:

单容器日志容量 ≈ max-size × max-file

它不是整个主机的日志容量上限,因为:

  • 每个容器分别计算;
  • 某些驱动存在额外索引或元数据;
  • 应用可能绕过 stdout 写入绑定挂载目录;
  • journald 或远程日志系统还有自己的保留策略。

local 驱动适合希望由 Docker 在本机进行高效轮转的场景。若日志需要集中检索,则可以使用 syslogjournaldfluentdgelf 或其他驱动,但必须确认目标日志系统可用。远程日志驱动发生网络阻塞时,可能影响应用写日志甚至容器行为;可靠性和可观测性之间需要结合驱动实现评估。

4. 日志驱动改变了 docker logs 的边界

docker logs 依赖所选日志驱动是否支持读取。并非所有远程驱动都能完整支持本地回读。

例如,应用配置为直接写文件:

/var/log/app/app.log

那么 docker logs app 可能没有内容,即使应用运行正常。反过来,应用将大量内容写到 stdout,也不代表日志一定永久保存,保存周期由本地轮转或远程系统策略决定。

生产验证应同时检查:

docker logs --tail 20 app
docker inspect app --format '{{.LogPath}}'
df -h
df -i

LogPath 只对某些本地日志驱动有意义;远程驱动可能为空或不提供可直接访问的本地日志文件。


四、代理:Daemon、BuildKit、容器和 CLI 是四个不同位置

1. Docker Daemon 什么时候需要代理

Daemon 可能需要访问:

  • 拉取基础镜像;
  • 推送镜像;
  • 访问 Docker Hub 或其他 Registry;
  • 某些插件或外部服务。

给宿主机 shell 设置:

export HTTPS_PROXY=http://proxy.example.com:3128

并不保证 systemd 启动的 dockerd 能看到这个变量。因为 systemd 服务通常不继承交互式 shell 的环境。

一种常见的 systemd 配置方式:

sudo mkdir -p /etc/systemd/system/docker.service.d
sudo tee /etc/systemd/system/docker.service.d/proxy.conf >/dev/null <<'EOF'
[Service]
Environment="HTTP_PROXY=http://proxy.example.com:3128"
Environment="HTTPS_PROXY=http://proxy.example.com:3128"
Environment="NO_PROXY=localhost,127.0.0.1,::1,registry.internal.example.com,.internal.example.com"
EOF

应用配置:

sudo systemctl daemon-reload
sudo systemctl restart docker
sudo systemctl show --property=Environment docker

现代 Docker Engine 也支持在 Daemon 配置中声明代理,例如:

{
  "proxies": {
    "http-proxy": "http://proxy.example.com:3128",
    "https-proxy": "http://proxy.example.com:3128",
    "no-proxy": "localhost,127.0.0.1,::1,.internal.example.com"
  }
}

具体字段和版本应以当前 Engine 文档为准。不要同时在 daemon.json 和 systemd drop-in 中设置同一代理变量,否则排障时很难判断最终来源。systemd 服务实际环境可通过:

systemctl show --property=Environment docker

确认。

2. NO_PROXY 的语义和常见错误

NO_PROXY 表示哪些目标不经过代理。它不是“内部网络自动识别”,而是字符串匹配规则,具体行为会受到客户端和 Go 运行时实现影响。

常见值包括:

localhost,127.0.0.1,::1,registry.internal.example.com,.corp.example.com

常见错误有:

NO_PROXY=registry.internal.example.com:5000

某些客户端要求主机名和端口匹配方式不同;因此配置私有 Registry 时应根据实际客户端验证,而不能只凭猜测。

若内部 Registry 被错误地送进外部代理,可能出现:

proxyconnect tcp: ...
x509: certificate signed by unknown authority
connection reset by peer

代理绕过和 TLS 信任是两个独立问题:即使请求正确绕过代理,Daemon 仍需要信任 Registry 的证书。

3. 构建代理不等于 Daemon 代理

构建过程包含多个网络角色:

Docker CLI
   │ 发送构建请求
   ▼
dockerd / BuildKit
   │ 拉取 Dockerfile 中的 FROM 镜像
   │ 执行 RUN 中的网络访问
   ▼
构建步骤容器

Daemon 的代理主要影响 Daemon 或构建后端访问 Registry 等操作;而 Dockerfile 中的:

RUN apt-get update
RUN npm install

是在构建步骤环境中执行的,需要向构建过程传递代理。现代 BuildKit 可使用构建参数:

docker build \
  --build-arg HTTP_PROXY=http://proxy.example.com:3128 \
  --build-arg HTTPS_PROXY=http://proxy.example.com:3128 \
  --build-arg NO_PROXY=localhost,127.0.0.1,.internal.example.com \
  -t example/app:build .

Dockerfile:

FROM debian:bookworm

ARG HTTP_PROXY
ARG HTTPS_PROXY
ARG NO_PROXY

RUN apt-get update \
 && apt-get install -y --no-install-recommends curl \
 && rm -rf /var/lib/apt/lists/*

BuildKit 对预定义代理参数有特殊处理,通常不会把它们作为普通构建参数写入最终镜像配置,但应避免把带用户名和密码的代理 URL 直接写进 Dockerfile、镜像层或构建日志。若使用 RUN export HTTPS_PROXY=...,秘密很容易进入 shell 历史、构建缓存或错误输出。

容器运行时代理则是另一件事:

docker run -d \
  -e HTTP_PROXY=http://proxy.example.com:3128 \
  -e HTTPS_PROXY=http://proxy.example.com:3128 \
  -e NO_PROXY=localhost,127.0.0.1,.internal.example.com \
  example/app:latest

Compose 中也可以为服务设置环境变量,但这只影响容器进程,不会配置 Daemon:

services:
  app:
    image: example/app:latest
    environment:
      HTTP_PROXY: http://proxy.example.com:3128
      HTTPS_PROXY: http://proxy.example.com:3128
      NO_PROXY: localhost,127.0.0.1,.internal.example.com

4. 代理故障的验证路径

验证 Daemon 代理时,不要只在 shell 中执行 curl。应验证真实 Daemon:

docker pull alpine:3.20

同时观察:

sudo journalctl -u docker -f

验证构建代理:

DOCKER_BUILDKIT=1 docker build \
  --progress=plain \
  --build-arg HTTPS_PROXY=http://proxy.example.com:3128 \
  -t proxy-test .

Dockerfile 中可以临时打印网络测试结果,但不要打印包含凭据的完整代理 URL。验证容器代理则进入容器检查:

docker exec app sh -c 'env | grep -i proxy'

五、镜像源:Registry、镜像加速器和私有仓库不是同一个概念

1. 镜像名称如何决定目标仓库

下面三个镜像引用的目标不同:

alpine:3.20
docker.io/library/alpine:3.20
registry.example.com/team/app:1.0

通常:

  • alpine:3.20 默认解释为 Docker Hub 的 library/alpine
  • docker.io/library/alpine:3.20 明确指定 Docker Hub;
  • registry.example.com/team/app:1.0 明确指定私有 Registry。

镜像源配置的作用必须放在这个解析规则上理解。它不是把任意域名的 Registry 全部透明改写成另一个地址。

2. registry-mirrors 的作用范围

一个典型配置:

{
  "registry-mirrors": [
    "https://mirror.example.com"
  ]
}

它用于为 Docker Hub 拉取提供镜像加速或缓存。镜像站必须实际实现兼容的 Registry 服务,常见方式是部署 Registry pull-through cache。仅仅部署一个 HTTP 反向代理,不一定满足 Registry API、认证和镜像清单处理要求。

配置后查看 Daemon 是否加载:

docker info

在输出中应看到类似:

Registry Mirrors:
  https://mirror.example.com/

验证:

docker image rm alpine:3.20
docker pull alpine:3.20

单凭 docker info 只能证明配置已被 Daemon 读取,不能证明镜像站可用。还要检查:

  • 镜像站是否可解析;
  • TLS 证书是否受信任;
  • 镜像站是否能访问上游;
  • 认证是否正确;
  • 上游限流或缓存是否返回错误清单。

3. 镜像源不等于私有 Registry 重写

下面的配置:

{
  "registry-mirrors": [
    "https://mirror.example.com"
  ]
}

通常不会把:

docker pull registry.internal.example.com/team/app:1.0

改写为:

mirror.example.com/team/app:1.0

私有仓库仍应在镜像引用中写出完整地址。若组织需要统一镜像代理,应使用明确的 Registry 代理、仓库策略或构建系统重写,而不是假设 registry-mirrors 会匹配所有仓库。

4. 认证和镜像源的边界

docker login registry.internal.example.com
docker pull registry.internal.example.com/team/app:1.0

docker login 主要保存 CLI 使用 Registry 的凭据,默认位于用户的 Docker 配置目录,例如:

~/.docker/config.json

它不一定替 Daemon 配置凭据。Daemon 拉取镜像时涉及的认证流程由 Registry API 和客户端凭据机制共同决定。不要把 Registry 用户名和密码直接写进 daemon.json


六、TLS:加密传输、身份验证和 Registry 证书信任

TLS 在 Docker 场景中至少有两种不同用途:

  1. Docker API TLS:保护 Docker CLI 到远程 dockerd 的连接。
  2. Registry TLS:保护 Daemon 到镜像仓库的 HTTPS 连接。

两者都使用证书,但配置位置、信任关系和故障表现不同。


七、Docker API TLS:让远程 Daemon 验证客户端

1. 不启用 TLS 时的风险

本地 Unix socket:

unix:///var/run/docker.sock

通常依靠 Unix 文件权限控制访问。

远程 TCP API 如果监听:

tcp://0.0.0.0:2375

且没有 TLS,相当于在网络上暴露一个高权限远程控制接口。能够调用该 API 的用户通常可以创建特权容器、挂载宿主机根目录,从而取得宿主机控制权。

因此,生产环境不应把未加密的 Docker API 暴露在不可信网络中。2375 不是“开发方便端口”,它代表没有 TLS 的 API 监听约定。

2. TLS 的验证模型

完整的双向 TLS 通常包含:

客户端 ── 提供 client.crt / client.key ──> Daemon
       <── 提供 server.crt / server.key ─── Daemon
       └── 双方都验证对方证书链

Daemon 需要:

  • CA 证书;
  • 服务端证书;
  • 服务端私钥;
  • 启用客户端证书验证。

客户端需要:

  • CA 证书;
  • 客户端证书;
  • 客户端私钥。

Daemon 端的关键参数通常是:

--tlsverify
--tlscacert=/etc/docker/pki/ca.pem
--tlscert=/etc/docker/pki/server-cert.pem
--tlskey=/etc/docker/pki/server-key.pem
-H=tcp://0.0.0.0:2376

其中:

  • --tlsverify 要求验证客户端证书;
  • --tlscacert 指定信任的 CA;
  • --tlscert--tlskey 是服务端证书及私钥;
  • 2376 是启用 TLS 的常见约定端口,不是安全性的来源。

具体启动方式可以放在 systemd drop-in 中,但必须避免与发行版已有的 ExecStart 重复定义 -H 或 TLS 参数。修改前:

systemctl cat docker

修改后:

sudo systemctl daemon-reload
sudo systemctl restart docker
sudo journalctl -u docker -b --no-pager -n 100

3. 客户端如何连接

客户端可通过环境变量指定远程 TLS 连接:

export DOCKER_HOST=tcp://docker.example.com:2376
export DOCKER_TLS_VERIFY=1
export DOCKER_CERT_PATH="$HOME/.docker/docker.example.com"

该目录通常包含:

ca.pem
cert.pem
key.pem

验证:

docker version
docker info

成功时,docker version 会同时显示 Client 和 Server 版本信息。只显示 Client、随后提示连接失败,说明 CLI 配置已读取但无法完成 API 连接。

也可以使用 Docker Context 管理这些连接:

docker context create remote-prod \
  --docker "host=tcp://docker.example.com:2376,ca=$HOME/.docker/remote/ca.pem,cert=$HOME/.docker/remote/cert.pem,key=$HOME/.docker/remote/key.pem"

docker context use remote-prod
docker info

Context 解决的是“CLI 使用哪一组连接参数”的问题;它不会修改远程 Daemon 的 daemon.json,也不会把本地镜像、卷或容器自动带到远程主机。

4. 用 OpenSSL 检查证书是否匹配

远程服务端证书的主机名必须包含客户端连接时使用的主机名或 IP 的 SAN。检查:

openssl s_client \
  -connect docker.example.com:2376 \
  -CAfile ca.pem \
  -verify_return_error </dev/null

检查证书名称:

openssl x509 \
  -in server-cert.pem \
  -noout -subject -issuer -dates -ext subjectAltName

若证书只写了:

CN=docker.example.com

但没有相应的 subjectAltName,现代 TLS 客户端可能仍然拒绝它。常见错误包括:

x509: certificate is valid for docker.example.com, not 10.0.0.10

修复方式是使用证书中包含的 DNS 名称连接,或者重新签发包含正确 SAN 的证书;不能靠关闭验证解决生产问题。


八、Registry TLS:Daemon 如何信任私有仓库

1. 私有 CA 的正确安装方式

若内部 Registry 使用组织自建 CA 签发证书,Daemon 主机需要信任该 CA。Docker 常见的按 Registry 配置方式是:

/etc/docker/certs.d/registry.internal.example.com/ca.crt

如果 Registry 使用非默认端口,目录通常要包含端口:

/etc/docker/certs.d/registry.internal.example.com:5000/ca.crt

然后重启 Docker:

sudo systemctl restart docker
docker pull registry.internal.example.com:5000/team/app:1.0

目录名必须与镜像引用中的 Registry 主机和端口一致。以下两个目录不是同一个目标:

/etc/docker/certs.d/registry.internal.example.com/ca.crt
/etc/docker/certs.d/registry.internal.example.com:5000/ca.crt

证书文件应是 CA 证书,而不是随意复制服务端私钥。私钥应只存在于签发或服务端使用的位置。

2. insecure-registries 的含义

示例:

{
  "insecure-registries": [
    "registry.internal.example.com:5000"
  ]
}

这类配置通常允许 Daemon:

  • 使用 HTTP 访问指定 Registry;或者
  • 不严格验证该 Registry 的 HTTPS 证书。

它不是“信任一个自签名证书”的精确替代品,而是降低安全检查的兼容模式。风险包括:

  • 镜像内容可能被中间人替换;
  • 凭据可能通过不安全通道暴露;
  • 错误配置可能把不可信网络当成可信仓库。

若只是内部 CA 不受信任,应优先安装正确的 ca.crt,而不是使用 insecure-registries。该配置必须精确限定主机和端口,不能为了省事配置过宽的网段或通配范围。

验证当前 Daemon 是否加载:

docker info

查看:

Insecure Registries:
  registry.internal.example.com:5000

修改 insecure-registries 后需要重启 Daemon,因为它影响 Registry 客户端初始化和连接策略。


九、Docker Daemon TLS 与 Registry TLS 的区别

这两个故障经常被混为一谈。

场景 连接双方 证书配置位置 典型错误
Docker API TLS CLI ↔ dockerd CLI 的 Context/环境变量、Daemon 启动参数 Cannot connect to the Docker daemonx509
Registry TLS dockerd ↔ Registry /etc/docker/certs.d/...、系统 CA、Registry 配置 x509: certificate signed by unknown authority
容器访问 HTTPS 容器进程 ↔ 外部服务 镜像内 CA、容器环境 应用自己的 TLS 错误

例如:

docker -H tcp://docker.example.com:2376 info

失败,优先检查 Docker API TLS;而:

docker pull registry.internal.example.com/team/app:1.0

失败并出现:

x509: certificate signed by unknown authority

则更可能是 Daemon 到 Registry 的证书信任问题。给 CLI 设置 DOCKER_CERT_PATH 不会替 Daemon 信任 Registry 的 CA。


十、验证:从配置文件验证到真实业务路径

配置验证应按“语法—服务状态—运行状态—真实路径”的顺序进行。

1. 验证配置文件和启动状态

jq empty /etc/docker/daemon.json

sudo systemctl restart docker
sudo systemctl is-active docker
sudo journalctl -u docker -b --no-pager -n 200

若启动失败,先看日志,不要立即删除配置:

sudo systemctl status docker --no-pager --full
sudo journalctl -xeu docker

典型原因包括:

  • JSON 末尾多逗号;
  • 字段拼写错误;
  • 同一选项同时出现在 daemon.json 和启动参数中;
  • TLS 私钥权限或路径错误;
  • 端口已被占用;
  • data-root 不存在或不可写;
  • 存储驱动与目标文件系统不兼容。

2. 验证实际生效值

docker info
docker version
docker system df

也可以使用格式化输出:

docker info --format '
root={{.DockerRootDir}}
driver={{.Driver}}
logging={{.LoggingDriver}}
server={{.ServerVersion}}
'

配置文件存在并不证明 Daemon 使用了它。docker info 显示的运行时状态才是第一层证据。

3. 验证日志轮转

创建一个临时容器:

docker run -d \
  --name log-test \
  alpine:3.20 \
  sh -c 'i=0; while true; do echo "line-$i"; i=$((i+1)); done'

检查配置:

docker inspect log-test \
  --format '{{json .HostConfig.LogConfig}}' | jq

停止并清理:

docker rm -f log-test

这个测试只能验证容器实际拿到的日志配置;要验证轮转是否发生,还需要在测试环境产生超过 max-size 的日志,并观察日志文件或驱动行为。不要在生产机用无限循环制造日志。

4. 验证镜像源和 Registry

docker pull alpine:3.20
docker pull registry.internal.example.com:5000/team/app:1.0

查看仓库相关配置:

docker info | sed -n '/Registry Mirrors:/,/Live Restore Enabled:/p'

Registry TLS 的底层验证可以先用:

curl -v https://registry.internal.example.com:5000/v2/

返回 401 Unauthorized 反而可能说明:

  1. DNS 和 TCP 连接成功;
  2. TLS 握手成功;
  3. Registry API 已响应;
  4. 只是当前请求没有认证。

如果直接出现证书错误,则是 TLS 信任链问题;如果连接超时,则优先检查网络、代理和防火墙。

5. 验证远程 Daemon 的环境隔离

docker context ls
docker context show
docker info --format 'Name={{.Name}} Root={{.DockerRootDir}}'

在远程 Context 下执行:

docker ps
docker image ls

看到的是远程主机的容器和镜像,而不是本机对象。可以通过 docker context showdocker info 防止误删错误环境中的资源。


十一、配置变更的状态、并发和故障路径

Daemon 配置不是静态文本替换,而是影响多个长期状态的控制面:

配置文件或 systemd
        │
        ▼
dockerd 启动时解析
        │
        ├── 初始化 data-root 和存储驱动
        ├── 初始化日志驱动默认值
        ├── 初始化 Registry 客户端
        ├── 初始化代理环境
        └── 监听 Unix socket / TCP API
                │
                ▼
        CLI、Compose、BuildKit 调用 API

当重启 Daemon 时,客户端请求可能处于以下状态:

请求已发送但服务端未完成
请求等待存储或 Registry
Daemon 正在退出
Daemon 已启动但尚未恢复状态

因此,systemctl restart docker 不是无状态切换。live-restore 可以减少 Daemon 重启对运行中容器的影响,但它不能保证所有插件、网络操作、日志驱动和编排行为都完全无中断。配置涉及网络监听、存储驱动或日志驱动时,应按有维护窗口处理。

并发方面,多个 Compose 项目或自动化系统可能同时调用同一个 Daemon。修改日志默认值后,一个项目重建了容器,另一个项目仍运行旧容器,就会出现同一主机上日志策略不一致的状态。验证应按容器逐个检查,而不是只检查全局 LoggingDriver


十二、失败表现与诊断顺序

1. Daemon 无法启动

首先执行:

sudo systemctl status docker --no-pager --full
sudo journalctl -u docker -b --no-pager

然后检查:

jq empty /etc/docker/daemon.json
systemctl cat docker
ls -ld /srv/docker
ls -l /etc/docker/pki
ss -lntp | grep -E ':(2375|2376)\b'

诊断原则是先确认 Daemon 是否活着,再诊断 CLI、Registry 或容器。Daemon 未启动时,docker pull 的错误只是二次表现。

2. docker pull 失败

按顺序区分:

Cannot connect to the Docker daemon
  → CLI 到 Daemon 的连接、Context、socket 权限

proxyconnect / timeout
  → Daemon 代理、NO_PROXY、DNS、防火墙

x509: certificate signed by unknown authority
  → Registry CA 信任链

unauthorized
  → Registry 认证或凭据

manifest unknown
  → 镜像名称、仓库路径或 tag 不存在

no matching manifest
  → 平台架构与镜像清单不匹配

不要把所有 pull 错误都归因于镜像源。镜像源只处于请求路径中的一个环节。

3. 改了日志配置但没有变化

检查三个层次:

docker info --format '{{.LoggingDriver}}'
docker inspect CONTAINER --format '{{json .HostConfig.LogConfig}}' | jq
docker compose config

如果全局默认值已经是 local,但容器仍是 json-file,通常是旧容器没有重建,或者 Compose 服务显式设置了:

services:
  app:
    logging:
      driver: json-file

Compose 服务级配置优先于 Daemon 默认值。修改 Daemon 不能覆盖应用清单中明确指定的 logging 配置。


十三、升级、迁移和回滚时的取舍

1. data-root 迁移的回滚

在切换后不要立刻删除旧目录:

sudo mv /var/lib/docker /var/lib/docker.before-migration

更稳妥的方式是先保留旧目录,并在验证完成后再归档。若新路径启动失败:

sudo systemctl stop docker

恢复原来的 data-root 配置并启动:

sudo systemctl start docker
docker info

如果新旧路径同时被两个 Daemon 使用,可能造成严重数据损坏。因此切换期间必须保证同一数据根只有一个活跃的 Docker Daemon 使用者。

2. 存储驱动不是普通配置开关

overlay2btrfszfs 等存储驱动管理的数据结构不同。直接把一个驱动的数据目录复制给另一个驱动,不能视为可移植迁移。改变 storage-driver 可能使原有镜像和容器在新驱动下不可见,或者需要通过镜像导出、重新拉取和卷备份完成迁移。

不要把以下操作当作安全升级策略:

{
  "storage-driver": "overlay2"
}

然后指望已有数据自动转换。存储驱动变更应先确认兼容性、备份策略和回滚路径。

3. 版本升级后的验证

Docker Engine 升级后至少验证:

docker version
docker info
docker ps
docker image ls
docker volume ls
docker network ls
docker pull alpine:3.20
docker run --rm alpine:3.20 uname -a

如果使用 Compose 和 BuildKit,还要执行真实构建和部署路径:

docker compose config
docker compose build
docker compose up -d
docker compose ps

升级验证的重点不是“服务进程为 active”,而是确认:

  • API 可用;
  • 旧容器状态和挂载正确;
  • 镜像拉取和 Registry TLS 正常;
  • 日志仍可读取并轮转;
  • 构建缓存和代理路径符合预期;
  • Compose 使用的是目标 Daemon,而不是错误 Context。

十四、一个可审计的最小配置示例

下面是一个偏向单机生产环境的示例:

{
  "data-root": "/srv/docker",
  "log-driver": "local",
  "log-opts": {
    "max-size": "20m",
    "max-file": "5",
    "compress": "true"
  },
  "registry-mirrors": [
    "https://mirror.example.com"
  ],
  "proxies": {
    "http-proxy": "http://proxy.example.com:3128",
    "https-proxy": "http://proxy.example.com:3128",
    "no-proxy": "localhost,127.0.0.1,::1,registry.internal.example.com,.internal.example.com"
  },
  "live-restore": true
}

使用它之前仍需完成以下事实验证:

jq empty /etc/docker/daemon.json
test -d /srv/docker
curl -fsS https://mirror.example.com/v2/ || true
sudo systemctl restart docker
docker info
docker pull alpine:3.20

其中 curl ... || true 只是避免由于 Registry 返回 401 而中断脚本;生产脚本不应把所有错误都吞掉,而应根据 HTTP 状态码区分“TLS 和连接成功但需要认证”和“真实连接失败”。

若有私有 CA:

sudo install -d -m 0755 \
  /etc/docker/certs.d/registry.internal.example.com:5000

sudo install -m 0644 internal-registry-ca.crt \
  /etc/docker/certs.d/registry.internal.example.com:5000/ca.crt

sudo systemctl restart docker
docker pull registry.internal.example.com:5000/team/app:1.0

若启用远程 Docker API,则应额外配置 mTLS、限制监听地址和防火墙范围,并用专用客户端证书验证:

export DOCKER_HOST=tcp://docker.example.com:2376
export DOCKER_TLS_VERIFY=1
export DOCKER_CERT_PATH="$HOME/.docker/remote-prod"

docker context show
docker version
docker info

十五、核心边界

最后可以用以下关系检查配置是否放在了正确位置:

data-root
  → Daemon 管理的数据位置

log-driver / log-opts
  → 新建容器默认如何处理 stdout/stderr

proxies(Daemon)
  → dockerd 或构建后端访问外部服务时如何出网

HTTP_PROXY / HTTPS_PROXY(容器)
  → 容器内应用如何出网

registry-mirrors
  → 主要影响 Docker Hub 拉取的镜像缓存路径

/etc/docker/certs.d
  → Daemon 如何信任特定 Registry 的 TLS 证书

--tlsverify / DOCKER_TLS_VERIFY
  → CLI 与远程 Docker API 如何互相验证

Docker Context
  → CLI 当前把 API 请求发送到哪个 Daemon

配置变更的最小可靠闭环是:

备份配置
  → 校验语法
  → 检查 systemd 启动来源
  → 重启或 reload
  → 查看 Daemon 日志
  → 查看 docker info 实际状态
  → 执行 pull / build / run / logs 真实路径验证
  → 保留回滚数据直到业务确认

只验证配置文件内容,无法证明 Daemon 使用了它;只验证 Daemon 处于运行状态,无法证明 Registry、代理、日志和远程 TLS 正常。只有沿着真实数据流从 CLI 经过 Daemon,再到存储、日志、代理或 Registry,才能确认配置真正生效。


系列导航与关联阅读

官方资料

本文依据 Docker、OCI 与 CNCF 官方文档重新梳理;正文与生产检查清单由 WR BLOG 编写。