上个月帮 A 同学处理一台 Ubuntu 20.04 的生产服务器,准备在上面装 Docker。他照着网上搜到的一篇教程,一路复制粘贴,结果在apt update这里直接翻车:404 报错刷满屏幕,跟着又来了一串NO_PUBKEY的 GPG 错误。我把那篇教程翻出来看,问题非常明显——仓库地址写的是老镜像源、系统代号对不上、密钥管理方式还是几年前的apt-key老路子。
在 Ubuntu 22.04 以下版本上安装 Docker,表面上是几条命令的事,实际拆开看涉及的东西远比想象中多。18.04、20.04 这两代 LTS 到今天依然有大量存量服务器,很多机器跑着核心业务,不到万不得已是不会动系统级的。Docker 又是一个需要和内核、systemd、iptables 深度配合的组件,哪一个环节没对上,安装后就是一串莫名其妙的坑。
这篇文章我不打算只贴一份命令清单。我会把安装前要摸清的底细、官方源的正确配置方式、apt 源失效的完整排查链路、旧系统上最常见的三大故障,以及装完之后的权限、自启、镜像加速、离线部署、卸载升级这些事,按照我在真实服务器上操作的顺序,一五一十地摊开写。
1. 为什么单独写“22.04 以下版本”,这套安装指南和官方文档差在哪
1.1 存量旧服务器多,升级没那么轻松
很多运维同学手里都压着几台 Ubuntu 18.04 或 20.04 的机器,数据库、内部系统、老业务代码都跑在上面。系统标准支持结束后,业务依然稳如泰山,谁也不敢贸然do-release-upgrade。这种环境下,Docker 作为新的运行时组件被塞进旧系统,是再常见不过的需求。
问题是,网上绝大多数 Docker 安装教程默认你用的是最新 LTS。官方文档写得再清楚,也不会替你想清楚“我的 18.04 该怎么办”“我的 20.04 能不能直接用这条命令”。这两个版本的内核、systemd、cgroup 默认配置都不一样,直接复制最新教程的命令,翻车概率非常高。
1.2 自带源里的 docker.io 和官方 docker-ce 完全是两回事
Ubuntu 自带源里其实有一个叫docker.io的包,很多新手图省事直接apt install docker.io。装完发现能用,但版本落后很多,而且不带docker compose插件,不带 buildx,后续想要 Docker Compose v2 语法还得另配。对生产环境来说,这基本是条死路。
Docker 官方提供的docker-ce仓库则会持续跟进新版本,包含docker-ce-cli、containerd.io、docker-buildx-plugin、docker-compose-plugin这一整套。我要讲的就是这条官方路线,而不是 Ubuntu 自带的“阉割版”。
1.3 教程失效的根源:GPG 密钥与仓库配置方式早已换代
旧教程里最常见的写法是这样:
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"这套写法放在 Ubuntu 16.04、18.04 时代没问题,但现在的apt已经明确弃用apt-key。新的推荐做法是把密钥单独存到/etc/apt/keyrings/docker.gpg,然后在仓库配置里用signed-by指定它。这样某个仓库的密钥只验证该仓库,不会因为一把全局密钥泄露导致所有源都不安全。
另外一个隐蔽的坑是:部分第三方镜像站只同步了新版本系统的仓库,比如只同步 jammy,不同步 focal、bionic。你配好源以后apt update直接 404。这也是我为什么在 1.3 里反复强调:优先用官方源地址,别去抄网上的“加速源”。
2. 安装前先把你系统的底细摸清楚:版本、内核、残留一个都不能漏
2.1 版本、内核、架构一条一条确认
我每到一个新环境,动手装 Docker 之前,一定会先跑下面四条命令:
cat /etc/os-release lsb_release -a uname -r dpkg --print-architecture为什么要这么干?因为后面的仓库地址里,$(lsb_release -cs)直接决定 apt 会去哪个目录找包。20.04 输出的是focal,18.04 输出的是bionic。如果这条命令因为某些精简系统没装lsb-release而报错,你至少要能从/etc/os-release里认出版本代号。
内核版本决定存储驱动能不能用overlay2。Docker 官方要求内核 3.10 以上,但我的经验是至少 4.0 以上才稳。20.04 默认内核 5.4,18.04 默认 4.15,基本都满足。但是如果你的机器是云主机老模板,内核被裁剪过,后面overlay模块找不到就会出幺蛾子。
架构信息同样重要。绝大多数服务器是 x86_64,也就是amdp --print-architecture会输出amd64。如果是 ARM 机器,后面的仓库配置里arch=字段就要写成arm64。这一步错了,apt update一样会报错。
2.2 清掉历史残留,避免新旧组件打架
很多机器不是“全新安装”,以前可能装过docker.io,或者某个脚本安装的旧版 Docker。不清理干净,后面安装docker-ce时会出现包冲突,尤其是containerd和runc这两个底层组件。
建议先执行一遍:
sudo apt remove docker docker-engine docker.io containerd runc这一步会移除旧包,但默认不会删/var/lib/docker里的镜像、容器和数据卷。所以如果你只是想换个新版本 Docker,这套操作是安全的;如果你打算彻底重来,那装完新版本之后再考虑单独清理数据目录。
2.3 基础依赖四件套,缺一个后面都会出幺蛾子
sudo apt update sudo apt install ca-certificates curl gnupg lsb-release这四个包各管一段:ca-certificates保证下载 https 资源时能完成证书校验,内网镜像环境尤其容易缺它;curl负责拉 GPG 密钥;gnupg负责把密钥处理成 apt 认识的.gpg文件;lsb-release提供lsb_release命令。
我的习惯是提前把这四个包装好,别等到配仓库时才发现缺东西。特别是极简模板,连ca-certificates都可能没装,curl访问 https 直接报证书错误,查起来很费劲。
3. 官方 apt 仓库安装全流程,以及仓库失效的完整排查链路
3.1 官方仓库的逐步配置命令
先把密钥目录建好,再拉取 GPG 密钥转成 apt 能识别的格式:
sudo mkdir -p /etc/apt/keyrings curl -fsSL https://download.docker.com/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://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list如果lsb_release -cs输出异常,就直接手动写死:20.04 写focal,18.04 写bionic。比如:
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu focal stable" | sudo tee /etc/apt/sources.list.d/docker.list接着更新源并安装:
sudo apt update sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin如果你的机器之前装过奇怪的 containerd,有极小概率遇到“containerd.io 与某些包冲突”的情况,先apt remove containerd再装,一般能解决。
3.2 apt update 报 404 的完整排查链路
这是旧系统上最常出现的翻车点。报错长这样:
The repository 'https://download.docker.com/linux/ubuntu focal Release' does not have a Release file.拿到这个报错,不要急着换源。我一般按下面这条路走:
cat /etc/apt/sources.list.d/docker.list,确认仓库 URL 和系统代号写对了。lsb_release -cs,确认当前系统代号到底是focal还是bionic。- 手动探测仓库是否真的存在,比如:
curl -I https://download.docker.com/linux/ubuntu/dists/focal/stable/Release如果返回 200,说明源没问题,那问题大概率出在公司内网的 apt 缓存或者代理镜像上,需要检查代理配置。如果返回 404,说明这个源根本没有这个发行版的目录。
第三步对于“判断这是谁的锅”非常管用。很多教程里的镜像站只同步了 jammy,没有同步 focal,你按教程配完一定 404。遇到这种情况,我的建议是直接切回官方源。
还有一个高频 GPG 报错:
The following signatures couldn't be verified because the public key is not available: NO_PUBKEY ...处理思路也很直接:重新下载 key 并生成/etc/apt/keyrings/docker.gpg,然后确认 deb 配置里的signed-by路径和实际文件路径一致。可以用gpg --show-keys /etc/apt/keyrings/docker.gpg查看密钥指纹,和官方文档列出的指纹对一下,确保不是下载到了残缺文件。
3.3 装完后的第一眼自检
安装完成先别急着跑容器,做一次快速自检:
systemctl status docker然后:
docker version docker info重点看Server Version是否正常、Storage Driver是不是overlay2、Cgroup Driver是systemd还是cgroupfs。如果docker version的 Client 正常但 Server 部分报错,说明 dockerd 没起来,需要去查日志。这一眼扫完,基本能判断安装是否成功了一大半。
4. 旧版本系统上最常踩的三个坑,我把排查过程完整走一遍
4.1 内核模块缺失:overlay 与 br_netfilter
症状很典型:dockerd 能启动,但创建容器时报错,日志里出现error creating overlay mount,或者容器启动到一半就退出了。
原因在旧内核的模块没有自动加载。Ubuntu 18.04 的老内核尤其常见。先确认:
lsmod | grep overlay lsmod | grep br_netfilter如果输出为空,就手动加载:
sudo modprobe overlay sudo modprobe br_netfilter但手动加载是一次性的,重启就没了。我习惯顺手把模块写进/etc/modules-load.d/docker.conf:
overlay br_netfilter创建文件后可以用sudo systemctl restart systemd-modules-load验证是否生效。
与此同时,桥接网络转发相关的内核参数也顺手开一下。新建/etc/sysctl.d/99-docker.conf:
net.bridge.bridge-nf-call-iptables = 1 net.bridge.bridge-nf-call-ip6tables = 1 net.ipv4.ip_forward = 1然后sudo sysctl --system让它生效。这一步做完,后面容器网络的很多怪问题都能少一半。
4.2 cgroup 驱动不一致,dockerd 起不来
另一种常见症状是我在云主机和某些学校机房服务器上碰到过的:dockerd直接启动失败,日志里写着:
failed to start daemon: Devices cgroup isn't mounted这个报错和系统当前的 cgroup 模式直接相关。老系统默认是 cgroup v1,但一些虚拟化模板或者后来升级过 systemd 的环境,挂载状态是残缺的。
排查命令:
mount | grep cgroup ls /sys/fs/cgroup如果/sys/fs/cgroup下根本没有devices目录,说明 v1 挂载不完整。
我的建议是先让 Docker 和 systemd 保持同一个驱动。在/etc/docker/daemon.json里写:
{ "exec-opts": ["native.cgroupdriver=systemd"] }然后重启 Docker:
systemctl restart docker docker info | grep -i cgroup看到Cgroup Driver: systemd就说明对上了。
还有一种情况是系统内核默认走了 cgroup v2,而你的 systemd 版本对 v2 支持不完整,表现为各种奇怪的容器内资源限制失效。Ubuntu 20.04 的 systemd 是 245,对 v2 支持还行;18.04 的 systemd 是 237,对 cgroup v2 支持不完整。遇到这种老系统,可以在 GRUB 内核参数里强制回退 v1:
systemd.unified_cgroup_hierarchy=0改完/etc/default/grub后执行sudo update-grub,重启生效。这套操作对老系统非常见效。
4.3 容器起得来但网络不通,iptables 拦了哪一层
症状是容器能创建、能进入,但ping不通外网,容器之间网络也可能不通。
先查内核转发:
cat /proc/sys/net/ipv4/ip_forward输出是 0 就说明没开转发。临时开启:
sudo sysctl -w net.ipv4.ip_forward=1然后在/etc/sysctl.d/99-docker.conf里持久化写入net.ipv4.ip_forward = 1。
接着看 iptables 的 NAT 规则:
sudo iptables -L -n -t nat | grep docker如果没有任何 docker 相关的链,多半是 Docker 启动时没能接管 iptables,或者 ufw 的 FORWARD 默认策略把容器流量拦了。可以用sudo ufw status看防火墙状态。
这里我要特别提醒一句:不要因为网络不通就把"iptables": false写进 daemon.json。那是给专业网络环境做旁路接管用的,普通用户开了以后只会更乱。正确做法是把 FORWARD 策略放行,或者给 ufw 配置一条针对 Docker 网段的放行规则。
5. 官方脚本与离线 deb 包,两条备用安装路线怎么选
5.1 官方脚本:快是快,但生产环境别赌
Docker 官方给了一个一键安装脚本,命令简单到离谱:
curl -fsSL https://get.docker.com -o get-docker.sh sudo sh get-docker.sh这个脚本会自动检测系统、配源、装依赖、装最新版 Docker。开发机、临时实验环境用它可以省很多时间。
但生产服务器我不推荐这么做。脚本内容随版本更新会变,你今天拉下来的脚本和三个月前可能完全不同;它默认安装的是当前最新版本,版本策略不可控;而且脚本在安装过程中会自己改源,如果你公司内网要求走指定镜像源,脚本反而会打乱你的环境。
5.2 离线 deb 包:隔离网络下的唯一选择
有些服务器在等保环境、内网机房,完全连不上外网。这时候唯一靠谱的路子就是离线 deb 安装。
预先在有网的机器上把包下载好。在已经配好 Docker 官方源的环境执行:
apt download docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin把生成的.deb文件拷贝到离线机器后:
sudo dpkg -i *.deb如果报依赖缺失,用sudo apt -f install修一下,或者缺哪个包就再下载哪个包一起拷过去。这种方式的优点是完全可控,版本、依赖、时间点都能锁定;缺点是需要手动维护依赖链,稍微繁琐。
5.3 三条路线的对比表
| 安装方式 | 网络要求 | 版本可控性 | 依赖处理 | 生产环境推荐度 |
|---|---|---|---|---|
| 官方 apt 仓库 | 需要能访问官方源或内网镜像 | 高,可锁定到具体版本 | apt 自动处理 | 推荐 |
| 官方脚本 | 需要能访问外网 | 低,默认最新版 | 脚本自动处理 | 不推荐 |
| 离线 deb 包 | 完全离线可用 | 高 | 需手动解决依赖 | 隔离网络下推荐 |
6. 装完别急着跑容器:权限组、开机自启、镜像加速和安全底子一次补齐
6.1 把用户加进 docker 组,但先搞清楚它的权限代价
安装完成后,如果你不想每条命令都敲sudo,可以把当前用户加进 docker 组:
sudo usermod -aG docker $USER改完必须重新登录会话,或者执行newgrp docker临时刷新。很多时候用户加完组发现还是要输 sudo,就是因为没重新登录。
但这里我要泼一盆冷水:docker 组的权限等价于 root 权限。能操作 Docker 的用户可以挂载宿主机目录、运行特权容器,基本等于控制了整台机器。多人共用服务器时,不要随手把所有人都加进 docker 组。运维团队建议单独建一个专用账号,只给这个账号授权。
6.2 开机自启,别等重启以后才发现没起来
大部分使用官方 apt 仓库安装的机器,Docker 和 containerd 的服务已经配置了开机自启。但用脚本安装或者老环境升级上来的,有可能没注册。手动确认一遍最稳妥:
sudo systemctl enable docker sudo systemctl enable containerd sudo systemctl start docker如果你克隆过虚拟机模板,也建议检查一下模板里是不是把服务 disable 了,否则每克隆一台机器都要手动启一次 Docker,很耽误事。
6.3 daemon.json 一次配好:数据目录、日志轮转、镜像加速
装完之后,我第一件事通常不是跑容器,而是写/etc/docker/daemon.json。一个常用的基础配置如下:
{ "registry-mirrors": ["https://mirror.example.com"], "data-root": "/data/docker", "log-driver": "json-file", "log-opts": { "max-size": "50m", "max-file": "3" }, "exec-opts": ["native.cgroupdriver=systemd"] }这里的registry-mirrors是镜像加速地址。很多云服务商都提供免费的公共加速地址,一般在你自己的云平台控制台或者镜像站页面里能找到。注意:不要照抄别人配置文件里的地址,别人能用的不一定对你有用。加速地址失效时删除该字段即可,不影响容器运行,只是拉镜像变慢。
>sudo systemctl restart docker docker info | grep -i "docker root dir\|logging driver\|cgroup"
看到输出和配置一致,才算生效。
6.4 顺手完成安全基线检查
有几个基础安全项,我每次装机都会顺手过一遍:
- 不要随便给容器加
--privileged,除非确定业务必须。 - 不要修改
/etc/docker目录的权限,这个目录里存放着证书和关键配置。 - Docker 默认不监听 TCP 端口。如果你需要远程管理 API,别直接暴露 2375/2376 端口到公网,优先考虑走 SSH 隧道或内网受控网络。
7. 验证安装、卸载与升级,把退路留好
7.1 安装结果自检组合拳
配置完成后,按顺序跑三组验证:
docker version docker info docker run --rm hello-worlddocker version确认 Client 和 Server 都正常响应;docker info再核对一次存储驱动、cgroup 驱动、镜像加速是否生效;hello-world则验证了从拉取镜像、创建容器到运行退出全链路都通。
如果hello-world拉取镜像超时,先回头检查registry-mirrors是否写对、docker info里能不能看到镜像加速地址。网络隔离环境里拉不到镜像,就先用脚本或离线包预置几个必要镜像,再测试运行。
7.2 卸载要卸得干净:哪些该删,哪些别手滑
如果哪一天 Docker 服务彻底不想要了,完整的卸载流程是这样的:
sudo apt purge docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin然后清理残留目录:
sudo rm -rf /etc/docker /var/lib/docker /etc/apt/keyrings/docker.gpg /etc/apt/sources.list.d/docker.list强烈提醒一句:/var/lib/docker是镜像、容器、数据卷的存放地点。如果里面还有任何需要保留的容器或数据卷,先备份,再动手删。
7.3 升级 Docker 和跨版本升级系统,顺序不能乱
日常升级 Docker 很简单:
sudo apt update sudo apt install --only-upgrade docker-ce docker-ce-cli containerd.io软件升级默认不会动/var/lib/docker,所以正常情况下镜像和数据卷都还在。但为了稳妥,升级前至少把重要的镜像清单docker images存一份,关键数据卷用docker run --volumes-from的方式打包备份。
如果你准备把系统从 20.04 升级到 22.04,那就更要注意了:升级前确认当前 Docker 版本对目标系统有新版本兼容;升级前对数据盘做快照;升级后用docker info立刻检查 cgroup 驱动、存储驱动有没有变化。跨版本升级系统以后,容器网络最容易出问题,到时候回到第 4 章把 iptables 和内核转发检查一遍就够了。
最后说点个人体会。在旧版 Ubuntu 上装 Docker,问题十有八九不出在 Docker 本身,而是你对系统底子了解多少:内核模块加载了吗,cgroup 是几,iptables 让不让转发。把这几件事先搞明白,再回去看那些安装教程,你会发现它们其实简单得很。
另外一个小技巧:遇到任何装完起不来的怪问题,先执行journalctl -u docker -n 50看日志,这比在搜索引擎里把所有报错复制一遍快得多。日志里那行真正的原因,往往就在最底部。