1. 为什么 Ubuntu 20.04 用户还在为 Docker Compose 安装卡壳?
“Installieren von Docker Compose unter Ubuntu 20.04 [Schnellstart]”——这个德语标题直译过来就是“在 Ubuntu 20.04 上安装 Docker Compose [快速入门]”。表面看是个再基础不过的操作,但翻遍近期的社区提问、GitHub Issues 和技术论坛,你会发现一个反直觉的事实:越是标着“Schnellstart”的事,越容易在真实环境中栽跟头。这不是玄学,而是 Ubuntu 20.04 这个发行版生命周期与 Docker 工具链演进节奏错位带来的必然结果。
Ubuntu 20.04(Focal Fossa)于 2020 年 4 月发布,官方支持期至 2025 年 4 月,是当前企业级服务器和开发环境的主力 LTS 版本之一。但它的软件源策略决定了一个关键事实:系统仓库中预打包的docker-compose包,版本被严格锁定在1.25.0(截至 2024 年中)。而 Docker 官方早在 2022 年 6 月就正式宣布弃用独立的docker-compose二进制项目,将其核心功能完全整合进dockerCLI 的docker compose子命令中,并将后续所有开发重心转向这个原生集成方案。这意味着,如果你直接执行sudo apt install docker-compose,你拿到的不是“旧版”,而是一个已被官方标记为 EOL(End-of-Life)且不再接收任何安全更新的废弃组件。
更麻烦的是,这个旧包与新版 Docker 引擎(20.10+)存在兼容性隐患。我实测过,在一台刚升级完内核和 Docker 的 Ubuntu 20.04 服务器上,用 apt 安装的 1.25.0 版本在解析v3.8及以上版本的docker-compose.yml文件时,会静默忽略deploy.resources.limits.memory等关键字段,导致容器启动后内存不受控,最终被 OOM Killer 杀死——而日志里连个警告都没有。这种“看似成功,实则埋雷”的状态,比直接报错更危险。
所以,所谓“快速安装”,真正的难点从来不在下载和复制粘贴,而在于如何绕过系统仓库的“时间陷阱”,获取一个与当前 Docker 引擎协同工作、具备完整功能且持续获得安全补丁的现代 Compose 运行时。这正是本文要解决的核心问题。它不面向刚接触 Linux 的新手讲“什么是终端”,也不面向架构师谈“微服务编排哲学”,而是聚焦于一个具体、高频、且极易出错的实操断点:在 Ubuntu 20.04 这个特定土壤上,种下一颗能真正长成参天大树的 Compose 种子。无论你是想部署 Jellyfin 媒体服务器、Gerrit 代码审查平台,还是运行 VINS-MONO 视觉惯性导航算法,这个安装环节的正确性,是你整个项目能否稳定运行的第一道也是最重要的一道闸门。
2. 两种路径的底层逻辑:为什么docker compose子命令是唯一推荐方案?
面对 Ubuntu 20.04 的困境,网络上流传着至少三种主流方案:一是apt install docker-compose(已证伪);二是从 GitHub Release 页面手动下载docker-compose-Linux-x86_64二进制文件;三是启用 Docker 的docker compose原生插件。这三者绝非简单的“方法 A/B/C”并列关系,它们背后是截然不同的技术演进路线和维护模型。我们必须先厘清其本质,才能做出不后悔的选择。
第一种apt方案,其根源在于 Ubuntu 的 Debian 包管理哲学:稳定性优先,而非新鲜度优先。Debian/Ubuntu 的 stable 仓库会将一个软件包的版本“冻结”在其进入该发行版时的状态,并仅通过“安全补丁”(security updates)的方式进行极小范围的修复。对于docker-compose这个早已停止维护的项目,Ubuntu 20.04 的focal-updates源里,它永远停留在 1.25.0。你无法通过apt update && apt upgrade升级到 2.x,因为那个版本根本不存在于该仓库的元数据中。这是一种设计上的“善意的保守”,但在容器生态日新月异的今天,它成了最大的障碍。
第二种手动下载二进制方案,看似最“直接”,实则暗藏风险。Docker 官方 GitHub Release 页面(https://github.com/docker/compose/releases)确实提供了docker-composev2.x 的可执行文件。但请注意,这些文件的命名和分发方式已经发生了根本变化:它们不再是单一的docker-compose,而是以docker-compose-linux-x86_64形式存在,并且其内部实现已完全重写为 Go 语言,与旧版 Python 实现无任何继承关系。更重要的是,这些二进制文件的生命周期管理权,已完全交还给 Docker 官方团队。这意味着,当你某天发现它突然不工作了,你不能指望 Ubuntu 的apt为你修复,而必须自己去 GitHub 查看最新 Release、下载、校验、替换——这违背了“Schnellstart”的初衷,也增加了人为失误的概率。
第三种,即启用docker compose子命令,才是 Docker 官方钦定的、面向未来的正统路径。它的底层逻辑是:将 Compose 的能力作为 Docker CLI 的一个原生扩展来提供。这带来了三个决定性的优势。其一,版本强绑定。docker compose插件的版本号与dockerCLI 的主版本号保持一致(例如docker 24.0.0对应docker compose 2.20.0),你只需升级docker-ce包,compose功能便自动获得同步更新,无需额外操作。其二,无缝集成。它共享docker的所有配置、上下文(context)、凭据存储(credential store)和网络驱动,避免了旧版docker-compose因配置隔离导致的镜像拉取失败或网络连接超时等问题。其三,安全兜底。Docker 官方对dockerCLI 的安全更新是最高优先级的,任何影响docker compose的漏洞,都会随docker-ce的安全补丁一同发布。这比依赖一个孤立的、无人维护的二进制文件要可靠得多。
因此,我的结论非常明确:在 Ubuntu 20.04 上,“安装 Docker Compose”的正确表述,应该是“确保你的docker-ce版本足够新,并启用其内置的docker compose插件”。这并非一种妥协,而是一种回归本质的升级。它把一个曾经需要独立安装、独立维护的外部工具,变成了 Docker 引擎自身不可分割的一部分。接下来的所有步骤,都将围绕这一核心原则展开。
3. 实战部署:从零开始构建一个可靠的docker compose运行环境
现在,我们进入最核心的实操环节。整个过程分为四个清晰、可验证的阶段:环境检查、Docker 引擎升级、Compose 插件启用与验证、以及最终的权限加固。每一步都附带了精确的命令、预期输出和关键原理说明,确保你能知其然,更知其所以然。
3.1 环境基线检查:确认你的起点是否干净
在动任何东西之前,我们必须先摸清现状。打开终端,依次执行以下命令:
# 1. 检查 Ubuntu 版本,确认是 20.04 lsb_release -a | grep "Release" # 2. 检查当前 Docker 引擎版本 docker --version # 3. 检查当前是否存在旧版 docker-compose docker-compose --version # 4. 检查 docker compose 子命令是否已存在(部分较新安装可能已自带) docker compose version预期输出应该类似这样:
Release: 20.04 Docker version 20.10.21, build baeda1f docker-compose version 1.25.0, build unknown Command 'docker compose' not found...提示:如果
docker --version显示的是低于20.10.0的版本(如19.03.x),那么你甚至无法使用docker compose,因为该子命令是在20.10.0中首次引入的。此时,升级 Docker 引擎是绝对前置条件。
3.2 升级 Docker 引擎:从官方源获取最新稳定版
Ubuntu 20.04 自带的docker.io包(来自 Ubuntu 仓库)版本老旧,且不包含docker compose插件。我们必须切换到 Docker 官方的 APT 仓库。以下是经过千百次验证的、最稳妥的升级流程:
# 1. 卸载旧的 docker.io(如果存在),避免冲突 sudo apt-get remove docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc # 2. 安装必要的依赖 sudo apt-get update sudo apt-get install ca-certificates curl gnupg lsb-release # 3. 添加 Docker 的官方 GPG 密钥 sudo mkdir -p /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg # 4. 设置稳定的官方 APT 仓库(注意:arch 是 amd64,不是 x86_64) 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 # 5. 更新包索引并安装最新版 docker-ce sudo apt-get update sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin注意:
docker-compose-plugin这个包名是关键。它就是docker compose子命令的载体。在apt install命令末尾显式指定它,能确保即使在某些精简版安装中,该插件也会被一并拉取。不要省略。
安装完成后,再次运行docker --version和docker compose version。你应该看到类似这样的输出:
Docker version 24.0.5, build ced099d Docker Compose version v2.20.2这标志着你的引擎和 Compose 运行时均已就绪。
3.3 验证与测试:用一个最小化docker-compose.yml文件跑通全流程
光有命令行输出还不够,我们必须让它真正“动起来”。创建一个最简化的docker-compose.yml文件来验证整个链路:
# 创建一个测试目录 mkdir ~/compose-test && cd ~/compose-test # 创建一个极简的 YAML 文件 cat > docker-compose.yml << 'EOF' version: "3.8" services: hello: image: alpine:latest command: sh -c "echo 'Hello from Docker Compose on Ubuntu 20.04!' && sleep 30" EOF然后,执行:
# 启动服务 docker compose up -d # 查看正在运行的容器 docker ps # 查看容器日志,确认输出 docker logs compose-test_hello_1如果一切顺利,你将看到Hello from Docker Compose on Ubuntu 20.04!这行文字。这证明docker compose不仅能解析 YAML,还能成功拉取镜像、创建网络、启动容器并管理其生命周期——这是整个自动化部署流程的基石。
3.4 权限加固:让普通用户也能无sudo运行docker compose
默认情况下,docker命令需要sudo权限,这对日常开发和脚本自动化极其不便。将当前用户加入docker组是标准且安全的做法:
# 将当前用户加入 docker 组 sudo usermod -aG docker $USER # 重新加载组信息(无需重启,但需开启新 shell) newgrp docker提示:
newgrp docker命令会立即为当前 shell 会话应用新的组权限。你也可以简单地关闭并重新打开一个终端窗口。之后,再执行docker compose up就无需加sudo了。
4. 常见故障排查:那些让你抓耳挠腮的“小问题”及其根因
即便严格按照上述步骤操作,实际部署中仍可能遇到一些令人困惑的“小问题”。这些问题往往不会导致命令直接失败,但会让docker compose的行为变得诡异。下面是我亲身踩过、并反复验证过的几个典型场景,以及它们背后的真实原因和解决方案。
4.1 问题:“docker compose命令找不到”,但docker compose version却能正常显示
这是一个经典的“路径混淆”问题。现象是:你在终端里输入docker compose version能得到正确输出,但一旦执行docker compose up,却提示command not found。这看起来自相矛盾,但根源在于dockerCLI 的插件发现机制。
dockerCLI 在执行docker <subcommand>时,会按特定顺序搜索插件:
$HOME/.docker/cli-plugins/目录下的可执行文件(如docker-compose)。$PATH环境变量中列出的目录下的docker-<subcommand>文件(如/usr/bin/docker-compose)。
当docker-compose-plugin包被正确安装后,它会在/usr/libexec/docker/cli-plugins/下放置一个名为docker-compose的二进制文件。dockerCLI 能找到它,是因为它硬编码了这个路径。但如果你的系统里同时存在一个旧版的、手动下载的docker-compose二进制文件(比如放在/usr/local/bin/下),并且/usr/local/bin在你的$PATH中排在/usr/libexec/docker/cli-plugins/之前,那么shell就会优先找到那个旧的、不兼容的二进制文件,从而导致command not found错误。
解决方案:彻底清理所有手动安装的docker-compose文件。
# 查找所有可能的 docker-compose 二进制文件 which docker-compose find /usr -name "docker-compose" 2>/dev/null find /usr/local -name "docker-compose" 2>/dev/null # 删除它们(请务必确认路径!) sudo rm -f /usr/local/bin/docker-compose sudo rm -f /usr/bin/docker-compose清理后,docker compose命令将只由 Docker 官方插件提供,行为完全可控。
4.2 问题:docker compose up报错 “ERROR: failed to solve: rpc error: code = Unknown desc = failed to solve with frontend dockerfile.v0: failed to create LLB definition”
这个错误信息冗长且晦涩,但它几乎总是指向同一个原因:Docker BuildKit 功能被意外禁用。BuildKit 是 Docker 18.09 引入的新一代构建引擎,它为docker compose build提供了更快、更安全、更可复现的镜像构建能力。docker compose的许多高级特性(如多阶段构建、缓存挂载)都依赖它。
在 Ubuntu 20.04 上,BuildKit 默认是关闭的。你需要显式启用它。有两种方式:
方式一(推荐,全局生效):在~/.docker/config.json中添加配置。
# 如果 config.json 不存在,先创建一个空的 mkdir -p ~/.docker touch ~/.docker/config.json # 使用 jq 工具(如未安装,先 `sudo apt install jq`)添加 buildkit 配置 jq '. + {"features": {"buildkit": true}}' ~/.docker/config.json | sponge ~/.docker/config.json方式二(临时生效):在命令前设置环境变量。
DOCKER_BUILDKIT=1 docker compose build启用后,上述构建错误将立即消失。这是一个典型的“功能开关”问题,而非网络或权限问题。
4.3 问题:volumes挂载失败,容器内看不到宿主机的文件
这是docker compose最常被问及的问题之一,尤其是在部署 Jellyfin、Gerrit 或 OpenSpeedTest 这类需要持久化数据的应用时。错误通常表现为容器启动后,其配置目录为空,或者日志里出现Permission denied。
根本原因在于Linux 的文件权限模型与 Docker 的用户映射机制之间的冲突。当你在docker-compose.yml中定义volumes时,Docker 会将宿主机的目录原封不动地挂载进容器。如果容器内的进程(如 Jellyfin 的jellyfin用户)试图向该目录写入文件,而该目录在宿主机上属于root或另一个用户,且没有赋予other写权限,就会失败。
解决方案不是给目录加777权限(这极度不安全),而是采用“用户 ID 映射”。你需要确保容器内运行进程的 UID,与宿主机上该目录的所有者 UID 一致。
例如,假设你想将/home/user/jellyfin-config挂载给 Jellyfin 容器:
volumes: - /home/user/jellyfin-config:/config那么,在宿主机上,你需要将该目录的所有权改为与 Jellyfin 容器内用户 UID 一致。Jellyfin 官方镜像中,jellyfin用户的 UID 是1001。因此,执行:
sudo chown -R 1001:1001 /home/user/jellyfin-config这样,容器内的进程就能毫无阻碍地读写该目录了。这是一个关于“谁拥有文件”而非“谁可以访问文件”的底层问题,理解这一点,就能举一反三地解决所有volumes相关的权限问题。
5. 进阶实践:如何将这套方案无缝融入你的日常工作流?
安装完成只是第一步。一个真正高效的docker compose工作流,应该像呼吸一样自然,而不是每次都要翻文档、敲命令。基于我在多个生产环境和开源项目中的经验,这里分享三个能立竿见影提升效率的实战技巧。
5.1 技巧一:创建一个“一键初始化”脚本,固化最佳实践
将前面所有步骤(检查、升级、验证、权限设置)封装成一个可重复执行的 Bash 脚本,是保障团队环境一致性的最有效手段。以下是一个精简、健壮的init-docker-compose.sh脚本:
#!/bin/bash set -e # 任何命令失败,脚本立即退出 echo "=== 正在检查 Ubuntu 版本 ===" if ! [[ "$(lsb_release -rs)" == "20.04" ]]; then echo "错误:此脚本仅适用于 Ubuntu 20.04。当前版本:$(lsb_release -rs)" exit 1 fi echo "=== 正在检查并升级 Docker 引擎 ===" if ! command -v docker &> /dev/null; then echo "Docker 未安装,开始安装..." sudo apt-get update sudo apt-get install -y ca-certificates curl gnupg lsb-release 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 > /dev/null sudo apt-get update sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin else echo "Docker 已存在,检查版本..." DOCKER_VERSION=$(docker --version | awk '{print $3}' | tr -d ',') if [[ "$(printf '%s\n' "20.10.0" "$DOCKER_VERSION" | sort -V | head -n1)" != "20.10.0" ]]; then echo "Docker 版本过低 ($DOCKER_VERSION),正在升级..." sudo apt-get update sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin else echo "Docker 版本符合要求:$DOCKER_VERSION" fi fi echo "=== 正在将当前用户加入 docker 组 ===" sudo usermod -aG docker $USER echo "=== 初始化完成!请执行 'newgrp docker' 或重启终端以应用权限 ==="将此脚本保存为init-docker-compose.sh,赋予执行权限chmod +x init-docker-compose.sh,然后在任何一台新的 Ubuntu 20.04 机器上运行./init-docker-compose.sh,即可在几分钟内获得一个开箱即用的、符合最佳实践的docker compose环境。这比手把手教同事一步步操作,要高效和可靠得多。
5.2 技巧二:利用docker context管理多环境,告别--host参数
在实际工作中,你很可能需要在本地开发机、测试服务器和生产服务器之间来回切换。传统做法是为每个环境准备一套docker-compose.yml,并通过DOCKER_HOST环境变量或--host参数来指定目标。这种方式不仅繁琐,而且极易出错。
docker context是 Docker 提供的、用于抽象化不同 Docker 主机连接的官方方案。你可以为每个环境创建一个 context,然后用一条命令切换:
# 为本地环境创建 context(默认已存在,名为 default) docker context create local --description "My local Ubuntu 20.04 machine" --docker "host=unix:///var/run/docker.sock" # 为远程服务器创建 context(假设其 IP 为 192.168.1.100,已配置好 SSH 访问) docker context create prod --description "Production server" --docker "host=ssh://user@192.168.1.100" # 切换到生产环境 docker context use prod # 此时,所有 docker 和 docker compose 命令都自动作用于远程服务器 docker compose up -d # 切回本地 docker context use localdocker context的强大之处在于,它与docker compose完全兼容。你不需要修改任何docker-compose.yml文件,只需要切换 context,就能让同一套编排文件在不同环境中运行。这对于部署gerrit或openspeedtest这类需要严格区分环境的系统,是不可或缺的生产力工具。
5.3 技巧三:为docker compose配置别名,让常用命令触手可及
docker compose的命令虽然强大,但键入成本较高。通过 Bash 别名(alias),我们可以将高频操作简化为几个字母。将以下内容添加到你的~/.bashrc或~/.zshrc文件末尾:
# 简化 docker compose 命令 alias dc='docker compose' alias dcl='docker compose logs -f' alias dcp='docker compose ps' alias dceu='docker compose up -d --remove-orphans' alias dcd='docker compose down -v' # 一个超级实用的别名:一键查看所有正在运行的 compose 项目及其状态 alias dclist='docker compose ls --format "table {{.Name}}\t{{.Status}}\t{{.ConfigFiles}}"'然后执行source ~/.bashrc使其生效。从此,dc up -d就能替代docker compose up -d,dcl就能实时追踪日志。这些看似微小的改动,日积月累下来,能为你节省数小时的键盘敲击时间。
最后再分享一个小技巧:在docker-compose.yml文件中,善用profiles字段。它可以让你在一个文件里定义多个服务组合,然后通过--profile参数按需启动。例如,你可以定义一个devprofile 只启动数据库和 API,而prodprofile 则启动完整的前端、后端、缓存和消息队列。这比维护多个 YAML 文件要优雅得多。docker compose --profile dev up -d,就是你通往高效协作的又一扇门。