1. 对比包管理器与二进制通用包:什么环境才值得选后者
1.1 两种安装方式的分水岭
大多数人在 Linux 上装 Docker,第一反应就是 apt 或 yum 一把梭。这个思路本身没错,apt install docker.io或者yum install docker-ce在普通场景里确实省心,依赖自动拉、systemd 单元自动写、启动脚本自动配好,几乎零成本。但如果你在下面这几类环境里待过,就会明白包管理器方案其实非常别扭:
- 内网离线服务器,软件源根本没有 Docker 的包,想临时挂载一个本地源又要折腾半天;
- 发行版比较冷门,或者是一个阉割版系统,包仓库里没有 Docker,或者版本老得没法看;
- 生产环境要求版本可预期、可锁定,不允许 apt upgrade 一冲动把 Docker 版本顺带换掉;
- 需要在一批不同发行版上保持完全一致的 Docker 版本,方便排障和运维。
有这些需求时,官方发布的二进制通用包就是最合适的答案。它本质上是一个静态编译的 tarball,不依赖系统里有没有某版本 libc,不需要包管理器参与,把里面的工具铺好位置,自己写好 systemd 单元,Docker 就能像原生服务一样跑起来。
官方把这种安装方式称为 static binary。下面这张表是我在实际运维中反复对比后的结论:
| 对比项 | 包管理器安装(apt/yum) | 二进制通用包安装 |
|---|---|---|
| 依赖处理 | 自动拉取,但可能污染系统中的共享依赖 | 基本零库依赖,全部内置在 tar 包中 |
| 版本控制 | 受仓库源版本约束,容易被动升级 | 自己下载指定版本,版本完全可控 |
| 离线安装 | 需要完整的离线源,工作量大 | 只需一个 tarball 和校验文件 |
| systemd 集成 | 自动完成 | 手动编写 unit 文件,约 10 分钟 |
| 升级/回滚 | 依赖包管理器的行为 | tar 包覆盖即升级,留旧版即回滚 |
| 学习成本 | 低 | 中等,但对排障能力很有帮助 |
一句话:包管理器适合"求快",二进制通用包适合"求稳、求可控、求可复现"。生产环境我后面基本一律二进制。
1.2 二进制包的适用边界与先决条件
需要说清楚,二进制包不是万能的,它也有自己的边界。首先是内核与系统基础组件,这恰恰是很多人忽略的点。Docker 的静态包虽然把用户态的东西都打包了,但底层仍然依赖 Linux 内核特性,比如 namespaces、cgroups、overlayfs、iptables/nftables。也就是说,你可以在一个非常精简的最小系统上装 Docker,但这个系统的内核要足够新,至少能支撑 overlay2 存储驱动和 cgroup v2(内核 4.9 以上基本没压力,5.x 内核是最舒服的)。内核太老的话,再高级的二进制包也白搭。
其次是进程托管方式。官方二进制包里的 dockerd 不带自带的服务管理逻辑,你希望它开机自启、崩溃自动拉起、和 systemd 日志系统无缝集成,就得自己写 systemd unit。如果目标机器是 SysV init 或纯容器化环境,连 systemd 都没有,那就要用dockerd &这类方式凑合,但这种跑法不适合长期生产,日志和进程管理都很脆弱。
最后是SELinux 和 AppArmor。在 RHEL/CentOS 系列上,如果用二进制包手动铺装,polkit 规则和 SELinux 上下文一般不会自动配好,遇上"服务起不来 / 容器无法访问文件"这类怪问题时,优先怀疑 SELinux 而不是 Docker 本身。好在大部分普通场景下把这层先 relax 掉就能跑通,具体怎么定位我放到后面故障排查那一节细说。
1.3 装之前先明确这套方案的"管理成本"
聊完优点,我也得把账算明白。二进制通用包的主要代价就是一切手动:
- 不会自动升级,安全性补丁需要自己关注 release 动态;
- 不会自动补 unit 文件,初始化时必须自己写;
- 不会自动处理用户组,普通用户要访问 Docker 得手动加入 docker 组;
- 不会自动处理镜像加速与日志轮转,这些都要写进
/etc/docker/daemon.json。
这些成本加起来差不多也就是半小时的一次性投入,换来的是对整套运行时(dockerd、containerd、runc)的完全掌控。我见过不少团队被包管理器升级搞怕了:一次ubuntu unattended-upgrade把 Docker 从 20.10 升到 24.0,结果 K8s 集群直接不认存储驱动。用二进制包锁版本之后,这种事情再没发生过。接下来我就按完整流程走一遍。
2. 下载、校验与拆包:把官方仓库的 tarball 变成可控资产
2.1 确认架构与官方目录结构
动手之前先确认两件事:一是目标系统的 CPU 架构,二是目标系统能不能访问官方下载站点。架构直接决定下载路径,别想当然地以为都是 x86_64。
uname -m cat /etc/os-release常规服务器输出一般是x86_64,树莓派之类的是aarch64或armv7l,还有s390x、ppc64le这类小众架构。Docker 官方下载目录把架构分得很清楚:
https://download.docker.com/linux/static/stable/x86_64/ https://download.docker.com/linux/static/stable/aarch64/ https://download.docker.com/linux/static/stable/armv7l/ https://download.docker.com/linux/static/stable/s390x/注意stable和edge(有的版本叫test)的区别。stable目录下的才是经过完整验证的稳定版,edge或者test目录下的版本用于试验新功能,千万不要拿到生产环境去装。有的教程为了追新版本,直接从别处复制一个 URL 就下载,这很容易下到非 stable 分支的包,后面的行为都不可预期。
2.2 选版、下载与校验
确认好架构后,进目录看一下有哪些版本。我用 x86_64 举例,假设当前 stable 目录下的最新版是docker-27.3.1.tgz,实际以你在目录列表里看到的为准:
curl -sSL https://download.docker.com/linux/static/stable/x86_64/ | grep 'docker-27'找个干净的目录,比如/opt/software,把包下载下来:
mkdir -p /opt/software cd /opt/software wget https://download.docker.com/linux/static/stable/x86_64/docker-27.3.1.tgz下载完成后,务必做校验。这一步很多人会跳过,但我强烈建议不要省。静态包大多是压缩包,传输过程中有损坏,或者镜像源被替换,都可能导致后面装出一个行为诡异的 Docker。校验方法很简单:
sha256sum docker-27.3.1.tgz然后把输出的哈希值和 Docker 官方在该版本 Release Notes 或 GitHub Release 页面公布的校验值对比。同一目录下也有文件大小的参考值,连大小都对不上的话基本可以判定文件坏了。
我踩过这样一个坑:某次在内网通过跳板机传输 tarball,中间环节的文件被截断了 200 字节,tar没有立刻报错,结果解压出来的 dockerd 一启动就 segmentation fault,查了大半天才发现是包坏了。从那以后,凡是从外部拿回来的安装包,一律先sha256sum再落地,这应该成为安装规范的一部分。
2.3 解包后先验货:tarball 里到底有什么
下载校验通过后,解压:
tar xzf docker-27.3.1.tgz ls -lh docker/你会看到一个docker目录,里面是这套通用包的完整内容。我以某个较新的版本为例,典型的文件清单如下:
docker/ ├── containerd ├── containerd-shim-runc-v2 ├── ctr ├── docker ├── docker-init ├── docker-proxy ├── dockerd └── runc这些文件各管一摊:
docker:客户端 CLI,就是你平时敲docker ps、docker run用的那个程序;dockerd:守护进程端,真正管理镜像、容器、网络的那部分;containerd:容器运行时管理器,负责拉镜像、管理容器生命周期,Docker 在现代版本里把它作为底层运行时的核心组件;containerd-shim-runc-v2:容器和 containerd 之间的中间进程,负责容器退出后的兜底和 IO 转发;runc:OCI 容器的真正创建者,负责和 Linux 内核打交道;ctr:containerd 自带的调试客户端,平时用不到,但排查 containerd 问题时要靠它;docker-init:容器内 PID 1 的轻量初始化程序,负责处理孤儿进程和信号转发;docker-proxy:宿主机端口映射到容器时的辅助代理,负责 docker run -p 的端口转发。
先知道包里都有什么,后面排查问题时思路会清晰很多。比如容器里进程变僵尸、端口映射时灵时不灵,你就能联想到是 docker-init 或 docker-proxy 的问题,而不是瞎猜。
3. 铺装整机:二进制布局、权限与三段 systemd 单元
3.1 拷贝二进制、准备目录与用户组
验完货直接拷贝。我习惯把二进制放到/usr/bin,这样 systemd 单元里写ExecStart=/usr/bin/dockerd最自然,也符合大多数发行版 PATH 的默认范围。如果你想装到/usr/local/bin也完全可以,但 unit 文件里的路径必须同步修改,别配岔了。
cp docker/* /usr/bin/ chmod 755 /usr/bin/docker /usr/bin/dockerd /usr/bin/containerd /usr/bin/runc /usr/bin/ctr /usr/bin/docker-init /usr/bin/docker-proxy /usr/bin/containerd-shim-runc-v2接着准备 Docker 自己的目录和用户组:
mkdir -p /etc/docker mkdir -p /var/lib/docker groupadd docker/etc/docker是 daemon.json 配置所在地,/var/lib/docker是默认数据根目录,容器、镜像、卷全在这里。这里有一点值得展开:如果你打算把数据盘挂到独立分区,一定要在初始化阶段就把>getent group docker || groupadd docker
加组这步不会立刻对当前登录的 shell 生效,后面我会讲具体怎么让权限马上可用。
3.2 containerd 先行:为什么这套方案离不开鸡生蛋蛋生鸡的编排
老版本的 Docker 直接由 dockerd 自己管理容器运行时,新版本的架构则是 dockerd 与 containerd 协作。从 Docker 20.10 以后,containerd 必须作为独立服务存在,dockerd 启动时会去连 containerd 的 socket(默认在/run/containerd/containerd.sock)。如果你只把 dockerd 拉起来而 containerd 没运行,dockerd 会反复报 "failed to connect to containerd" 之类的错误。
所以这套方案的正确编排是:docker.socket先监听好 API socket,containerd.service先运行起来,docker.service再作为最终承载启动 dockerd。三个单元互相之间要靠Requires、After、Wants把顺序约束住,否则 systemd 并行启动时会出现竞态,偶尔成功偶尔失败。
这一段是我实际排障得来的教训。最早我图省事,只写了 docker.service 一个单元,把 containerd 直接忽略,结果服务 10 次里有 3 次起不来,起不来时 journald 里的错误信息还非常具有迷惑性。后来老老实实拆成三个单元,顺序理顺,这个问题彻底消失。
3.3 三段 unit 文件逐一拆解
下面是三个 unit 文件的具体内容,你可以直接复制参考,只要把路径和你的环境对齐就可以。
先创建/etc/systemd/system/docker.socket:
[Unit] Description=Docker Socket for the API [Socket] ListenStream=/var/run/docker.sock SocketMode=0660 SocketUser=root SocketGroup=docker [Install] WantedBy=sockets.target这个 socket 单元的意义在于:systemd 会提前把/var/run/docker.sock创建好并监听。这样一来,即使 dockerd 还没完全就绪,客户端连这个 socket 时 systemd 也能通过 socket activation 机制把 dockerd 拉起来。SocketMode=0660和SocketGroup=docker就决定了普通 docker 组成员能不能访问——如果你省掉这个设置,后面保准会遇到权限 denied,这是权限问题的根源之一。
再创建/etc/systemd/system/containerd.service:
[Unit] Description=containerd container runtime Documentation=https://containerd.io After=network.target local-fs.target [Service] ExecStartPre=-/sbin/modprobe overlay ExecStart=/usr/bin/containerd Type=notify Delegate=yes KillMode=process Restart=always RestartSec=5 LimitNOFILE=1048576 LimitNPROC=infinity LimitCORE=infinity TasksMax=infinity [Install] WantedBy=multi-user.target这个单元里有几个细节值得说明。ExecStartPre=-/sbin/modprobe overlay中的短横线表示这条命令即使失败也不要影响服务启动,因为 overlay 模块在很多新内核里已经内置了,加载失败不代表有错误。Type=notify表示 containerd 会通过 sd_notify 协议主动告诉 systemd "我已经准备好",这比盲目等 timeout 要可靠得多。Delegate=yes允许 containerd 完整接管 cgroup 子树的资源管理,配合TasksMax=infinity避免进程数限制导致容器启动报 "cgroup" 相关错误。
最后创建/etc/systemd/system/docker.service,这是最核心的一个:
[Unit] Description=Docker Application Container Engine Documentation=https://docs.docker.com After=network-online.target firewalld.service containerd.service docker.socket Wants=network-online.target Requires=network-online.target docker.socket containerd.service [Service] Type=notify ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock ExecReload=/bin/kill -s HUP $MAINPID TimeoutStartSec=0 RestartSec=2 Restart=always LimitNOFILE=infinity LimitNPROC=infinity LimitCORE=infinity TasksMax=infinity Delegate=yes KillMode=process [Install] WantedBy=multi-user.target解释几个关键点:
ExecStart里的-H fd://表示 dockerd 通过 systemd 传入的 socket fd 来接收请求,而不是自己再开一个 socket 监听。这依赖 docker.socket 单元,如果 socket 单元没写对,这里就会出现fd://无法工作的怪问题。--containerd=/run/containerd/containerd.sock明确指定 dockerd 要找的 containerd socket 路径。TimeoutStartSec=0表示不限制 dockerd 启动等待时间。首次启动时 dockerd 要初始化网络、存储、底层运行时等,对于大型数据盘或者特殊存储驱动,启动可能超过默认的 90 秒,不设成 0 就会看到 "timed out" 但实际服务还在初始化。Restart=always配合RestartSec=2,进程异常退出后 2 秒自动拉起,这在生产环境能大大减少人工干预。
三个文件写完后,执行:
systemctl daemon-reload每次修改 unit 文件后都必须daemon-reload,否则 systemd 还记着旧配置,改了等于白改。
3.4 daemon.json 的初始化配置
还记得前面建好的/etc/docker目录吗?现在给/etc/docker/daemon.json写入一份初始配置。我建议哪怕是临时测试环境也写上,避免后面为日志撑爆磁盘而头疼:
{ "data-root": "/var/lib/docker", "exec-opts": ["native.cgroupdriver=systemd"], "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" }, "storage-driver": "overlay2", "registry-mirrors": ["https://your-mirror-address.example.com"] }其中exec-opts里的native.cgroupdriver=systemd是我的强烈建议。当 Docker 在 systemd 下运行时,如果 cgroup 驱动保持默认的 cgroupfs,以后要是再叠上 Kubernetes 这类编排系统,就会出现 cgroup 驱动不一致的问题,K8s 会直接拒绝调度。storage-driver=overlay2是当前 Linux 主流内核环境下的最优选择,性能比 vfs 好一个数量级。registry-mirrors留了占位符,在实际使用中替换为你所在网络环境里可用且可信的镜像加速地址,能显著缓解拉镜像慢的问题。
如果写错了 daemon.json 导致 Docker 起不来,先别慌,把配置一项项读出来检查 JSON 格式(逗号、括号最容易错),改完再启动。
4. 首次启动与全链路验证:从 systemd 到拉镜像跑容器
4.1 按依赖顺序拉起服务
一切就绪,执行:
systemctl enable --now docker.socket containerd.service docker.service用enable --now一次性完成开机自启和立即启动。如果你担心顺序问题,可以分步:
systemctl start docker.socket systemctl start containerd systemctl start docker启动完先用systemctl status看一眼,这是最直观的状态反馈:
systemctl status docker --no-pager -l看到Active: active (running)基本就成功了一大半。然后测试 socket 和进程:
ls -l /var/run/docker.sock ps aux | grep -E 'dockerd|containerd'/var/run/docker.sock的属主应该是 root,属组是 docker,权限 0660。如果 socket 不存在,说明 docker.socket 单元没生效,检查systemctl status docker.socket。
4.2 docker version 与 docker info 该看哪些字段
服务起来之后立刻验证 CLI 能不能连上 daemon:
docker version这个命令会分两段显示:Client段和Server段。Client来自/usr/bin/docker这个二进制,Server来自 dockerd。如果 Server 段正常打印出来,说明客户端和 daemon 之间的链路、containerd 的连接都通了。如果 Server 段报错,那问题多半在 dockerd 或 containerd,需要去 journal 里翻日志。
接着看docker info的关键字段:
docker info重点确认这几项:
Server Version:是否和你下载的版本一致;Storage Driver:应该是 overlay2,如果变成 vfs,说明 overlay 模块有问题;Cgroup Driver:systemd 模式下应该显示 systemd;Docker Root Dir:确认数据目录是你预期的路径,别不知不觉写到了系统盘;Containers/Running/Images:初始状态为 0,这里能快速发现是否有历史数据残留。
4.3 hello-world 验证与普通用户免 sudo
然后跑最经典的验证:
docker run --rm hello-world这条命令背后发生了一连串事情:CLI 通过 socket 通知 dockerd,dockerd 让 containerd 去拉取 hello-world 镜像,containerd 通过 runc 创建容器,容器里输出一段欢迎信息。整条链路任何一个环节断了都会在这条命令上暴露。如果镜像拉取很慢,先检查 daemon.json 里的 registry-mirrors 是否生效,docker info里会列出当前 registry mirrors。
验证 Docker 功能正常后,处理普通用户权限。用普通用户执行docker ps时会看到经典的报错:
permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock处理方式是把自己加入 docker 组:
sudo usermod -aG docker $USER注意,加入 docker 组后不会立刻生效,因为当前会话的组信息还停留在登录时。要么重新登录,要么在当前 shell 里执行:
newgrp dockernewgrp这条命令很多人不知道,它能直接刷新当前终端进程的组身份,不用退出重登,省了很多事。之后再用docker ps就正常了。这里多说一句安全话题:docker 组等同于 root 权限,因为它可以挂载宿主机目录、操作特权容器,所以把哪些账号加进 docker 组要慎重,开发机无所谓,生产机上尽量只给运维账号。
到这里,一套由二进制通用包搭建的 Docker 已经完整跑起来了。但这只是开始,真正有价值的是后面遇到问题时怎么把故障拆解掉。
5. 启动失败与权限问题:一线排查的完整思路
5.1 第一步永远是看日志而不是瞎重启
服务起不来的时候,我见过太多人第一反应是systemctl restart docker,但这基本等于蒙着眼睛开车。正确的第一步是看日志:
journalctl -u docker -n 100 --no-pager journalctl -u containerd -n 100 --no-pager如果日志里信息不够,dockerd 还可以用前台调试模式跑,这样所有报错都会打在终端上,信息密度比 journal 高很多:
systemctl stop docker dockerd --debug前台模式下,你能看到 dockerd 一步步初始化:加载配置、初始化存储驱动、创建网桥、连接 containerd。卡在哪一步,错误就在哪一行。看到报错后按 Ctrl+C 停掉,再修复问题。这个方法对一切"启动失败但原因扑朔迷离"的场景都有效,建议每个人都练几遍,比背命令有用得多。
5.2 "Cannot connect to the Docker daemon" 的处理路径
客户端报Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?时,不要急着怀疑 daemon,先做三件事:
第一,确认 socket 文件是否存在:
ls -l /var/run/docker.sock不存在说明 docker.socket 没起来,或者启用了但被 systemd 移除了。检查systemctl status docker.socket。
第二,确认 socket 是否挂在 systemd 手里:
systemctl list-sockets | grep docker如果 socket 存在但 docker.service 没启动,所有连接会在 systemd 层面被 accept,然后触发 docker.service 启动。如果 docker.service 启动失败,连接就会报错。这时候看前面提到的 journal 日志,找 dockerd 的启动错误。
第三,确认 DOCKER_HOST 环境变量有没有被污染:
echo $DOCKER_HOST有些教程或者脚本会把DOCKER_HOST设成远程 daemon 地址,一旦被设成了非本机地址,客户端就会去连一个不存在的地址。清掉这个变量或用docker -H unix:///var/run/docker.sock ps强制指定就是另一个世界。
5.3 权限 denied:docker 组与 socket 模式的细节
权限问题的典型报错就是 5.3 节开头说的permission denied while trying to connect to the Docker daemon socket。它的根源通常是用户不在 docker 组,或者 socket 权限设置不对。
先验证你的用户在哪个组:
id sudo -u $USER groups如果不在 docker 组里,按前面 4.3 的方法加组。如果已经在组里还报权限错误,那就看 socket 权限:
ls -l /var/run/docker.sock期望结果是srw-rw----且 root 用户、docker 组。如果你的 socket 权限是 0600 或者其他异常值,说明 docker.socket 单元里的SocketMode=0660没写,或者后来有别的进程改动过 socket 权限。修复方式有两种:改 unit 文件然后重启 docker.socket,或者手动chmod 0660 /var/run/docker.sock。前者是治本,后者是应急。
还有一个隐蔽坑:在某些精简系统上,docker 组可能不存在,但 unit 文件里写了SocketGroup=docker,systemd 在创建 socket 时发现组不存在,会把组设置成一个不正常的值。所以前面提到getent group docker || groupadd docker的预检查要在写 unit 文件之前就做掉。
5.4 网络与存储驱动:两个高频翻车点
网络问题主要集中在宿主机上看到 docker0 网桥没有创建,或者容器之间互通异常。dockerd 启动时会自动创建 docker0,并往 iptables 里写入 NAT 和 FORWARD 规则。常见故障场景:
- 宿主机 netfilter 的 FORWARD 链策略被某些安全软件改成了 DROP,导致跨容器、容器外网流量全部被丢。解决办法是显式放行:
iptables -P FORWARD ACCEPT这只是临时做法,更持久的做法是把规则写进防火墙配置里,或者研究清楚是谁改了默认策略。
- 缺少
br_netfilter内核模块时,Docker 网络会出现各种诡异问题:
modprobe br_netfilter sysctl -w net.bridge.bridge-nf-call-iptables=1- 云平台环境常见的坑是主网卡没有开启混杂模式或受云防 ARP 限制,docker0 通了但容器出不去,这时排查顺序是:宿主机
ping 8.8.8.8是否通、容器里ping 宿主机是否通、iptables -t nat -L POSTROUTING里有没有容器网段的 MASQUERADE 规则。
存储驱动翻车则主要围绕 overlay2。如果你的内核开启了 module 加载限制,或者使用了一个不支持 overlay 的文件系统(比如把 Docker 数据目录放在某个云盘的只读挂载点上),dockerd 启动会退化成 vfs 驱动,性能大打折扣。验证方法:
cat /proc/filesystems | grep overlay有 overlay 输出说明内核支持。如果不支持,可以尝试modprobe overlay;还不行,就需要检查内核版本,或者换一个数据目录位置。用vfs跑生产环境不是不行,但容器层文件变化时是完整拷贝,镜像体积和 IO 消耗会非常感人。
5.5 常见报错速查表
下面把我在一线见过的高频报错整理成速查表,遇到问题先对照:
| 报错特征 | 可能原因 | 处理方向 |
|---|---|---|
Cannot connect to the Docker daemon | daemon 未启动、socket 不存在、DOCKER_HOST 错误 | 查 socket、journal、环境变量 |
permission denied...docker.sock | 用户不在 docker 组、socket 权限被改 | usermod -aG,chmod 0660,newgrp |
failed to dial gRPC: containerd | containerd 未启动、socket 路径不一致 | 启动 containerd,核对 dockerd 参数 |
error creating overlay mount | 内核无 overlay 模块、数据目录文件系统不支持 | modprobe overlay,换存储驱动 |
iptables failed/docker0 not found | FORWARD 策略、br_netfilter 缺失、firewalld 冲突 | 放行 FORWARD、加载模块、处理防火墙 |
error saving image...operation not permitted | daemon.json 路径权限错误、磁盘只读 | 检查 /var/lib/docker 权限与挂载 |
Device cgroup isn't mounted | cgroup 挂载异常(老内核/容器内嵌套) | 检查 /sys/fs/cgroup 挂载状态 |
cgroup v2: systemd driver vs cgroupfs | 后续加 K8s 时驱动不一致 | daemon.json 里统一为 systemd |
排查的核心思路永远是:先看日志、再查链路、最后动配置。别一上来就docker system prune或者重装系统,大部分问题都是配置层面的,日志里都写着答案。
6. 版本升级、回滚与日常维护:把二进制安装跑成长久方案
6.1 升级操作的标准动作
用二进制包方案的长期运行,绕不开升级和回滚。升级的标准动作其实很简单:
# 1. 停止 Docker 相关服务 systemctl stop docker.socket docker.service containerd.service # 2. 下载新版本 tarball 并校验(过程参考第二节) cd /opt/software wget https://download.docker.com/linux/static/stable/x86_64/docker-27.4.1.tgz sha256sum docker-27.4.1.tgz # 3. 解压并覆盖旧二进制 tar xzf docker-27.4.1.tgz cp docker/* /usr/bin/ # 4. 重新加载并启动 systemctl daemon-reload systemctl start containerd docker.socket docker.service升级前重点检查两件事。一是兼容性:新版本 dockerd 一般能兼容旧版本数据目录,但跨大版本升级(比如 20.x 直接到 27.x)建议先看官方 release notes 里是否提及存储格式或配置变更。二是containerd 版本:二进制 tar 包里的 containerd 是和 dockerd 配好套的,升级时应该一起覆盖。最忌讳的是系统里原来有一个 apt 装的 containerd,你又拿 tar 包的 containerd 覆盖了一半,最后两套版本混着用,保准出怪问题。
6.2 回滚预案
升级回滚是二进制方案的天然优势。只要升级前备份了旧版本二进制,回滚就是几个命令的事:
# 假设升级前你把旧二进制都留在了 /opt/docker-backup systemctl stop docker.socket docker.service containerd.service cp /opt/docker-backup/* /usr/bin/ systemctl daemon-reload systemctl start containerd docker.socket docker.service所以关键动作在升级前就要做:
mkdir -p /opt/docker-backup cp /usr/bin/docker /usr/bin/dockerd /usr/bin/containerd /usr/bin/runc /usr/bin/ctr /usr/bin/docker-init /usr/bin/docker-proxy /usr/bin/containerd-shim-runc-v2 /opt/docker-backup/另外,升级前尽量做一次数据层面的快照。/var/lib/docker里存放着容器和镜像,数据卷也在这里。虽然版本回滚一般不影响已有数据,但万一升级过程中数据目录被新版本 daemon 做了迁移,没有备份就只能干瞪眼。数据目录的备份不需要停服务,用tar --exclude排除运行时文件也能凑合,最稳的还是配合存储的快照能力。
6.3 日志、磁盘与孤儿资源
长期运行的 Docker 宿主机,最容易被忽视的是日志和磁盘。daemon.json 里配置的日志轮转参数要尽早配好。一旦没配,容器里程序疯狂打日志,json-file 日志文件可能以 GB 计的速度增长,直接撑爆系统盘。检查当前各容器日志大小的命令:
du -sh /var/lib/docker/containers/*/*-json.log | sort -rh | head如果已经积累了大量日志,可以即时清掉(容器还在运行,清完会继续写):
truncate -s 0 /var/lib/docker/containers/<container-id>/*-json.log镜像和容器长期增删后,悬空镜像和停止的容器会占用不少磁盘。定期清理是必要的:
docker system prune -f但注意-a参数会把所有未被容器使用的镜像都删掉,包括你想本地留档的版本。我自己一般只执行不带-a的docker system prune,版本镜像保留用docker image tag打上明确的版本号,避免误删。
6.4 几点长期经验
最后分享几条我用二进制方式维护 Docker 的长期经验:
一是保留一套固定的安装脚本。把下载、校验、铺装、unit 文件生成写成一个幂等 shell 脚本,放到服务器上,每次新机器处理直接跑一遍。这样能保证每台机器环境一致,出问题也好对比。
二是记录版本与校验值。我习惯在安装目录里放一个version.txt,记录安装/升级的时间、版本号、sha256 值。等到这台机器变成事故现场时,这些记录可能就是唯一能帮助你确认"当初到底装了什么"的依据。
三是关注 containerd 和 runc 的安全通告。二进制方案没有包管理器帮你跟踪 CVE,所以最好定期关注 Docker 官方 release 页面,确认当前稳定分支有没有重要的安全修复。这个成本不高,但收益很高。
四是别把 systemd 单元当一次性文件。它和 daemon.json 一样都是这台机器的配置资产。我通常会把这三个 unit 文件和 daemon.json 一起纳入配置管理仓库(Git 或 Ansible 都行),任何修改走版本化流程,而不是直接在机器上改。需要回滚配置时,比什么都好使。
我从第一台用二进制包方式部署 Docker 的机器算起,这套方案已经稳定跑了三四年,期间经历了多次大版本升级,既没有被包管理器的依赖绑架过,也没有因为某次自动升级导致集群不可用。每个方案都有自己的适用场景,但如果你和我一样要管理多个发行版混合的环境、又不想被系统包仓库牵着鼻子走,二进制通用包这条路,值得你认真走一遍。