每次帮同事处理 Ubuntu 服务器上的 Docker 问题,我都习惯性会问一句:你是自己手动装的,还是用了个什么脚本?这个问题的答案基本决定了后面半小时是轻松收工还是开始排雷。网上搜“Ubuntu22.04 安装 Docker”,出来的教程确实多,但不少还停留在老一套:一键脚本装完不知道装的是哪个版本、docker-compose和docker compose两个命令混着用、装完拉镜像慢到怀疑人生、容器 network 出问题不知道怎么查。这篇我把自己在 Ubuntu 22.04 上反复验证过的一套安装流程完整写出来,覆盖 Docker 引擎、Docker Compose v2 插件、镜像加速配置,以及我踩过之后整理了排查思路的几个高频报错。无论你是第一次在新机器上装,还是已经被各种错误信息卡到心累,照着这套走基本能避开大部分坑。
1. 装之前先把三件事想清楚:版本、方式和残留
1.1 先确认系统版本和 CPU 架构
任何安装教程的第一步都该是先看清楚自己站在哪块地上。Ubuntu 22.04 的代号是 jammy,这个代号在后面添加软件源时会被直接用上。
lsb_release -a cat /etc/os-release我一般直接看cat /etc/os-release,里面VERSION_CODENAME=jammy这一行比较关键。再看一下 CPU 架构:
uname -mx86_64 对应 AMD64 架构,aarch64 对应 ARM64。国内很多云服务器是 x86_64,但树莓派、飞腾、鲲鹏这些 ARM 机器也不少见。架构直接决定了后面 apt 源里arch=参数的值,所以先确认没有坏处。
内核版本方面,Ubuntu 22.04 默认内核是 5.15,Docker 要求的内核最低版本早就覆盖到了,不用刻意升级内核。不过如果你的机器是从老版本 LTS 一路升上来、内核还停留在 4.x,那先别急着装 Docker,把系统完整升级一遍再继续。
1.2 为什么我不用一键脚本,也不用 snap 版本
你可能会问,Docker 官网上不是有curl -fsSL https://get.docker.com -o get-docker.sh这种一键脚本吗?为什么不用?我用它装过几次,确实快,但有几个让我不太放心的地方:
第一,脚本会自动判断系统然后配置源并安装,整个过程像黑盒,出了问题你很难知道它到底改了什么,卸载的时候更是两眼一摸黑。第二,它安装的版本取决于脚本发布时的指向,可能不是你预期中的那个稳定版本。第三,一键脚本在有些精简版系统上还会顺手改掉一些网络配置,比如iptables的默认策略,装完可能留下一堆你并不希望发生的改动。
snap 版的 Docker 我也试过,它的问题是运行和隔离方式跟传统 systemd 服务有点区别,在部分服务器上会导致 systemctl 管理 docker 的体验很奇怪,而且 snap 包默认的刷新策略有可能会半夜自动升级 Docker 版本,这在生产环境里并不让人省心。
我的选择是:用 Docker 官方维护的 apt 仓库安装。这样装出来的是标准 deb 包,能被apt-get upgrade统一管理,也能用apt remove docker-ce干干净净卸载,版本锁定和回滚都方便。
下面这个表格是三种方式的直接对比:
| 安装方式 | 优点 | 缺点 |
|---|---|---|
| 官方 apt 仓库 | 标准 deb 包,可卸载、可锁版本,和 systemd 配合正常 | 需要多敲几步命令 |
| get.docker.com 脚本 | 一次执行完,看起来省事 | 黑盒式改动,卸载困难,行为不可控 |
| snap install docker | 命令简单 | 自动刷新存在变数,systemd 关联不直观,部分功能受限 |
1.3 清掉历史残留再动手
如果你之前已经用 apt 装过 docker、docker.io、containerd 这类包,先清理掉,不然新老版本会互相干扰。
sudo apt-get remove docker docker-engine docker.io containerd runc如果之前用二进制方式或者手动装过 Docker Engine,还要手动确认一下是否有残留进程:
which -a docker ps aux | grep dockerd这些命令会告诉你当前系统里是不是已经有 docker 可执行文件在跑。如果查到/usr/bin/docker和/usr/local/bin/docker同时存在,一定先处理掉非 apt 的那份,再继续。
有一点必须提醒:除非你已经明确决定放弃所有容器镜像和容器数据,否则别在这个阶段直接删/var/lib/docker目录。这个目录存放所有镜像层、容器层和卷数据,删了就真没了。卸载不解决问题的数据问题,完全可以留到确认不需要时再做处理。
2. 正式安装 Docker 引擎:我固定使用的 apt 流程
2.1 导入 GPG key 并创建 apt 源
二维码没意思,直接讲命令背后的逻辑。Docker 官方 apt 仓库为了保证包安全,所有 Release 文件都用 GPG 签名。我们需要先把官方 GPG key 下载下来转成 apt 能识别的 keyring 文件,再在 sources.list 里通过signed-by=指定它。
sudo apt-get update sudo apt-get install -y ca-certificates curl gnupg lsb-release 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 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 sudo apt-get update逐行说几个细节。
sudo install -m 0755 -d /etc/apt/keyrings这行是很多老教程里没有的。早期 Docker 文档推荐直接用gpg --dearmor输出到/usr/share/keyrings,但为了统一干净,现在官方文档从 22.04 起推荐独立使用/etc/apt/keyrings目录,并设置 0755 权限。如果没有这个目录,后面 curl 管道输出的重定向可能会因为找不到路径失败。
$(dpkg --print-architecture)会自动输出 amd64 或 arm64,这比手工写死amd64稳妥。$(lsb_release -cs)在 22.04 上会输出jammy。如果之前装了 lsb-release,直接可用。
还有一个注意点:如果apt-get update更新官方 Docker 仓库时一直落在download.docker.com上超时,那是这条源的问题,不是后面镜像加速能解决的。这种情况下可以考虑把源地址换成国内的 docker-ce 镜像源,比如清华或阿里云维护的 docker-ce 仓库,这是另一套配置,不影响后面 registry mirror 的设置。不过并不建议一上来就换,很多网络环境下官方源本身是通的。
2.2 一次性安装五个核心组件
源配置好之后,直接装:
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin这里我把 docker-buildx-plugin 和 docker-compose-plugin 一起装了。很多教程只写 docker-ce 和 docker-ce-cli,装完发现docker buildx和docker compose都不存在。
这几个包各自负责什么:
docker-ce:Docker 引擎守护进程,也就是 dockerd。docker-ce-cli:docker 命令行工具。containerd.io:容器运行时,dockerd 底层通过它管理容器生命周期。docker-buildx-plugin:构建镜像的核心插件,负责 BuildKit 支持。docker-compose-plugin:Docker Compose v2 的官方插件,命令是docker compose。
2.3 启动服务并跑通第一个容器
装完立刻启动并设置开机自启:
sudo systemctl enable --now docker sudo systemctl status docker如果一切正常,你会看到服务状态是 active (running)。然后跑一下官方提供的最小测试镜像:
sudo docker run hello-worldhello-world是一个极小镜像,拉取下来后会启动一个容器并打印一段提示,然后退出。看到提示说明守护进程能正常工作。如果卡在 Pulling 那一步,先去看后面第 4 节的镜像加速配置,大概率是网络问题。
3. 权限、开机自启和 Compose v2 插件一次配好
3.1 让普通用户免 sudo 操作 docker
默认情况下,操作 docker 命令需要 root 权限,因为 docker CLI 是通过/var/run/docker.sock与守护进程通信的,这个 socket 的所有者是 root。
执行:
sudo usermod -aG docker $USER把当前用户加进 docker 用户组。然后必须重新登录(注销当前会话或重新连接 SSH),组权限才会生效。验证方式:
id -nG输出里能看到docker就算成功。如果重新登录后 docker 命令还提示权限不足,多半是当前 SSH 会话的组缓存没刷新,可以试试newgrp docker或者干脆重开一个连接。
我不建议通过chmod 666 /var/run/docker.sock或手动改 socket 权限来解决问题,那是把守护进程完全裸露给所有本地用户,安全风险极高。docker 用户组是官方推荐的正规做法,安全边界也清晰得多。
3.2 Docker Compose v2:同一个命令的两种身份
这是最容易踩坑的地方。Docker Compose v2 现在是 Go 语言实现的插件,使用方式是:
docker compose version注意是docker compose,中间有空格,连字符版本是老一代 Python 实现的docker-compose命令。v2 插件已经包含在上一步安装的docker-compose-plugin包里,所以你不需要再去 pip install docker-compose。
我见过不少人在 22.04 上装完 Docker,然后习惯性地curl -L ... docker-compose-Linux-x86_64下载一个 v1 二进制到/usr/local/bin/docker-compose,再继续用连字符命令。这会导致系统中同时存在docker compose和docker-compose两套工具,compose.yaml 文件的字段解析逻辑和不一致问题会在后续逐步暴露。
如果确实还需要旧版兼容,可以用 Compose v1 的独立二进制,但我建议新项目统一用docker compose。验证 v2 插件是否生效:
docker compose version能看到类似Docker Compose version v2.x.x的输出就没问题。
3.3 systemd 开机自启的完整配置
刚才已经执行过systemctl enable --now docker,它的作用是把 docker 服务设置为开机启动并立即启动。
containerd 也一样处理:
sudo systemctl enable containerd虽然 docker 服务在启动时会拉起 containerd,但显式 enable 可以避免某些系统环境下服务依赖不完整的问题。确认状态:
systemctl is-enabled docker systemctl is-enabled containerd两个都是 enabled 就正常。
4. 镜像加速和 daemon.json:装完马上做的一件事
4.1 registry-mirrors 到底怎么配
Ubuntu 22.04 装好 Docker 后,第一件不要拖延的事情就是配置 registry mirror。原因很简单:没有加速的情况下,docker pull拉镜像分分钟超时,尤其是刚接触 Docker 想跑第一个 Nginx 的时候。
Docker 守护进程的全局配置文件是/etc/docker/daemon.json。这个文件默认不存在,需要自己创建。
sudo vim /etc/docker/daemon.json写入:
{ "registry-mirrors": [ "https://docker.mirrors.ustc.edu.cn", "https://hub-mirror.c.163.com" ] }注意,这些公共镜像站地址可能因运营策略变更而失效,实际用时最好确认当前可用的加速地址。如果你用的是云厂商服务器,比如阿里云或腾讯云,可以在容器镜像服务控制台里找到分配给你的专属加速地址,那种个人专属地址一般更稳定。
写 JSON 的时候要特别小心:不能有注释,不能有尾逗号,否则 docker 启动时解析失败,直接起不来。写完后可以用下面命令校验:
python3 -m json.tool /etc/docker/daemon.json4.2 顺手把日志轮转也做了
有一个坑在没有预兆的情况下经常爆:某个容器日志一直在写,把/var/lib/docker所在磁盘塞满,整个服务器所有服务开始报“磁盘空间不足”。所以我会在 daemon.json 里顺手加上日志轮转配置:
{ "registry-mirrors": [ "https://docker.mirrors.ustc.edu.cn", "https://hub-mirror.c.163.com" ], "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } }这表示每个容器的单个日志文件超过 10MB 时自动滚动,最多保留 3 个文件。改完配置重启 Docker:
sudo systemctl restart docker验证配置是否生效:
docker info | grep -A 2 "Registry Mirrors" docker info | grep "Logging Driver"能看到镜像加速地址列表,以及Logging Driver为json-file,就算配置成功。
顺便说一句:daemon.json 里配置镜像加速只影响docker pull对 Docker Hub 的拉取,不会影响前面 apt 源里的 download.docker.com。很多人在 apt update 卡住时以为是这里的问题,这两个是完全不同的链路。
5. 装完之后我最常用的一组命令
5.1 镜像、容器、日志、清理速查
装完 Docker 之后,光有命令不算完,关键是理解每个命令的作用。下面是我实际使用中最常用的一套组合。
| 用途 | 命令 | 说明 |
|---|---|---|
| 查看运行中的容器 | docker ps | 只显示 running 状态的容器 |
| 查看所有容器 | docker ps -a | 包含已退出的容器 |
| 查看镜像 | docker images | 列出本地所有镜像 |
| 拉取镜像 | docker pull nginx | 从仓库拉取 |
| 前台运行测试 | docker run --rm -it ubuntu bash | 用完即删,适合测试 |
| 后台运行 | docker run -d --name web -p 8080:80 nginx | 用 -d 进入后台 |
| 查看日志 | docker logs -f --tail 100 web | 实时滚动最后 100 行 |
| 进入容器 | docker exec -it web bash | 在运行中容器里开一个 shell |
| 停止/启动/重启 | docker stop web/docker start web/docker restart web | 容器生命周期管理 |
| 清理失效容器 | docker rm $(docker ps -aq) | 谨慎使用,会删所有停止容器 |
| 清理悬空镜像 | docker image prune | 只删 dangling 镜像 |
有一个习惯我强烈建议养着:测试容器的时候,只要不需要保留容器状态,就加上--rm。这样容器退出后会自动删除,不会让系统里堆满 Exited 状态的僵尸容器。
docker run里的常用参数也说明一下:-p 宿主机端口:容器端口做端口映射,-v 宿主机目录:容器目录做数据卷挂载,--restart=always设置容器异常退出后自动重启。初次配置服务时,建议先不加--restart=always,跑通命中了再加,避免因为配置错误陷入重启循环。
5.2 第一次跑 Nginx 和 MySQL 的实践样板
跑一个 Nginx 试手,常规操作:
docker run -d --name nginx-test -p 8080:80 nginx curl 127.0.0.1:8080生成出 HTML 页面就是正常的。
跑数据库的时候不一样,MySQL 必须挂载数据卷并设置环境变量:
docker run -d --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=yourpassword \ -v /data/mysql8:/var/lib/mysql \ mysql:8.0启动后先看docker logs mysql8,确认没有报错再考虑用客户端连接。数据卷挂载的意义在于删除容器不会丢掉数据库数据,这点在不上点心的人手里经常被忽略。
6. 高频故障排查:每一条我都走了一遍完整链路
6.1 Cannot connect to the Docker daemon:先分清两类原因
这是新手遇到最多的报错,完整内容是:
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?这个报错就两种可能:守护进程根本没启动,或者当前用户没有访问 socket 的权限。
我的排查顺序是固定的:先看进程状态,再看日志,最后看权限。
systemctl status docker systemctl start docker journalctl -u docker -n 50如果 service inactive,启动它。如果启动失败,journalctl 会给出真正原因。如果服务是 active 的但还是报这个错误,那大概率是权限问题,用id -nG确认自己是否在 docker 组里,或者临时用sudo docker ps验证一下。
还有一个容易忽略的情况:你人在 A 机器上,DOCKER_HOST环境变量却指向了 B 机器的地址,或者残留了 Windows Docker Desktop 环境里的DOCKER_HOST。检查一下:
env | grep DOCKER如果 DOCKER_HOST 被设置了,优先把它清掉再看。
6.2 守护进程启动失败:containerd 冲突、overlay2 和磁盘问题
journalctl -u docker -n 100能看到类似:
failed to start containerd: ... failed to load plugin: ...这种情况先检查 containerd 版本和 Docker 版本是否匹配:
docker version containerd --version如果你是通过 apt 装的,版本一般是自动对齐的。但如果之前系统里有 snap 或二进制残留的 containerd,就可能出现两份 containerd 抢 socket 的情况。
overlay2 存储驱动的问题也经常出现。Docker 默认存储驱动是 overlay2,它要求宿主机文件系统是 ext4 或 xfs。如果/var/lib/docker所在分区是 btrfs、zfs 或者其他特殊文件系统,Docker 可能会自动降级或直接起不来。可以用这个命令确认:
df -T /var/lib/docker文件系统类型不对的情况下,要么换个挂载点要么换文件系统,别硬扛。
磁盘写满也是一个隐蔽原因。df -h看到 100%,Docker 在写镜像或容器层时就会失败。可以先清理日志:
sudo sh -c 'truncate -s 0 /var/lib/docker/containers/*/*-json.log'这是应急手段,长期方案还得靠日志轮转和定时清理。
6.3 容器内 ping 不通外网,但宿主机能上网
这个报错的排查链路比较重要,因为它不是单点问题。我的习惯是先区分是网络不通还是 DNS 不通。
先跑一个临时测试容器:
sudo docker run --rm alpine ping -c 3 8.8.8.8如果 8.8.8.8 能通,说明容器网络和 NAT 没问题,问题出在域名解析上。接着试:
sudo docker run --rm alpine ping -c 3 baidu.com域名不通而 IP 通,就是 DNS 问题。Ubuntu 22.04 默认用 systemd-resolved,宿主机上的/etc/resolv.conf会被链接到 127.0.0.53,容器拿到这个地址后可能无法解析。解决办法是在 daemon.json 里直接指定一个可靠 DNS:
{ "dns": ["223.5.5.5", "8.8.8.8"] }重启 docker 之后,新创建的容器内部 DNS 就会使用这个配置。
如果 IP 也不通,那就是数据链路问题。常见原因有三个:宿主机 net.ipv4.ip_forward 被设为 0;iptables 规则被云平台初始化脚本改动;防火墙软件截断了转发。
依次排查:
sysctl net.ipv4.ip_forward sudo iptables -t nat -L -n | grep -i masquerade sudo systemctl status firewalldnet.ipv4.ip_forward应该是 1,不是的话执行sudo sysctl -w net.ipv4.ip_forward=1,并写进/etc/sysctl.conf永久保存。iptables 的 nat 表里应该有 Docker 创建的 MASQUERADE 规则链,如果规则丢了,重启 Docker 服务通常能重建。
6.4 配置了镜像加速拉镜像还是慢
先确认 daemon.json 写的位置对不对。必须是/etc/docker/daemon.json,不是用户目录,不是 home 下的 docker 文件夹。写完后必须:
sudo systemctl restart docker重启后看:
docker info输出里能直接看到Registry Mirrors列表。如果列表是空的,说明配置没被加载,先检查 JSON 是否合法。我见过不少人把注释写进了 JSON,导致整个文件解析失败。
还要注意一个问题:镜像加速服务也有很多不同的实现,不是每个镜像站都能覆盖所有热门仓库的镜像。万一某个加速源挂了或者同步慢,多配几个备用的,Docker Hub 会按顺序尝试。
6.5 容器日志把磁盘塞满之后的应急与长效
容器日志文件的位置在/var/lib/docker/containers/<容器ID>/*-json.log,这几个文件可能轻松膨胀到几十 GB。
临时清理可以这样:
sudo find /var/lib/docker/containers -name "*-json.log" -type f -size +100M -exec truncate -s 0 {} \;但只治标不治本。长效方案是第 4 节里配置的日志轮转,再加下面两个定期命令:
docker system df docker system prunedocker system df查看磁盘占用分布,docker system prune清理停止的容器、悬空镜像、无用网络。如果连构建缓存的垃圾都要清,加-a,但要注意这会删掉所有未被容器使用的镜像,包括你可能之后还要用到的那些。
7. 现在我在新机器上三分钟装完的流程
这篇文档写到最后,分享点真正实操层面的东西。因为装得多了,我自己已经有一套固定的执行顺序,基本可以在十分钟内完成全套安装和配置:
- 先确认系统和架构,再把旧包清掉。
- 创建
/etc/apt/keyrings目录,导入 GPG key,写入 docker.list。 apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin。- 立刻编辑
/etc/docker/daemon.json,把镜像加速和日志轮转一次性写好。 systemctl restart docker && docker run hello-world。usermod -aG docker $USER,重登验证。
顺序里有一点比较关键:我习惯先把 daemon.json 写好再启动 docker,而不是启动好了再改配置。这样避免第一次启动时用错配置,后面还要重启一次。
另外一个小技巧:把整套步骤写进一个 shell 脚本放到~/bin/docker-setup.sh,新机器或新服务器到手直接执行。但不要在脚本里写死密钥和专属加速地址,这些可以留到执行时替换,否则脚本复制到别的机器上会有兼容问题。
还有一点是我实际踩过坑之后的经验:不要在容器里为了“方便”再套一层 docker。除非你明确需要 docker-in-docker 做 CI 流水线,否则多服务项目直接用docker compose管理就够,嵌套 Docker 会带来网络、存储和权限一连串莫名其妙的麻烦,排查起来非常费神。
这套流程我在干净安装的 22.04 上跑过很多次,也在从 20.04 升级上来的旧系统上跑过,只要装之前肯花 30 秒检查残留,后面基本不会出什么意外。如果你照着一步步操作完还有问题,第一件事永远是看日志:journalctl -u docker -n 100和docker logs YOUR_CONTAINER,这两个命令比任何“万能重启”都更能告诉你真相。