做运维时间长了,你会发现最怕的不是新系统上的新功能,而是那些还在跑着老系统的机器。明明业务一直没迁移,安全补丁还得打,新组件还得装,偏偏环境又是 Ubuntu 20.04、18.04 这样的中老年系统。折腾 Docker 的时候,网上教程一大把,但大部分都默认你用的是最新 LTS,一换到旧版本就开始报错,不是仓库不对就是依赖冲突。这篇是我运维系列里的参考记录,标了【仅供参考】,因为不同内核、不同系统代差、不同网络策略,真照着抄的时候还是得结合自己环境来调整。
1. 为什么要在 Ubuntu 22.04 以下版本折腾 Docker
1.1 旧版本环境依然广泛存在
很多人会觉得,新装系统直接用 24.04,谁还在 20.04、18.04 上折腾?但实际生产环境里,老版本 LTS 的存在感极强。业务系统跑得稳的时候没人愿意动底层系统,数据库、应用中间件、编译环境全都绑在上面,一升级就是大项目,风险远大于收益。于是这些机器常年保持着 Ubuntu 20.04、18.04,甚至 16.04,直到某天有新的交付需求,需要部署容器化应用,Docker 就不得不上了。
在我的实操里,遇到过两种典型场景。一种是新项目要落地,但资源有限,不想再拆新机器出来,就拿现有的旧系统复用;另一种是客户要求环境统一,不同开发机、测试机、生产机要保持同一套系统版本,其中一部分机器是老的 LTS,导致新装机那套 Docker 安装方法在老的上面完全跑不通。这两种场景都很现实,核心矛盾就是:系统版本偏老,但容器化需求不会因此降低。
1.2 Docker 在旧版本上的兼容性边界
Docker 引擎不是单个静态二进制跑起来就完事,它依赖内核特性,比如 namespace、cgroups、iptables、overlayfs,还依赖 init 系统如何管理守护进程。Ubuntu 22.04 以下版本跨度很大,我用表格拉一个参考,方便你快速判断自己机器在什么水平:
| 系统版本 | 默认内核 | 安装 Docker 时的注意点 |
|---|---|---|
| Ubuntu 20.04 (focal) | 5.4 | 官方仓库支持好,注意 iptables 与 nftables 的关系 |
| Ubuntu 18.04 (bionic) | 4.15 | 需要用 lsb-release 正确获取代号,HWE 内核更稳 |
| Ubuntu 16.04 (xenial) | 4.4 | 新版 Docker 部分特性受限,建议锁定版本安装 |
| Ubuntu 14.04 (trusty) | 3.13 | 官方仓库已放弃,基本不建议强行安装 |
Docker 的最低内核要求是 3.10,所以从内核门槛来讲,16.04 以上都没问题。但存储驱动 overlay2 对内核要求更高,老内核上可能遇到挂载 overlay 失败的问题,尤其 16.04 默认 4.4 内核,跑新版 Docker 时偶尔会有莫名其妙的文件系统错误。
另外还要注意 init 系统。Docker 官方 deb 包安装后会注册为 systemd 服务,Ubuntu 16.04 之后都已经切换到 systemd,这一块问题不大。真正需要花心思的是仓库源能不能对上号,以及网络能不能访问到 Docker 官方源。
2. 安装方式选型:别急着复制粘贴
2.1 官方 apt 仓库安装
这是我最推荐的生产环境安装方式。通过官方 apt 仓库安装有几个明确好处:版本可锁定、升级路径清晰、卸载也干净。安装之前,关键一步是正确添加仓库,里面有个陷阱:仓库地址里的系统代号如果写错,后面 apt update 会直接报 404,或者根本找不到对应软件包。
不同 Ubuntu 版本的代号如下:
- 20.04 → focal
- 18.04 → bionic
- 16.04 → xenial
网上很多教程直接让你用$(lsb_release -cs)动态获取,这个命令在大多数情况下是对的,但前提是系统里装了 lsb-release,而且系统代号确实在 Docker 官方仓库支持范围内。如果机器是 14.04,lsb_release 会返回 trusty,但 Docker 官方仓库里已经没有 trusty 目录了,照样报错。
所以我建议在动手前先执行一条命令确认系统代号:
cat /etc/os-release重点看VERSION_CODENAME,后续仓库地址要用它来核对。
2.2 离线 deb 包安装
不少生产环境是内网隔离的,无法访问外网,这时候不能直接往 /etc/apt/sources.list.d 里添加源,装到一半也会因为没有网络而失败。离线安装的思路是:找一台与目标机器同系统版本、同架构、能联网的机器,先把所需 deb 包都下下来,然后拷贝到目标机器上安装。
具体操作,联网机器上先添加官方 repo 并执行 apt update:
sudo apt update sudo apt-get install --downloadonly docker-ce docker-ce-cli containerd.io下载好的 deb 包默认在/var/cache/apt/archives/目录里。把目录下相关 deb 包拷到目标机器,然后在目标机器上进入存放目录执行:
sudo dpkg -i *.deb如果 dpkg 报依赖缺失,说明依赖包没有全部下载,需要回到联网机器上把依赖也一并拿下来。这里有个经验:离线安装时不要指望 apt 帮你自动补齐依赖,最好用apt depends docker-ce把依赖树列出来,逐一核对。更简单的方法是在联网机器上用一个干净目录做模拟安装,比如apt install --no-install-recommends --downloadonly并检查缓存目录里是否有全部依赖。
2.3 为什么我不建议生产环境用卷脚本
curl -fsSL https://get.docker.com | sh这种一键脚本确实方便,很多测试环境我本人也这么干过,但生产环境我坚决不用。原因有三点:脚本内容不可审计,每次执行拿到的东西可能不一致;安装版本不受控,没法精确锁定你需要的版本;一旦脚本执行一半失败,残留的源和包状态清理起来费劲,反而比从零开始装还麻烦。
还有一点很现实:很多内网环境根本访问不了 get.docker.com,脚本第一步就挂掉。与其到时候找各种奇奇怪怪的搬运方案,不如从一开始就选择 apt 仓库或离线 deb 包方案,至少整个安装过程可追溯、可复现。
3. 完整安装步骤拆解
3.1 环境检查与依赖安装
登录机器后,先确认系统版本和内核:
cat /etc/os-release uname -r接着更新 apt 索引,安装基础依赖。以 Ubuntu 20.04/18.04 为例:
sudo apt update sudo apt install -y ca-certificates curl gnupg lsb-release这几个依赖各司其职:ca-certificates 负责 HTTPS 证书验证,curl 用来下载 Docker 官方 GPG 密钥,gnupg 用于验证密钥格式,lsb-release 负责提供 lsb_release 命令从而获取系统代号。
在老版本系统上,你还要顺手确认系统里是否已经有 iptables,因为 Docker 网络功能高度依赖 iptables:
iptables -t nat -L -n如果这条命令执行报错,说明 iptables 本身有问题,后面启动 Docker 大概率也会死在网络上。遇到这种情况应该先修复 iptables,而不是急着装 Docker。
3.2 添加 Docker 官方仓库和 GPG 密钥
先建立密钥目录,并下载 Docker 官方 GPG 密钥:
sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod a+r /etc/apt/keyrings/docker.gpg这里解释一下每个动作的意图:install -m 0755 -d是创建 /etc/apt/keyrings 目录并设置权限,确保后续 apt 能读取;管道后接gpg --dearmor是将下载的 ASCII 格式公钥转换成二进制格式,apt 处理二进制密钥文件更稳定;chmod a+r保证系统其他用户也能读,避免权限问题导致 apt 拒绝使用密钥。
添加 apt 仓库:
echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null这段命令里有几个细节值得展开:dpkg --print-architecture会输出 amd64 或 arm64,确保架构正确;signed-by 参数指向刚刚存放的密钥文件;最后的$(lsb_release -cs)就是系统代号,这里需要确认输出的结果在官方仓库中存在。
如果你的网络访问 download.docker.com 很慢或经常超时,可以直接把仓库地址替换成国内镜像站,效果立竿见影。比如使用阿里云镜像站:
curl -fsSL https://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://mirrors.aliyun.com/docker-ce/linux/ubuntu \ $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null注意,启用新仓库后必须重新执行 apt update,否则 apt 不会感知新源:
sudo apt update这一步如果报错,比如 404 或源里找不到路径,多半是系统代号不对,或者源地址不支持当前版本。
3.3 安装 Docker 引擎并配置开机自启
执行安装命令:
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin如果是在 18.04、16.04 上,docker-buildx-plugin和docker-compose-plugin可能在对应源里不存在,导致整体安装失败。这时候就只装核心组件:
sudo apt install -y docker-ce docker-ce-cli containerd.io装完之后确认版本:
docker --version启动服务并设置开机自启:
sudo systemctl enable docker --now systemctl status docker --no-pager看到 active (running) 说明守护进程已经起来了。接着跑一个 hello-world 验证整体链路:
sudo docker run hello-world如果镜像拉取失败,先不要急,可能是网络问题,也可能是没有配置镜像加速,后面章节会讲。
3.4 用户权限与基础配置
默认情况下,只有 root 或者 docker 组成员才能操作 docker 命令。为避免每次敲命令都加 sudo,把当前用户加入 docker 组:
sudo usermod -aG docker $USER然后必须重新登录或者执行:
newgrp docker否则当前会话的组权限不生效,你以为命令不对,实际上只是还没建立新会话。这个细节我在不少机器上踩过,网上很多教程都没提。
此时可以执行docker info看配置概览,确认存储驱动是 overlay2。如果输出里显示 storage-driver 不是 overlay2,而是 vfs 或其他,后面容器运行性能会很差,需要重点关注。
4. 旧版本系统上最常见的问题排查
4.1 添加仓库连接超时或签名验证失败
在旧版本系统上,最容易遇到的是网络问题。常见表现有两个:一个是 curl 下载 GPG 密钥时卡住或超时,另一个是 apt update 时提示无法解析 download.docker.com。
排查思路和解决办法如下:
先确认基础网络连通性:
curl -I https://download.docker.com如果直接失败,换成国内镜像站往往是更快的解决办法。参考方法我在 3.2 节里已经写了,把源地址换成阿里云或中科大等镜像即可。
如果是签名验证失败,提示信息通常类似The following signatures couldn't be verified because the public key is not available。这种情况多半是密钥文件没下载完整,或者路径指向不对。重新部署一遍密钥文件,并确认 apt 源里的 signed-by 路径与实际密钥文件路径一致,基本能解决。
还有一个小概率问题:老系统上的 curl 版本过低,导致 HTTPS 证书验证失败。可以先更新 ca-certificates 再重试:
sudo apt update && sudo apt install -y ca-certificates4.2 安装后 Docker 服务启动不了
这是让人最头疼的反馈,命令行明明安装成功,systemctl start docker 却一直失败。第一步永远是看日志:
journalctl -u docker -n 50 --no-pager日志里比较常见的有两类信息。
第一类是 daemon.json 配置错误。Docker 启动时如果发现 /etc/docker/daemon.json 解析失败,会直接拒绝启动。比如少写一个逗号、多写一个括号,都会造成 JSON 解析错误。遇到这种问题,可以用下面命令先检查 JSON 合法性:
python3 -m json.tool /etc/docker/daemon.json如果没有 Python,就手动检查配置文件结构,确保键值对的引号和逗号都正确。
第二类是 iptables 初始化失败。日志中出现failed to create NAT chain或permission denied,说明 Docker 尝试操作 iptables 时被系统拦截。根本原因是 Ubuntu 20.04 之后系统默认使用 nftables,而 Docker 依赖的是传统 iptables 接口,两者切换和兼容问题经常搞人心态。
我的排查顺序是:先看iptables -t nat -L -n能不能正常输出;再查是否有 nft 规则冲突;必要时重新安装 iptables 或调整 alternatives:
sudo apt install -y iptables sudo update-alternatives --config iptables这一步可以在 iptables-legacy 和 iptables-nft 之间切换,根据系统实际情况选择,改完再重启 Docker。
4.3 容器端口映射和 UFW 防火墙冲突
跑在生产上的老机器,很多都开着 UFW 防火墙。Docker 默认会直接操作 iptables,它发布的端口位于 DOCKER 链中,会绕过 UFW 的 deny 规则。明明 UFW 已经禁用某端口,但从外部照样能访问到容器端口,这就是经典冲突现场。
这个问题最稳妥的解决方式不是关掉 UFW,而是在/etc/ufw/after.rules中编写规则,让 Docker 发布的端口也要经过 filter 校验。更极端的做法是在 daemon.json 里设置"iptables": false,让 Docker 放弃管理 iptables,但这样端口映射和跨主机容器网络都受影响,需要自己维护规则,新手不建议轻易尝试。
我个人在 18.04 上的处理方式是:保留 UFW,在 after.rules 里增加针对 Docker 网桥的过滤,再重启 UFW。整个过程比较复杂,但胜在生产环境安全可控。
4.4 旧系统内核导致的存储驱动问题
在 16.04 或更老的系统上,启动容器时可能遇到类似failed to mount overlay的报错。overlay2 需要内核支持 overlayfs,4.4 内核虽然理论上可以,但某些配置下会出现兼容性问题。
我提供的备选方案有三个:
第一,升级到 HWE 内核。Ubuntu 16.04 可以安装 HWE 内核,比如:
sudo apt install -y linux-generic-hwe-16.04装完重启,uname -r 看到新版本后,overlay2 通常就能用了。
第二,修改 daemon.json 使用 vfs 存储驱动。vfs 不依赖 overlayfs,兼容性最好,但性能很差,镜像和容器占用的磁盘空间也大,只适合临时验证。
第三,建议直接迁移系统或换新版本。老平台硬撑不是长久之计。
5. 安装完成后的优化建议
5.1 用 daemon.json 统一配置
安装完只是开始。Docker 默认配置不会管日志大小,也没配置镜像加速,跑一段时间后宿主机磁盘可能被容器日志占满。我建议安装完成后第一时间创建 /etc/docker/daemon.json:
{ "storage-driver": "overlay2", "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" }, "registry-mirrors": ["https://你的加速地址.mirror.aliyuncs.com"] }各字段含义说明:storage-driver 指定存储驱动,生产环境强烈建议 overlay2;log-driver 和 log-opts 限制单个容器日志文件大小和数量,防止日志撑爆磁盘;registry-mirrors 配置国内镜像加速器地址,不同云平台提供的地址不同,阿里云需要登录容器镜像服务控制台获取专属地址。
配置完成后重启 Docker:
sudo systemctl restart docker重启后执行docker info确认配置生效,重点看 Storage Driver、Logging Driver 和 Registry Mirrors 三行。
注意:daemon.json 语法错误会导致 Docker 启动失败。每次修改文件后,我都先跑一次
dockerd --validate或者用 json.tool 校验,确认无误再重启服务。
5.2 容器资源限制与监控
多容器部署在同一台老机器上时,资源隔离很关键。如果某个容器写死 CPU 或内存,可能拖垮整个宿主机。通过 daemon.json 也可以设置默认资源约束,比如:
{ "default-ulimits": { "nofile": { "Name": "nofile", "Hard": 65535, "Soft": 65535 } }, "pids-limit": 512 }pids-limit 限制每个容器可创建的进程状态数,防止 Fork 炸弹把系统打挂。default-ulimits 设置文件描述符上限,避免运行高并发应用时出现 too many open files。
还可以引入监控命令,快速查看容器资源占用:
docker stats --no-stream这个命令输出所有运行中容器的 CPU、内存、网络和磁盘 I/O 情况。老系统资源本来就有限,监控要尽早配置,等出问题再补救就很被动。
结尾
最后说点自己折腾老版本 Docker 的真实体会。最深的感受是:顺序比命令重要,环境比版本重要。先花几分钟确认系统代号、内核版本、iptables 状态和网络策略,远比装到一半再翻日志省力。网上很多教程默认你用的是新版 LTS,遇到老系统时仓库源、依赖包、存储驱动这些细节全都会变成拦路虎。还有一个我的习惯:装之前会把关键命令提前写成脚本保存到本地,方便批量或重复部署时直接复用,同时每一次安装都留一份操作记录,出问题能迅速定位是环境差异还是人为失误。
如果你手里的系统真的很老,比如连 Docker 官方仓库都已经放弃支持了,那我劝你认真考虑系统升级或迁移业务,老环境里强行堆新组件,后续维护成本会高到让你怀疑人生。这篇【仅供参考】的运维记录希望能让你在旧系统的 Docker 安装道路上少走几步。