FlowingDev

Docker Compose 深度解析:你容器们的“万能遥控器”

学习 Docker Compose 如何使用一个 YAML 文件来管理多容器应用,将复杂的 docker run 命令变成一个单一、可共享的蓝图。

试用工具: Docker Run 转 Compose

一句话概括

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 之后的自然一步,也是通往理解更复杂的应用编排世界的大门。它填补了运行单个容器和运行一个成熟的分布式系统之间的鸿沟。

深入探索

理论搞定,动手试试吧——100% 在你的浏览器中运行。

试用工具: Docker Run 转 Compose