Ubuntu 24.04 安装 Docker 与 Compose 实战指南

发布时间:2026/10/26 15:43:46
Ubuntu 24.04 安装 Docker 与 Compose 实战指南 上周帮同事在一台刚重装的 Ubuntu 24.04 上装 Docker 和 Docker Compose本以为十分钟能收工结果光 apt update 报签名错误就折腾了快半小时。这类事我碰到太多次了网上搜Ubuntu 安装 Docker 和 Docker Compose能翻出几十种写法有的让你apt install docker.io有的甩一条 curl 管道脚本还有的让你手动下二进制。命令都能跑但装完之后掉不掉链子、版本对不对得上、以后怎么升级差别很大。这篇文章就按我自己的习惯把 Ubuntu 上装 Docker Engine 和 Docker Compose 的完整路径拆开讲每一步在做什么、为什么这么选、哪些地方最容易翻车。不管你是第一次在云主机上搭环境还是想把手上一台老机器重装一遍都能直接照着走。1. 动手之前先把三样东西分清楚很多人安装失败问题不在命令敲错而在一开始就没搞明白自己到底要装什么。Ubuntu 上跟 Docker 沾边的名词有好几个混着用必然出岔子。1.1 服务器场景基本用不到 Desktop 版本Docker 目前主要有两条产品线一条是跑在服务器上的 Docker Engine纯命令行、无图形界面也是绝大多数生产环境和 CI 机器上用的另一条是带图形界面的桌面版主要面向在本地笔记本上做开发的人。你在一台没有桌面环境的 Ubuntu Server 上装桌面版要么装不上要么装上一堆用不到的依赖还多占几百兆内存。我做后端和运维相关的工作服务器上从来只装 Engine。判断标准很简单如果这台机器你不会打开浏览器看网页那就不需要桌面版。桌面版有它适用的场景比如本地开发想直接用图形界面管容器但那是另一码事不应该和服务器安装流程混在一起讲。1.2 docker-compose 和 docker compose 是两代东西这一条坑过的人最多。老版本的 Compose 是一个独立的 Python 程序命令是docker-compose中间带横杠新版本已经改写成 Go 语言实现作为 Docker CLI 的一个插件存在命令是docker compose中间是空格。两者在配置文件语法上大体兼容但命令形式、安装方式、版本号体系完全不同。现在新装环境没有理由再选老的那条路。老版本早就停止维护Python 依赖在某些 Ubuntu 版本上还会跟系统自带的包管理打架。所以你看到某篇教程里让你pip install docker-compose可以直接关掉页面了——那是好几年前的写法。判断自己机器上是哪种跑一句就够docker compose version docker-compose version第一条有输出说明插件版已就位第二条有输出说明还留着老版本建议卸掉避免以后脚本里调用混乱。1.3 Ubuntu 版本和内核这道隐性门槛Docker Engine 对内核有最低要求官方给的门槛不算高但 Ubuntu 16.04 之前的老系统、或者某些被裁剪过的镜像确实会卡住。更现实的问题是Ubuntu 每个长期支持版本对应一个代号官方 apt 源是按代号分目录的代号取错了源地址就是错的apt update 一定报 404。取当前系统代号的标准写法是读系统信息文件. /etc/os-release echo $VERSION_CODENAME这条命令会输出类似jammy、noble这样的词。写源列表时直接引用这个变量比手敲安全得多——手敲拼错一个字母报错信息往往只会告诉你找不到发布文件不会提示你拼错了。还有一个容易忽略的点CPU 架构。绝大多数云主机是 x86_64但近年 ARM 机器越来越多apt 源地址里如果写死了架构换机器就得改。用dpkg --print-architecture动态取值脚本可以一直复用。2. 走官方 apt 源密钥、源列表、安装三段的实操拆解安装 Docker 有三条常见路线Ubuntu 官方仓库里的docker.io包、Docker 官方维护的 apt 源、以及 curl 一键脚本。我基本只用第二条理由说清楚。Ubuntu 仓库里的docker.io胜在省事apt install一条命令完事但版本通常落后官方好几个小版本而且不带 Compose 插件也不带 buildx。一键脚本看着最爽缺点是它跳过包管理器后续升级、卸载、查依赖关系都不太透明出问题时排查成本高。官方 apt 源是折中方案版本新、走 apt 管理、可以正常升级。2.1 先清场残留包和旧版本的处理如果这台机器以前装过 Docker先卸干净。半新半旧的状态是最麻烦的——旧的服务配置还在新的二进制又覆盖上去docker info报出来的信息可能自相矛盾。sudo apt-get remove -y docker docker-engine docker.io containerd runc 2/dev/null这条命令里列的几个包名是历史上不同时期 Ubuntu 仓库里出现过的 Docker 相关包。删掉它们不影响/var/lib/docker里的镜像和容器数据,那部分目录会保留下来。如果你确定旧数据也不要了再手动清理那个目录。我一般会顺手看一眼有没有残留的 systemd 服务systemctl list-unit-files | grep -i docker如果还有docker.service处于 enabled 状态但对应的包已经删了重启时会报错用systemctl disable关掉。2.2 密钥环目录与签名密钥的正确落位apt 源需要验证包的签名,所以要导入官方公钥。传统的做法是用apt-key add,把密钥加到全局信任列表里。这个做法从 Ubuntu 20.04 之后就被标记为过时了,新的规范是把密钥单独放在/etc/apt/keyrings/目录下,在源列表里用signed-by精确指定用哪把钥匙。这么做的好处是隔离:某个源使用的密钥只对那个源生效,不会污染全局。所以第一步是确保目录存在并设置正确权限:sudo apt-get update sudo apt-get install -y ca-certificates curl gnupg sudo install -m 0755 -d /etc/apt/keyringsinstall -d是递归创建目录的写法,-m 0755顺手把权限设好,比mkdir加chmod两条命令更省事些。然后是下载并转换密钥格式:curl -fsSL https://download.docker.com/linux/ubuntu/gpg \ | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod ar /etc/apt/keyrings/docker.gpg这里有两个细节值得说。--dearmor是因为官方提供的是 ASCII armored 格式(文本格式),apt 需要的是二进制格式。chmod ar是为了让所有用户可读——如果权限设得太紧,非 root 用户执行 apt 相关操作时会因为读不到密钥而报权限错误,这个错误信息相当隐晦,很多人第一次遇到完全摸不着头脑。2.3 源列表写法与 update 报错怎么对上号源列表文件放在/etc/apt/sources.list.d/下,自己新建一个docker.list。我倾向于用变量拼接的方式生成,减少手误:echo deb [arch$(dpkg --print-architecture) signed-by/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release echo $VERSION_CODENAME) stable \ | sudo tee /etc/apt/sources.list.d/docker.list /dev/null方括号里的两部分是必须的:arch限定架构,signed-by指明验签用哪把钥匙。少了后者,apt 会退回去找全局密钥,在新系统上直接报没有公钥可用。写完立刻sudo apt-get update。这一步的报错信息对应关系我总结成了一张表,基本能覆盖九成情况:报错关键词大概率原因处理方向NO_PUBKEY密钥没导入或路径不对检查signed-by路径与文件是否存在404 Not Found系统代号写错重新取VERSION_CODENAMEMalformed entry源列表语法错,比如少括号直接看文件内容逐字符核对Release file is not valid yet系统时间不对校准时间后重试最后一条特别隐蔽。云主机刚开机时如果没配好时间同步,系统时间可能落后几分钟,apt 会认为仓库的签名时间在未来,从而拒绝。date看一眼,不对就sudo timedatectl set-ntp true。2.4 安装与第一次验证:hello-world 之外还要看什么源就绪之后安装:sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin五个包各有分工:docker-ce是主程序和服务,docker-ce-cli是命令行客户端,containerd.io是底层容器运行时,docker-buildx-plugin负责多架构镜像构建,docker-compose-plugin就是我们后面要用的 Compose。一次性装齐,省得后面缺东西再来补。装完先看服务状态:sudo systemctl status docker sudo systemctl enable --now dockerenable是设置开机自启,--now表示同时立刻启动。很多人只跑了start,重启机器后发现 Docker 没起来,就是这个原因。接着跑经典验证:sudo docker run --rm hello-world看到那段欢迎文字,说明拉取、解压、运行、清理整条链路都通了。但我不建议就此收工,再补一句sudo docker info,重点看三处:Server Version 确认版本、Storage Driver 确认存储驱动(overlay2 是正常值)、Cgroup Driver 确认是 systemd。最后一项如果不是 systemd,某些场景下容器的资源限制会不生效,是个容易被忽略的隐患。3. 镜像拉不动的处理:加速配置与 daemon.json 的写法hello-world能跑通,不代表拉别的镜像也顺。实际使用中最常见的卡点就是拉取超时,尤其是基础镜像体积大的时候。这里说说配置思路。3.1 daemon.json 每个字段在解决什么问题Docker 的守护进程配置统一放在/etc/docker/daemon.json,JSON 格式。文件不存在就新建:sudo mkdir -p /etc/docker sudo tee /etc/docker/daemon.json /dev/null EOF { registry-mirrors: [https://你的加速地址], log-driver: json-file, log-opts: { max-size: 50m, max-file: 3 }, live-restore: true } EOF四个字段各有用途,我一个个说清楚。registry-mirrors是镜像加速地址列表,填你所在网络环境下可用的加速服务地址即可。填多个会按顺序尝试,某一个不通还能往下走。这里要注意:加速地址只对 Docker Hub 的官方镜像生效,如果你从别的镜像仓库拉,这个配置不起作用。另外地址必须带协议头,只写域名会报格式错误。log-driver和log-opts这组是日志限制。默认情况下容器的标准输出会一直往磁盘写,不做轮转。一个话痨服务跑上几周,把磁盘写满是很常见的事故。限制单文件 50MB、最多保留 3 个,总量封顶 150MB,对绝大多数服务够用。live-restore设为 true,含义是 Docker 守护进程重启时容器继续运行。这个在升级 Docker 版本时特别有用——否则每次升级都要把线上容器全停一遍。3.2 改完配置必须走的那套重载动作JSON 文件改完,daemon.json的语法必须先自查。JSON 不容忍注释、不容忍尾随逗号,少一个引号整份文件都无效。快速检查可以用:python3 -m json.tool /etc/docker/daemon.json能正常格式化输出就说明语法没问题。这一步花三秒钟,能省掉后面看日志找原因的十分钟。然后重载配置并重启服务:sudo systemctl daemon-reload sudo systemctl restart docker注意顺序,先daemon-reload让 systemd 重读服务定义,再restart。有些人直接 restart,大部分情况下也没事,但改了服务单元文件的场景下不 reload 是不生效的。重启后如果有容器本来在跑,检查一下是否恢复正常:sudo docker ps --format table {{.Names}}\t{{.Status}}如果开了live-restore,容器状态应该是持续运行的,不会因为重启守护进程而中断。3.3 判断加速到底有没有生效配置写进去了不等于生效了。判断方法有个小技巧:拉一个镜像,看输出里的拉取地址。如果第一行显示的是你配置的加速域名而不是默认仓库地址,说明配置起作用了。另一种方式是看守护进程实际的运行参数:sudo docker info | grep -A 2 Registry Mirrors有输出说明配置被读进去了。如果这里是空的,回去确认文件路径是不是/etc/docker/daemon.json,以及 restart 之后有没有报错。顺便提醒一个反直觉的点:加速配置只在拉取时生效,已经拉到本地的镜像不受影响。所以如果你发现某个镜像拉得慢,配了加速之后要用docker pull重新拉一次才能验证效果,不能拿本地已有的镜像做判断。4. 免 sudo 与安全边界:docker 用户组到底该不该加装完之后每条 docker 命令都要敲 sudo,确实烦。于是就有了把当前用户加进 docker 组的做法。这件事该做,但要知道代价。4.1 加组的机制和它换来的东西Docker 守护进程默认监听一个 Unix socket,路径是/var/run/docker.sock,这个文件的所有者是 root,所属组是 docker。把用户加进 docker 组,就等于给了这个用户直接读写该 socket 的权限。sudo usermod -aG docker $USER-aG里的a是 append,表示追加而不是覆盖。少了这个a,用户的其他附加组会被清空,可能连带把 sudo 权限也弄丢,那就麻烦了。命令执行后当前终端不会立刻生效,因为组成员关系是在登录时加载的。三个选择:退出重新登录、执行newgrp docker、或者直接开一个新终端。最稳的是重新登录一次。关键要理解的是:加了 docker 组,本质上等同于给了这个用户 root 权限。因为你可以用docker run -v /:/host -it 镜像名把宿主机根目录挂进容器里任意修改。这不是漏洞,是设计如此。所以这条命令只应该在个人开发机或者你完全信任的单用户机器上执行。多用户共享的服务器上,我的建议是老老实实敲 sudo,或者用权限更细的替代方案。4.2 权限问题的另外几种解法如果机器是多人的,又不想每个人都用 sudo,有几个折中办法。一是给特定命令配 sudo 免密白名单,在/etc/sudoers.d/下加一条,限定只能是 docker 命令。这样用户不需要完整 root 权限,但能跑容器。配置要谨慎,允许docker基本也约等于允许 root,但至少操作有日志可查。二是不走默认 socket,给每个用户单独启动一个守护进程,监听不同的 socket 文件,用DOCKER_HOST环境变量指定。这套方案适合隔离要求高的场景,代价是管理复杂度上去了。对我自己来说,单人开发机就是加组,省事。团队共享机器就统一走 sudo,规矩清楚,谁动了什么都有记录。4.3 目录权限这类连锁问题加完组之后偶尔还会遇到权限错误,典型的是挂载目录写不进去。原因是容器内进程的 UID 和宿主机目录的属主对不上。比如官方 nginx 镜像里工作进程是 nginx 用户,UID 往往是 101 之类,而你宿主机的目录属主是你自己(UID 1000),写操作就会被拒。处理方式有两种。一种是启动时加--user $(id -u):$(id -g),让容器内进程以宿主机当前用户身份运行;另一种是提前把宿主机目录的属主改成镜像内进程的 UID。前者改动小,适合临时跑;后者更干净,适合长期部署。还有个细节:如果你把 Docker 的数据目录改到了别的磁盘,那个目录及其父目录的执行权限必须够,否则守护进程启动时直接失败,日志里报的是permission denied但不会明确告诉你是哪一层缺权限。用namei -l /path/to/dir可以一层层打印权限,排这类问题特别快。5. Docker Compose 的两种装法及版本对齐前面装的那批包里已经带了docker-compose-plugin。但实际环境里不一定这么理想,有的机器是别人装好的,有的系统源里没这个包,所以两种方式都得会。5.1 插件方式的安装与验证插件方式最省心,包管理器管着,升级跟着apt upgrade走:sudo apt-get install -y docker-compose-plugin docker compose version输出里会显示版本号,格式类似v2.x.x。这里要注意命令形式是docker compose,中间空格。如果敲成docker-compose报command not found,不是你装错了,是命令形式不对。插件装的另一个好处是它和 Docker CLI 绑在一起,docker compose能自动读到当前的上下文配置,包括DOCKER_HOST和 TLS 设置。手动装二进制偶尔会因为环境变量没传对连不上守护进程。5.2 二进制方式的安装与校验有些环境里 apt 源不可用,或者你需要固定某个特定版本,那就直接下二进制。流程是下载、赋权、放到 PATH 里:sudo curl -fsSL -o /usr/local/lib/docker/cli-plugins/docker-compose \ https://github.com/docker/compose/releases/latest/download/docker-compose-linux-$(uname -m) sudo chmod x /usr/local/lib/docker/cli-plugins/docker-compose注意目标路径。Compose 作为 CLI 插件,会被放在$HOME/.docker/cli-plugins或者/usr/local/lib/docker/cli-plugins这类固定目录下。放错地方docker compose就找不到,只能当独立二进制用。还有个必须做的动作是校验。下载大文件时网络中断、被中间设备截断,导致二进制损坏的情况并不罕见,表现出来是Exec format error这种莫名其妙的提示。校验办法:docker-compose version跑不起来就说明文件有问题,重新下。另外注意架构标识,uname -m在 x86 上返回x86_64,但下载地址里用的标识是x86_64,ARM 上返回aarch64,对应文件也是aarch64,两边能对上。如果你手动拼地址,别把这两个搞混。5.3 compose 文件格式与引擎版本的配合Compose 文件开头可以写version:字段,但这几年这个字段已经被弱化了——新版本会提示它已过时,建议直接省略。文件叫compose.yaml或compose.yml,新版本优先识别这两个名字;如果还叫docker-compose.yml,也能用,只是官方倾向新名字。真正要留意的是功能特性对版本的要求。比如depends_on的condition: service_healthy写法、profiles、develop这些字段,都需要较新的 Compose 版本才支持。反过来,如果 Compose 版本太新而引擎版本太旧,某些能力也调不起来。我给自己定的原则是:一台机器上 Engine 和 Compose 都从同一条官方源安装,版本自然匹配,不用操心对齐问题。混合安装来源才容易出这类麻烦。6. 用一个小项目把整套环境跑通装完之后最好别只看版本号,拿一个真实的多容器项目验一遍,能提前暴露网络、卷、健康检查这几类问题。6.1 compose.yaml 逐字段说明建一个空目录,写一个最简的compose.yaml:services: web: image: nginx:1.27-alpine ports: - 8080:80 volumes: - ./html:/usr/share/nginx/html:ro depends_on: - cache restart: unless-stopped cache: image: redis:7-alpine command: [redis-server, --appendonly, yes] volumes: - redis-data:/data restart: unless-stopped volumes: redis-data:逐项说。services下每个键是一个服务名,也是容器名的前缀。ports的写法是宿主机端口:容器端口,引号建议加上,避免 YAML 把8080:80解析成六十进制数字——这个坑很老但一直有人踩。volumes有短横线开头的是绑定挂载,写的是宿主机相对路径和容器内路径,末尾:ro表示只读。不带./前缀的是命名卷,由 Docker 管理,实际落在宿主机的数据目录下。命名卷的好处是不依赖宿主机路径,迁移时更省心。depends_on只保证启动顺序,不保证依赖的服务真的准备好了。Redis 启动很快所以问题不大,但换成数据库就要配合健康检查,否则应用容器可能在数据库还没接受连接时就启动失败,然后反复重启。restart: unless-stopped表示除了手动 stop 的,其他情况都自动拉起。机器重启、进程崩溃都能恢复,是我默认会加的配置。6.2 起停、日志、清理的常用组合mkdir -p html echo h1ok/h1 html/index.html docker compose up -d docker compose ps-d是后台运行。ps看状态,重点看 STATUS 那列有没有Up和健康标识。浏览器访问宿主机的 8080 端口,看到 ok 说明链路通了。如果打不开,先看日志:docker compose logs -f web docker compose logs --tail 100 cache-f是持续跟踪,--tail限定行数。查日志时注意时间戳,如果容器时间和你本地差很多,说明时区没对,虽然不影响功能但排查问题时会误导判断。停止和清理:docker compose stop docker compose down docker compose down -vstop只停容器,down连网络一起删,加-v会把命名卷也删掉。最后这条要格外小心——卷里可能是数据库数据,-v下去就没了。我在脚本里从来不给down加-v,要清数据就手动确认后再执行。6.3 一份高频报错对照表跑这个 Demo 的过程中,常见的报错和原因我列一下,基本能覆盖新手阶段的大部分场景:报错或现象可能原因排查动作port is already allocated宿主机端口被占用ss -lntp | grep 8080找占用进程permission denied while connecting用户不在 docker 组重新登录或临时用 sudono such file or directory (挂载)宿主机路径不存在先创建目录,注意相对路径基准是 compose 文件所在目录network has active endpoints有容器还在用该网络先down再重建manifest unknown镜像 tag 写错去镜像仓库确认 tag 存在context deadline exceeded拉取超时检查加速配置与网络表格里第一条尤其常见。我见过不止一次,有人本地开着某个开发服务占了 8080,然后以为 Docker 装坏了。遇到端口问题先查占用,别急着怀疑安装。7. 出问题时的定位顺序:我习惯从下往上排装 Docker 这件事,失败原因分布在好几个层次。乱试只会越弄越乱,我有一套固定的排查顺序,从底层往上走。7.1 先确认 apt 这一层是干净的所有问题的起点都应该是sudo apt-get update。这条命令如果有报错,后面的安装全是空中楼阁。GPG 错误、404、格式错误,前面表格里都列过对应处理。有个细节值得强调:如果 update 报的是某个无关源的错误,比如某个第三方软件源挂了,它会阻断整个 update 流程,导致你新加的 Docker 源也没被处理。这时候可以先临时把那行注释掉,确认 Docker 源本身没问题,再回头处理别的。另外,apt-get install报依赖冲突时,apt-cache policy 包名能看到各个源提供的版本和优先级。有时候是多个源提供同一个包,优先级没配好,装上去的是旧版本。7.2 分清服务层和容器层的边界服务没起来和容器跑不起来,是两类完全不同的问题,排查工具也不一样。服务层的问题看systemctl status docker和journalctl -u docker -n 100 --no-pager。常见的有:配置文件语法错误导致启动失败、数据目录权限不对、端口或 socket 冲突。日志里通常会直接指出是哪个文件第几行,照着改就行。容器层的问题看docker ps -a和docker logs 容器名。ps -a能看到已退出的容器和退出码,退出码 137 一般是内存不足被杀,1 是应用自己报错退出,125 是 Docker 自身参数错误。这几个码记住,判断方向能快很多。还有一层是网络层。容器内访问不到外网,先看docker network ls和docker inspect 容器名 | grep -A 5 NetworkSettings。默认 bridge 网络的网关和 DNS 配置有问题时,容器内ping域名会失败但pingIP 正常,这就是典型的 DNS 问题。7.3 重启之后不自启的几个来源机器重启后服务没起来,原因通常在这几处。一是docker.service没 enable。前面强调过systemctl enable --now,漏了就补上。二是容器本身没设重启策略,docker run时忘了加--restart,或者 compose 文件里没写restart字段。三是容器依赖的外部资源在开机时还没就绪,比如挂载的网络存储、外部数据库,导致容器启动失败后按策略不再重试。第三种情况比较隐蔽。我的做法是给这类容器配restart: unless-stopped配合健康检查,让它在依赖就绪后自动恢复。健康检查的写法是在服务下加healthcheck段,用test指定检测命令、interval指定间隔。有了健康检查,depends_on配condition: service_healthy才真正有意义,能解决启动顺序带来的竞态。最后分享一个小习惯:装完 Docker 之后,我会把版本信息、源列表内容、daemon.json 内容这三样存一份到笔记里。半年后回来升级或者迁移到新机器,直接对着这份记录走,不用重新回忆当时怎么配的。这个动作花两分钟,省下的是以后排查配置差异的时间——我在这上面吃过的亏,比安装过程本身多得多。