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 规范为基础。
需要先区分三类配置:
- Daemon 配置:决定镜像、容器、网络、存储和 API 服务端如何工作。
- CLI 或 Context 配置:决定客户端连接哪个 Daemon,以及客户端如何验证服务端。
- 容器或构建配置:决定容器进程和构建步骤如何使用代理、日志等能力。
例如:
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.json 和 dockerd --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 不再使用它们。
正确迁移的基本步骤是:
- 停止 Docker,避免迁移期间数据继续变化。
- 复制整个旧数据根。
- 保留权限、硬链接、稀疏文件和扩展属性。
- 切换
data-root。 - 启动并验证对象数量、挂载点和业务。
- 观察一段时间后再删除旧目录。
示例:
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 启动失败。它不一定包含容器应用输出。
容器的 stdout 和 stderr 是否被 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-file 和 local 的取舍
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 在本机进行高效轮转的场景。若日志需要集中检索,则可以使用 syslog、journald、fluentd、gelf 或其他驱动,但必须确认目标日志系统可用。远程日志驱动发生网络阻塞时,可能影响应用写日志甚至容器行为;可靠性和可观测性之间需要结合驱动实现评估。
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 场景中至少有两种不同用途:
- Docker API TLS:保护 Docker CLI 到远程
dockerd的连接。 - 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 daemon、x509 |
| 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 反而可能说明:
- DNS 和 TCP 连接成功;
- TLS 握手成功;
- Registry API 已响应;
- 只是当前请求没有认证。
如果直接出现证书错误,则是 TLS 信任链问题;如果连接超时,则优先检查网络、代理和防火墙。
5. 验证远程 Daemon 的环境隔离
docker context ls
docker context show
docker info --format 'Name={{.Name}} Root={{.DockerRootDir}}'
在远程 Context 下执行:
docker ps
docker image ls
看到的是远程主机的容器和镜像,而不是本机对象。可以通过 docker context show 和 docker 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. 存储驱动不是普通配置开关
overlay2、btrfs、zfs 等存储驱动管理的数据结构不同。直接把一个驱动的数据目录复制给另一个驱动,不能视为可移植迁移。改变 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 完整学习路线:从镜像与容器到安全、可观测和生产交付
- 上一篇:Docker Swarm 基础与边界:Service、Task、Overlay、Secret 和滚动更新
- 下一篇:Docker User Namespace Remap:UID 映射、Volume、权限和迁移
- 延伸:Docker Context 与远程 Daemon:SSH、TLS、权限和环境隔离
- 延伸:Docker Engine 升级:兼容、存储驱动、API、灰度、回滚和验证
官方资料
本文依据 Docker、OCI 与 CNCF 官方文档重新梳理;正文与生产检查清单由 WR BLOG 编写。

评论
0 条讨论