Docker Engine:从 Linux daemon 安装到可升级、可审计的开发运行底座
一台共享 Linux 开发机重启后,所有容器都停了。值班同学执行 docker ps 得到权限错误,于是把整个研发组加入 docker 组;当天晚些时候,一段测试脚本挂载宿主机根目录并修改了系统文件。第二天又有人把 /var/lib/docker 搬到新磁盘,daemon 能启动,旧镜像却全部“消失”。
这些现象不是三个孤立故障。Docker Engine 是常驻于宿主机的高权限 daemon,Docker CLI 通过 Unix socket 调用它,daemon 再协调 containerd、runc、网络、防火墙、镜像存储、volume 和日志。安装包、systemd、socket 权限、daemon.json 与数据目录必须作为一套系统管理,不能只确认 docker run 能输出一句欢迎语。
先识别 Engine 的控制链与状态边界
CLI 成功不代表 Server 健康。安装或排障时保留下面四层证据:
docker version
docker info
systemctl is-enabled docker
systemctl is-active docker
sudo journalctl -u docker.service -n 100 --no-pagerdocker version 应同时包含 Client 与 Server;systemctl is-active 预期输出 active;docker info 应记录当前存储后端、数据根目录、日志驱动、cgroup 驱动和安全选项。若 CLI 版本正常而 Server 连接失败,问题位于 socket、daemon 或 systemd,不要先重装客户端。
从官方软件仓库安装,而不是执行不透明远程脚本
包管理器安装能保留仓库签名、软件包版本和升级路径。便利脚本适合一次性实验机,不适合作为团队长期基线,因为脚本内容会变化,也很难在变更前审查实际安装版本。Linux 发行版自带的 docker.io 与 Docker 官方仓库的 docker-ce 由不同维护方构建,团队必须选定一条来源,不能混装后再把版本冲突归因于 Docker。
Ubuntu 与 Debian
先读取发行版身份并盘点冲突包:
. /etc/os-release
printf 'id=%s version=%s codename=%s arch=%s\n' "$ID" "$VERSION_ID" "${VERSION_CODENAME:-unknown}" "$(dpkg --print-architecture)"
dpkg -l | grep -E 'docker|containerd|runc' || true下面的脚本只接受原生 Ubuntu 或 Debian,并根据发行版选择官方仓库。衍生发行版要先确认它映射到哪个受支持的上游版本,再按上游官方页面单独生成配置,不能直接把自己的 codename 填进去。
sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
case "$ID" in
ubuntu) repo_os=ubuntu; repo_suite="${UBUNTU_CODENAME:-$VERSION_CODENAME}" ;;
debian) repo_os=debian; repo_suite="$VERSION_CODENAME" ;;
*) printf 'unsupported distribution: %s\n' "$ID" >&2; exit 23 ;;
esac
sudo curl -fsSL "https://download.docker.com/linux/$repo_os/gpg" \
-o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
sudo tee /etc/apt/sources.list.d/docker.sources >/dev/null <<EOF
Types: deb
URIs: https://download.docker.com/linux/$repo_os
Suites: $repo_suite
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF
sudo apt update
apt list --all-versions docker-ce团队机器应从列表中选择已经过兼容验证的完整版本字符串,而不是每台机器各自安装当日最新版本:
VERSION_STRING='<reviewed-version-from-apt-list>'
case "$VERSION_STRING" in
''|*'<'*|*'>'*) printf 'set an exact reviewed version\n' >&2; exit 23 ;;
esac
sudo apt install -y \
docker-ce="$VERSION_STRING" \
docker-ce-cli="$VERSION_STRING" \
containerd.io docker-buildx-plugin docker-compose-plugincontainerd.io 是 Docker 仓库提供的依赖组合。宿主机原先独立维护 containerd 或 runc 时,先评估其他工作负载是否依赖它们,再按目标发行版说明处理冲突,不能直接卸载共享运行时。
上面的命令只固定了 Engine 与 CLI;containerd.io、Buildx 和 Compose 仍由当时仓库解析。团队镜像或离线基线还要记录 apt-cache policy containerd.io docker-buildx-plugin docker-compose-plugin 与安装事务,或者给全部包使用经过验证的精确版本,避免同一 Engine 版本在不同日期解析出不同插件组合。
Fedora、CentOS 与兼容 RPM 系统
先确认发行版和仓库:
. /etc/os-release
printf 'id=%s version=%s arch=%s\n' "$ID" "$VERSION_ID" "$(uname -m)"
sudo dnf repolistCentOS 安装路径示例:
sudo dnf -y install dnf-plugins-core
sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
dnf list docker-ce --showduplicates | sort -rFedora 使用对应的 Fedora 仓库 URL。安装时同样选择列表中的完整版本:
VERSION_STRING='<reviewed-version-from-dnf-list>'
case "$VERSION_STRING" in
''|*'<'*|*'>'*) printf 'set an exact reviewed version\n' >&2; exit 23 ;;
esac
sudo dnf install -y \
"docker-ce-$VERSION_STRING" \
"docker-ce-cli-$VERSION_STRING" \
containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker首次接受仓库 GPG key 时要由自动化或人工核对当前官方指纹,不能看到“Docker”名称就直接确认。镜像站、代理缓存和离线包也要保存上游 URL、包摘要、签名验证结果和同步时间。
安装后的第一条证据链
sudo systemctl status docker --no-pager
sudo docker version
sudo docker info --format 'server={{.ServerVersion}} root={{.DockerRootDir}} driver={{.Driver}} logging={{.LoggingDriver}} cgroup={{.CgroupDriver}}'
sudo docker run --rm hello-world欢迎输出只证明 daemon 能拉取并启动那个镜像。随后还要验证端口、写时复制、volume、日志和重启行为,才能确认运行底座可用于项目。
用 systemd 管理生命周期与启动失败
Docker Engine 的安装包通常提供 docker.service 与 docker.socket。开发机是否开机自启要由用途决定:个人实验机可按需启动;共享开发机与本地 CI 节点需要显式启用,并监控启动失败。
sudo systemctl enable --now docker
systemctl is-enabled docker
systemctl is-active docker
sudo systemctl show docker -p FragmentPath -p DropInPaths -p ExecStart -p ActiveState不要直接编辑 /usr/lib/systemd/system/docker.service 或 /lib/systemd/system/docker.service,包升级会覆盖它。代理、资源限制或受控参数使用 drop-in:
sudo systemctl edit docker配置变化后的顺序是:验证配置、daemon-reload、重启、检查状态和日志。不要把 restart 与后面的验证用 ; 串联,否则重启失败后脚本仍可能继续:
sudo systemctl daemon-reload
sudo systemctl restart docker
sudo systemctl is-active --quiet docker
sudo journalctl -u docker.service -n 100 --no-pager
docker versionStart request repeated too quickly 只是 systemd 停止重试的结果,根因通常在更早的 daemon 日志,例如 JSON 无效、同一选项同时出现在 unit 参数和 daemon.json、数据目录不可访问、存储后端不兼容或防火墙规则失败。
daemon.json 每个字段都改变宿主机行为
Linux rootful daemon 默认读取 /etc/docker/daemon.json。先创建备份目录并只复制当前文件,不直接覆盖未知配置:
sudo install -d -m 0750 /etc/docker/backup
if sudo test -f /etc/docker/daemon.json; then
sudo cp -a /etc/docker/daemon.json "/etc/docker/backup/daemon.json.$(date +%Y%m%d%H%M%S)"
fi一份适合共享开发机讨论的基线如下,实际地址与并发数要由容量测试决定:
{
"data-root": "/var/lib/docker",
"log-driver": "local",
"log-opts": {
"max-size": "20m",
"max-file": "5"
},
"live-restore": true,
"max-concurrent-downloads": 4,
"max-concurrent-uploads": 2,
"default-address-pools": [
{ "base": "172.30.0.0/16", "size": 24 }
]
}字段影响不能停留在“建议值”:
data-root 决定镜像、容器元数据和部分 daemon 状态落在哪个文件系统。迁移必须停 daemon、保持所有权与扩展属性、验证挂载先于 Docker 启动,并保留旧目录回退。log-driver 是新建容器的默认日志实现。local 自带轮转且磁盘效率更高;json-file 默认不轮转,长期高日志量可能写满磁盘。log-opts 的值必须写成字符串。改变 daemon 默认值不会改造已经存在的容器,旧容器要重建后才采用新策略。
live-restore 允许 daemon 不可用时尽量保持运行容器,但不会让网络、exec、日志采集和容器管理继续可用,也不是高可用方案。下载上传并发会改变代理、registry 与磁盘压力。低带宽或高延迟网络不一定适合更高并发。default-address-pools 用来减少新建 bridge 网络与企业网段冲突;改变后不会自动重编已有网络。
在重启前验证 JSON 与 daemon 选项。目标版本支持时使用 daemon 自带验证;至少还要做严格 JSON 解析:
python3 -m json.tool /etc/docker/daemon.json >/dev/null
sudo dockerd --validate --config-file=/etc/docker/daemon.json反向实验应在临时文件中完成,不污染真实配置:
tmp="$(mktemp)"
trap 'rm -f -- "$tmp"' EXIT
printf '{"log-driver":"local",}\n' > "$tmp"
set +e
sudo dockerd --validate --config-file="$tmp" >"$tmp.out" 2>&1
rc=$?
set -e
test "$rc" -ne 0
grep -Ei 'invalid|error|character' "$tmp.out"
printf 'EXPECTED_FAILURE rc=%s\n' "$rc"
rm -f -- "$tmp.out"预期为非零退出码和 JSON 解析证据。若目标版本没有 --validate,使用与生产相同版本的隔离虚拟机做启动验证,不能在共享 daemon 上试错。
权限模型:docker 组不是普通开发组
rootful daemon 的 Unix socket 可以创建特权容器、挂载宿主机目录、访问设备和修改网络。能调用它的用户基本拥有 root 级能力。因此有三种常见模型:
受控 sudo
个人或少量管理员通过 sudo docker ... 操作,权限最清晰,适合共享宿主机。不要为“命令短一点”给整个研发组开放任意 Docker 命令的免密 sudo;即便只允许 docker run,用户也能挂载宿主机根目录获得高权限。
docker 组
sudo usermod -aG docker "$USER"重新登录后可免 sudo 调用 daemon,但这是一项主机特权授权。授权应进入资产与权限审计,限定专用开发机,离职或角色变化时移除。不要把公开 PR 的 CI Runner、多人教学账号和生产运维账号混在同一组。
如果此前用 sudo 运行 CLI 导致个人配置归 root:
sudo chown -R "$USER":"$USER" "$HOME/.docker"
sudo chmod -R g+rwx "$HOME/.docker"修改前先确认 $HOME/.docker 是当前用户目录且没有符号链接指向其他位置。
Rootless mode
Rootless 让 daemon 与容器都运行在用户 namespace 内,降低 rootful daemon 被利用时的宿主机风险。它适合个人开发环境、低端口服务和不需要特权设备的构建任务,但要验证 cgroup v2、UID/GID 子区间、网络、存储、端口和设备限制。
先确认当前用户拥有至少 65536 个连续 subordinate UID/GID,并判断系统级 daemon 是否仍在运行:
id -u
grep -E "^${USER}:" /etc/subuid /etc/subgid
command -v newuidmap
command -v newgidmap
systemctl is-active docker 2>/dev/null || true缺少 newuidmap/newgidmap 时安装发行版的 uidmap 包;缺少子区间时由主机管理员分配,不能让多个账户重叠。dockerd-rootless-setuptool.sh install 默认会在检测到系统级 Docker daemon 时拒绝安装;个人专用机可先停用 rootful daemon,共享机确需并存时才在风险评估后使用工具提示的 --force,并始终用 context 区分两套 socket。安装 docker-ce-rootless-extras 后,以普通用户执行:
dockerd-rootless-setuptool.sh install
systemctl --user enable --now docker
sudo loginctl enable-linger "$USER"
docker context use rootless
docker infodocker info 的 Security Options 应出现 rootless。部分工具还需要:
export DOCKER_HOST="unix:///run/user/$(id -u)/docker.sock"不要在同一个 Shell 同时设置 DOCKER_HOST 又随意切 context。低于 1024 的端口、设备映射、某些 overlay 网络和资源控制可能需要额外系统配置或无法等价实现。团队支持 Rootless 前,应使用真实 Compose、bind mount、调试器、数据库 volume 与 CI 构建做兼容矩阵。
用正反实验验证隔离、资源与持久化
下面的自包含脚本只创建唯一标签对象。无论正向断言、反向断言还是镜像拉取在哪一步失败,退出钩子都只清理标签与随机名称同时匹配的容器和 volume:
set -eu
lab="engine-lab-$(date +%s)"
volume="${lab}-data"
err="$(mktemp)"
cleanup() {
container_label="$(docker inspect "$lab" --format '{{ index .Config.Labels "tool-efficiency.lab" }}' 2>/dev/null || true)"
if [ "$container_label" = "$lab" ]; then
docker rm -f "$lab" >/dev/null
fi
volume_label="$(docker volume inspect "$volume" --format '{{ index .Labels "tool-efficiency.lab" }}' 2>/dev/null || true)"
if [ "$volume_label" = "$lab" ]; then
docker volume rm "$volume" >/dev/null
fi
rm -f -- "$err"
}
trap cleanup EXIT
docker volume create --label tool-efficiency.lab="$lab" "$volume" >/dev/null
docker run --name "$lab" \
--label tool-efficiency.lab="$lab" \
--memory 64m --cpus 0.25 \
--mount "type=volume,src=$volume,dst=/data" \
alpine sh -c 'printf ENGINE_OK >/data/result && cat /data/result'
test "$(docker inspect "$lab" --format '{{.State.ExitCode}}')" = 0
test "$(docker run --rm --mount "type=volume,src=$volume,dst=/data,readonly" alpine cat /data/result)" = ENGINE_OK
limits="$(docker inspect "$lab" --format 'memory={{.HostConfig.Memory}} nano_cpus={{.HostConfig.NanoCpus}}')"
test "$limits" = 'memory=67108864 nano_cpus=250000000'
set +e
docker run --rm --mount "type=volume,src=$volume,dst=/data,readonly" \
alpine sh -c 'printf SHOULD_FAIL >/data/result' 2>"$err"
rc=$?
set -e
test "$rc" -ne 0
grep -Ei 'read-only|permission denied' "$err"
printf 'VERIFY_OK lab=%s %s negative_rc=%s\n' "$lab" "$limits" "$rc"预期输出 VERIFY_OK,内存为 67108864,CPU 配额为 250000000,第二个容器能从同一 volume 读到 ENGINE_OK,只读写入返回非零。这证明容器可写层与 volume 持久状态是两套边界,也证明只读挂载门禁实际生效。
共享主机上的清理脚本应按项目 label、明确对象名和保留期执行。docker system prune 作用于整个 daemon,不应成为单个项目的 reset 命令。
日志必须有容量上限,也要知道丢日志的代价
daemon 日志与容器日志是两条链:
sudo journalctl -u docker.service --since '-30 min' --no-pager
: "${CONTAINER_NAME:?set CONTAINER_NAME to a real container name}"
docker logs --tail 100 "$CONTAINER_NAME"
docker inspect "$CONTAINER_NAME" --format '{{json .HostConfig.LogConfig}}'
docker info --format '{{.LoggingDriver}}'daemon 默认 json-file 时可能没有轮转。共享开发机可以选择 local,或为 json-file 配置 max-size 与 max-file。修改只影响新建容器,必须通过重建探针验证:
docker run --rm alpine echo LOG_PROBE
docker info --format 'default={{.LoggingDriver}}'阻塞日志模式能保留背压语义,但日志端异常可能拖慢应用;非阻塞模式保护应用线程,却会在缓冲区满时丢新日志。故障诊断与审计要求高的任务不能在没有丢弃指标和外部采集的情况下盲目改成非阻塞。无论使用哪种驱动,都要监控数据根文件系统的字节和 inode。
存储后端不是一个永远固定的 overlay2
镜像只读层与容器可写层由 Engine 的镜像存储后端管理,数据库等持久数据应放 volume 或经过设计的 bind mount。把数据库数据写进容器可写层会承担 copy-on-write 开销,删容器时也会丢失。
检查真实状态:
docker info --format 'driver={{.Driver}} status={{json .DriverStatus}} root={{.DockerRootDir}}'
findmnt -T "$(docker info --format '{{.DockerRootDir}}')"
df -hT "$(docker info --format '{{.DockerRootDir}}')"
df -ih "$(docker info --format '{{.DockerRootDir}}')"Docker Engine 29.0 及更高版本的全新安装默认使用 containerd image store 与 overlayfs snapshotter;从旧版本升级的机器继续使用经典 overlay2,除非显式启用新后端。切换两类后端时,另一后端创建的镜像与容器会暂时不可见,但数据仍占磁盘。containerd image store 还会同时保存压缩层和解压层,容量通常高于经典存储;它目前也不能与 userns-remap 同时使用。没有容量、兼容、迁移与回退计划时,不要通过改一个 feature 开关完成“升级”。
经典 overlay2 还依赖宿主文件系统能力,例如 XFS 需要合适的 d_type。更换 storage driver 或 backing filesystem 前应:停止写入、导出自建镜像或推送到 registry、对 volume 做应用一致性备份、记录现有 docker info、在同版本测试机验证,再安排维护窗口。绝不能手工修改 /var/lib/docker 内的层目录。
迁移 data-root 前先识别两套数据根
使用经典 overlay2 时,镜像、容器与 volume 主要位于 Docker data-root。使用 containerd image store 时,镜像内容与容器 snapshot 默认位于 /var/lib/containerd,volume、配置等其他状态仍在 /var/lib/docker;daemon.json 的 data-root 不会移动前者。先识别后端和两个文件系统:
docker info --format 'root={{.DockerRootDir}} driver={{.Driver}} status={{json .DriverStatus}}'
sudo du -sh /var/lib/docker /var/lib/containerd 2>/dev/null || true
findmnt -T /var/lib/docker
findmnt -T /var/lib/containerdcontainerd 路径要通过 /etc/containerd/config.toml 的 root 单独规划,并与 Docker 数据目录使用不同的专用目录。它涉及 containerd 服务和 snapshot 元数据,不应把下面只针对 Docker data-root 的复制步骤直接套上去;先在同版本隔离节点验证服务停止顺序、配置和恢复。
经典数据根的回退链
迁移前确认新路径是独立挂载且容量、inode、权限、SELinux/AppArmor 策略满足要求。操作框架如下:
old=/var/lib/docker
new=/srv/docker-data
test "$old" = /var/lib/docker
test "$new" = /srv/docker-data
sudo install -d -m 0710 "$new"
test "$(findmnt -n -o TARGET -T "$new")" = "$new"
sudo systemctl stop docker docker.socket
test "$(systemctl is-active docker 2>/dev/null || true)" != active
sudo rsync -aHAXx --numeric-ids "$old"/ "$new"/
sudo du -sb "$old" "$new"findmnt 断言要求新目录本身就是挂载点,避免数据意外写回根分区。du 只能发现数量级异常,不能证明扩展属性、稀疏文件和运行状态一致。然后设置 data-root,验证 JSON,启动 daemon,逐项检查镜像、容器、volume、网络和项目探针。旧目录先只读保留到回退窗口结束,不能立即 rm -rf。回退时停止 daemon、恢复原配置并重新指向旧目录。复制期间若 daemon 仍在写入,得到的不是一致状态。
代理与 CA 分三层排查
daemon 拉镜像的代理可写进 daemon.json,也可以通过 systemd drop-in 提供:
[Service]
Environment="HTTP_PROXY=http://proxy.example.invalid:3128"
Environment="HTTPS_PROXY=http://proxy.example.invalid:3128"
Environment="NO_PROXY=localhost,127.0.0.1,.internal.example.invalid"这里使用保留域名,实际值由企业配置系统注入。修改后:
sudo systemctl daemon-reload
sudo systemctl restart docker
sudo systemctl show --property=Environment docker
docker pull alpinesystemctl show 可能暴露代理 URL 中的凭证,因此生产代理优先使用不含明文用户密码的认证设计,日志与工单也要脱敏。daemon 代理只解决拉取和访问 registry;Dockerfile 构建阶段与运行中容器访问外网,还要分别验证自己的代理环境。
私有 registry 使用企业 CA 时,把只含公钥的 CA 放进与 registry 端点完全一致的目录,CA 扩展名必须是 .crt,不能误写成代表客户端证书的 .cert:
registry='registry.example.invalid:5000'
ca_file='./enterprise-registry-ca.crt'
test -f "$ca_file"
openssl x509 -in "$ca_file" -noout -subject -issuer -fingerprint -sha256
sudo install -d -m 0755 "/etc/docker/certs.d/$registry"
sudo install -m 0444 "$ca_file" "/etc/docker/certs.d/$registry/ca.crt"
sudo systemctl restart docker
http_code="$(curl -sS -o /dev/null -w '%{http_code}' "https://$registry/v2/")"
case "$http_code" in
200|401) printf 'TLS_OK http=%s\n' "$http_code" ;;
*) printf 'registry probe failed: http=%s\n' "$http_code" >&2; exit 23 ;;
esac示例使用保留域名,必须先替换并由配置管理注入真实端点;执行前核对证书指纹。未匿名开放的 Registry 正常返回 401,这证明 TLS 和 HTTP 已建立,只是还缺认证;连接错误或证书错误才进入 CA、SNI 和代理排查。失败时保留 journalctl -u docker.service 和 TLS 错误。不要用 insecure-registries 绕过证书错误作为长期方案,它把身份校验问题变成中间人攻击窗口。业务容器访问企业 HTTPS 还要在镜像内部安装 CA,并由语言运行时信任;宿主机信任不会自动进入容器。
四段探针能快速定位故障层:
curl -fsSI https://registry-1.docker.io/v2/ >/dev/null || true
docker pull alpine
docker run --rm alpine wget -qO- https://example.com >/dev/null
printf 'FROM alpine\nRUN wget -qO- https://example.com >/dev/null\n' | docker build --no-cache -t engine-net-probe -
docker image rm engine-net-probe宿主失败先查 DNS、路由、代理和 CA;宿主成功但 pull 失败查 daemon;pull 成功而容器失败查镜像信任库与容器代理;只有 build 失败则查 BuildKit 与构建参数。
远程 API、凭证与 socket 必须按 root 权限治理
不要把 dockerd 监听到 0.0.0.0:2375。未认证 Docker API 相当于远程 root。确需远程管理时,使用双向 TLS、主机防火墙、独立 CA、短生命周期客户端证书和访问审计,并让 context 指向受控端点。更常见的研发方案是通过 SSH context 访问专用主机,仍需限制远端账号权限。
把 /var/run/docker.sock 挂进 CI 容器或开发容器,同样把宿主机控制权交给该容器。所谓 Docker-outside-of-Docker 不是隔离,只是把客户端装进容器。运行不可信仓库代码时使用临时虚拟机、Rootless daemon 或具备明确隔离边界的构建服务,不要让公开 PR 接触共享 socket。
Registry 凭证位于用户 Docker 配置或凭证助手中。不要以 root 登录 registry 后把 /root/.docker/config.json 复制给团队,也不要把长期 token 烘焙进镜像。使用最小权限机器人账号、短期 token 和操作系统凭证存储;脚本退出与设备退役时撤销服务端凭证。
升级不是 apt upgrade 后看服务绿灯
升级前保存:
docker version
docker info
sudo systemctl cat docker
if sudo test -f /etc/docker/daemon.json; then
sudo cp -a /etc/docker/daemon.json "./daemon.json.before-upgrade"
fi
docker ps --format '{{.Names}}|{{.Image}}|{{.Status}}'
docker volume ls再阅读目标版本发行说明与弃用项,检查 API 兼容、cgroup、iptables/nftables、containerd image store、Compose/Buildx 插件和存储变化。代表性回归至少包含:拉取带摘要镜像、构建项目、创建 bridge 网络、端口访问、volume 重启持久化、日志轮转和 Rootless 路径。
共享开发机采用 canary:先升级一台具有相同内核、文件系统、代理和安全策略的机器,观察一个完整工作日或团队约定窗口,再滚动升级。包管理器要保留上一版软件包、完整依赖版本和可用仓库快照。APT 可先用 apt list --all-versions 确认旧版仍存在,再对 Engine、CLI 及经过验证的依赖执行精确版本安装;DNF 则先读取 dnf history info 和仓库版本列表。不要在故障主机上直接尝试 history undo:先停写、导出运行证据并在 canary 复现。
回退时不能只降 docker-ce,还要同步检查 CLI、containerd、Buildx、Compose、配置格式、API、网络规则和存储元数据是否发生不可逆变化。若升级同时启用了 containerd image store,先按后端切换方案恢复可见性,再判断是否降包;把“旧镜像重新出现”与“旧版 daemon 可以安全读取新元数据”当成两项不同验证。
出现升级后旧对象不可见时,先比较 docker info 的 Docker Root Dir、Driver 与 DriverStatus,再判断是否切换了 image store。不要立即清空数据目录。若新版本已迁移元数据或宿主防火墙规则,回退必须按对应发行说明执行,不能承诺“装回旧包就恢复”。
卸载与清理必须把软件包和数据分开
卸载 Engine 软件包通常不会自动删除 /var/lib/docker、/var/lib/containerd、volume 与自定义配置。这是防止误删数据的设计,不是卸载不完整。退役流程先列清:
docker context ls
docker system df -v
docker volume ls
sudo du -sh /var/lib/docker /var/lib/containerd 2>/dev/null || true
sudo find /etc/docker -maxdepth 3 -type f -print确认备份恢复、停止业务、撤销 registry 与远程 API 凭证,再由主机 owner 删除软件包。数据销毁必须使用资产系统确认的绝对路径,检查挂载点和设备,不能把官方卸载示例中的 rm -rf 原样放进自动脚本。对云盘或含密钥的设备,还要按组织数据销毁标准处理快照与备份。
把 Engine 管成团队共享能力
团队基线应记录发行版与内核、软件包来源、精确版本、daemon 配置、systemd drop-in、数据盘、存储后端、日志策略、代理与 CA、权限模型、升级 owner 和回退证据。配置进入受控仓库,但代理密码、registry token、客户端私钥和真实内网地址不进入公开文档。
容量判断至少观察数据根的字节与 inode、镜像和 Build Cache 增长、volume 增长、日志速率、pull 延迟、daemon 重启次数与容器启动失败。磁盘还有 30% 空闲却 inode 耗尽,同样会让容器创建失败;日志轮转减少磁盘风险,却会缩短本地排障窗口,需要外部日志系统承接长期证据。
共享开发主机还要明确租户边界。Docker Engine 默认不是强多租户沙箱,docker 组成员互相可见容器、镜像、网络和 volume 元数据,并可能获得宿主机控制权。不同信任域、客户数据或公开代码不能仅靠容器名隔离,应拆到不同 VM、不同 Rootless 用户或专用构建池。
可靠的验收不是“服务是 active”,而是团队能持续证明:软件包来自受信仓库且版本可追踪;CLI 连接预期 daemon;普通账号没有意外 root 权限;配置错误会在重启前被阻断;数据盘、日志与 inode 有容量门槛;代理和 CA 的故障层可定位;升级能在 canary 复现项目;退役时软件、数据与凭证都能按各自生命周期清除。
