一句话概括
Docker Compose 是一个工具,它让你用一个单一、人类可读的 YAML 文件来定义和运行多容器应用,而不是用一堆又长又笨拙的 docker run 命令。
它解决了什么问题
起初,我们有 docker run。嗯……还行吧。对于运行单个容器来说,它挺好的。你敲下命令,可能带个端口映射和数据卷,然后“砰”的一声,你的服务就跑起来了。
但当你的应用不止一个服务时会发生什么?如果你有一个 Web 前端、一个后端 API、一个 PostgreSQL 数据库和一个 Redis 缓存呢?突然之间,你不是只运行一个命令了,你得同时应付四个。
你得手动创建一个网络,好让它们能互相通信。你得确保不会不小心把它们都映射到同一个主机端口上。你还得记住每个服务那条又臭又长的 docker run 命令,以及它所有的环境变量和卷挂载。如果你想把这套配置分享给新来的队友,你就得写一个巨长的 README.md,内容基本上就是祈祷他们能把所有东西都正确地复制粘贴。这简直是滋生混乱、拼写错误和那句经典名言“嗯,在我电脑上明明是好的呀!”的温床。
Docker Compose 就是为了驯服这种混乱而生的,它源于一个名叫 Fig 的项目,Docker 在 2014 年收购了它。它的核心思想是从管理单个容器“宠物”转变为编排整个应用的“牧群”。你不再是发布一堆命令式的指令(运行这个,然后运行那个,像这样把它们连起来……),而是编写一个声明式的配置文件(我整个应用栈长这样),然后让 Compose 去处理那些把它变成现实的脏活累活。
底层工作原理
从本质上讲,Docker Compose 是一个翻译官。它把一个描述性的 YAML 文件翻译成一系列发往 Docker 引擎的 API 调用。它是容器管理界的“授人以渔”,而不是“授人以鱼”。
从 docker run 参数到 YAML 键
比方说,你想运行一个 PostgreSQL 数据库。那条 docker run 命令可能看起来像黑客电影里那种天书般的命令行:
docker run --name my-postgres-db \
-e POSTGRES_PASSWORD=supersecret \
-p 5432:5432 \
-v pgdata:/var/lib/postgresql/data \
--restart unless-stopped \
-d postgres:14
这个命令有一个名字、一个用于密码的环境变量、一个端口映射、一个用于持久化数据的命名卷、一个重启策略,并且它在后台运行(-d)。
现在,让我们看看在 docker-compose.yml 文件中等效的 Docker Compose 写法:
version: '3.8' # 指定 Compose 文件的版本
services:
db: # 这是我们给服务起的一个友好名称
image: postgres:14
container_name: my-postgres-db
restart: unless-stopped
environment:
POSTGRES_PASSWORD: supersecret
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata: # 我们在这里声明命名卷
看到它们的对应关系了吗?命令行参数变成了一目了然、井井有条的 YAML 键。这不仅仅是更漂亮了;它还是一个可版本控制、可共享、且出错率低得多的服务定义。
docker run 参数 |
docker-compose.yml 键 |
作用 |
|---|---|---|
postgres:14 (镜像) |
image: postgres:14 |
指定要使用的容器镜像。 |
--name my-postgres-db |
container_name: my-postgres-db |
给容器一个特定的名字。 |
-p 5432:5432 |
ports: ["5432:5432"] |
将主机的端口映射到容器。 |
-v pgdata:/var/lib/... |
volumes: [pgdata:/var/lib/...] |
挂载一个卷用于持久化数据。 |
-e VAR=value |
environment: { VAR: value } |
在容器内部设置环境变量。 |
--restart unless-stopped |
restart: unless-stopped |
定义容器的重启策略。 |
网络的魔法
这才是 Compose 真正大放异彩的地方。如果你在同一个 YAML 文件里再加一个 web 应用服务,像这样:
services:
db:
# ... (上面我们的 postgres 配置)
api:
image: my-cool-api:latest
ports:
- "8000:8000"
environment:
DATABASE_URL: postgresql://user:supersecret@db:5432/mydatabase
depends_on:
- db
当你运行 docker-compose up 时,Compose 会做一个非常聪明的操作:它为你的应用创建一个专属的虚拟网络。db 和 api 这两个容器都会被连接到这个网络上。
现在,api 服务可以使用主机名 db 来连接数据库——这正是我们在 YAML 文件里给服务起的名字!再也不用去查 IP 地址或者用脆弱的 --link 参数了。Compose 在其私有网络内处理了所有的 DNS 解析。你的 web 应用容器就是这样安全可靠地向数据库容器“暗送秋波”(或者说,发送 SQL 查询)的。
Compose 的生命周期
docker-compose 命令就是你整个应用栈的控制面板。
docker-compose up: 这是主命令。它会读取你的docker-compose.yml文件,检查你系统的当前状态,并执行一切必要的操作来让系统状态与文件描述一致。它会创建网络、构建镜像(如果你指定了build指令)、创建卷,并启动所有服务容器。如果你再次运行它,它会智能地检查变更,只重新创建需要更新的容器。docker-compose down:up的好基友。它会停止并移除所有容器,默认情况下还会移除它创建的网络。这就是那个“一键清理”按钮。如果想彻底清理,docker-compose down -v还会移除命名卷。docker-compose ps: 显示由你的 Compose 文件管理的所有容器的状态。docker-compose logs -f: 在一个方便的流中实时追踪所有服务的日志,并按服务用不同颜色区分。调试时的无价之宝。
真实世界的故事
“在我电脑上明明是好的呀”传奇
Devon 刚为公司的分析仪表盘完成一个新功能,这个仪表盘用到了 Node.js 后端和 MongoDB 数据库。在他的笔记本上,一切完美。他推送了代码,然后让一个新员工 Sam 拉取代码并运行。Devon 的 README 里有两条巨大的 docker run 命令。Sam 作为人类,在数据库卷路径上打错了一个字。应用启动了,但什么也保存不了。Sam 花了三个小时调试应用代码,以为 bug 在那里,而 Devon 则坚称代码没问题。最后,他们开了屏幕共享,才发现了那个拼写错误。挫败感爆棚。
教训: 如果他们用了 docker-compose.yml,新员工入职只需要一个 docker-compose up 命令。整个环境——端口、卷、环境变量——都会完全一致,从而消除了一大类配置错误,把半天的调试时间缩短为 30 秒的启动时间。
CI/CD 流水线噩梦
一个 DevOps 团队正在构建一个持续集成流水线来测试他们的微服务应用。这个应用有一个 web 服务器、一个 API 网关、一个用户服务和一个 Redis 缓存。他们的 Jenkinsfile 是一个由 shell 命令堆砌的可怕脚本,里面有四个独立的 docker run 调用和一堆 sleep 30 命令,寄希望于数据库能在 API 尝试连接它之前准备就绪。测试非常不稳定;有时通过,有时因为 sleep 时间不够长而失败。
教训: 他们用 Docker Compose 重写了流水线。Jenkinsfile 被简化为 docker-compose -f docker-compose.ci.yml up -d。他们使用了 depends_on 和 condition: service_healthy 来确保 API 只在数据库通过其内部健康检查后才启动。不稳定的测试一夜之间消失了。Compose 提供了一种可靠的、声明式的方式来定义和启动每次都完全相同的测试环境。
被遗忘的“边车”(Sidecar)
一位名叫 Maria 的开发者用 Docker 容器部署了她的主 Python 应用。几周后,她的经理让她添加详细的性能监控。最好的方法是运行一个 Prometheus exporter 作为“边车”——一个与主应用并存的第二个容器,共享其网络来抓取指标。Maria 用另一条 docker run 命令把它设置好了。但现在,每次她做改动时,都得记住去启动、停止和重启两个容器。有一天,她重启了应用却忘了那个边车,导致他们丢失了一整天的指标数据。
教训: Maria 把她的配置重构到了一个 docker-compose.yml 文件中。她把 Python 应用定义为一个服务,Prometheus exporter 定义为第二个。现在,docker-compose up 会让它们一起启动,docker-compose down 会让它们一起退役。它们被当作一个逻辑单元来管理,本该如此。Compose 让像边车这样的模式变得易如反掌。
常见的错误和陷阱
- 想当然地认为
depends_on会等待服务就绪。 默认情况下,depends_on只等待依赖的容器启动,而不是等待容器里的应用完全初始化并准备好接受连接。你的 API 可能会在数据库准备好接受查询之前就启动了,从而导致崩溃。真正的解决方案是在你的数据库服务的定义中使用healthcheck指令,然后使用带condition: service_healthy的depends_on完整语法。 - 把秘密信息提交到 Git。 把
POSTGRES_PASSWORD: my-secret-password这样的硬编码直接写在docker-compose.yml里实在是太诱人了。千万别这么干。任何能访问你代码仓库的人现在都知道了你的数据库密码。正确的方式是使用一个.env文件(并把它加到.gitignore里),然后在你的 Compose 文件里引用像${POSTGRES_PASSWORD}这样的变量。 - 混淆绑定挂载和命名卷。 一个以
.或/开头的路径(例如./my-app:/app)是绑定挂载,它会映射你主机上的一个目录。这对于开发来说非常棒,因为代码改动能立刻生效。一个只以名字开头的路径(例如db-data:/var/lib/mysql)是 Docker 管理的命名卷。这更适合持久化像数据库这样的数据,因为它平台无关,并且由 Docker 引擎管理。用错了可能会导致权限问题,或者数据不像你想象的那么可移植。 - 用它来搞大规模生产环境。 Docker Compose 对开发、测试甚至小型的单主机生产部署来说都非常出色。但它不是像 Kubernetes 或 Docker Swarm 那样成熟的容器编排器的替代品。它没有内置的多节点集群、自愈、跨节点负载均衡或复杂的滚动更新等概念。得知道它的边界在哪。
为什么你应该关注它
一旦你的应用涉及到不止一个活动部件,你就应该想到 Docker Compose。
- 你在构建一个带有独立数据库的 web 应用?用 Compose。
- 你需要运行你的应用、一个缓存和一个后台工作进程?用 Compose。
- 你想让你的队友用一个命令就能运行整个开发栈?用 Compose。
- 你在建立一个需要为测试启动临时环境的 CI 流水线?用 Compose。
它是本地容器化开发的行业默认标准工具。学习它是在掌握 docker run 之后的自然一步,也是通往理解更复杂的应用编排世界的大门。它填补了运行单个容器和运行一个成熟的分布式系统之间的鸿沟。
深入探索
- Docker Docs: Compose file version 3 reference - 官方规范,所有可用键和语法的权威来源。
- Docker Docs: Overview of Docker Compose - 来自创造者们的高层级介绍。
- YAML Spec - 因为所有的
docker-compose.yml文件都是 YAML,理解其语法(映射、列表、锚点)会非常有帮助。 - Awesome Docker - 一个精选的 Docker 资源列表,其中许多都包含了各种技术栈的教程和
docker-compose.yml示例文件。