跳转到内容

第 13 章 · Docker Compose

一句话:把几个容器的镜像、端口、卷、环境变量和启动顺序写进一个 compose.yaml,一条命令全部起来、一条命令全部删掉。 用它还是用别的:本机开发要同时跑应用、数据库、缓存,或者照着随书文件起服务,用它;只跑一个容器、试一下镜像,直接 docker run(第 12 章);多台机器上的生产集群用 Kubernetes 一类的编排平台,Compose 只管一台机器。 核对版本:Docker Compose v5(随 Docker Desktop 4.94),2026-10

Windows 和 macOS:Docker Desktop 自带 Compose,按第 12 章装好 Docker Desktop 就能用,不用另装。

Linux(Debian/Ubuntu):已按第 12 章添加 Docker 官方 apt 源的,装 Compose 插件:

终端窗口
sudo apt update
sudo apt install docker-compose-plugin

查版本,输出 Docker Compose version v2.x 或 v5.x 都可以,两者用法相同:

终端窗口
docker compose version

命令是 docker compose,中间是空格。带横杠的 docker-compose 是早已停止维护的 v1,不认 compose.yaml 里较新的写法;网上老教程里的 docker-compose up 一律改成 docker compose up。

  • 服务(Service):services: 下的每一项,如 postgres、redis,对应一个容器,写着用哪个镜像、映射哪些端口、挂哪些卷、传哪些环境变量。命令里指定服务都用这个名字,不用容器名。
  • 项目(Project):一个 compose 文件起来的所有东西合称一个项目,名字取自文件顶部的 name:,没写就用目录名。容器、网络、卷的名字都带项目名前缀,随书文件 name: devtools,所以卷叫 devtools_pgdata。
  • 组(Profile):给服务标上 profiles: [db, all],只有命令里写了 --profile db 或 --profile all 它才会启动。没标组的服务每次都启动。随书文件的服务全部标了组,不带 --profile 运行 up 不会起任何服务。
  • 命名卷(Named volume):文件底部 volumes: 段声明的卷,删容器不删卷,数据库数据存在这里。docker compose down 保留它,down -v 才删。
  • 默认网络与服务名:Compose 自动建一个 项目名_default 网络,所有服务接在上面,互相用服务名访问,端口用容器里的端口。应用容器连数据库写 postgres:5432,写 localhost 指的是应用容器自己。
  • 依赖与健康检查(depends_on、healthcheck):depends_on 决定启动顺序;只写服务名,Compose 只等对方容器启动,不等数据库真正能连。给数据库加 healthcheck,再在应用里写 condition: service_healthy,应用就会等到数据库通过检查才启动。

下图是随书 compose.yaml 的全部服务,按组分开,节点里是宿主机端口和挂载的卷或目录。所有服务都在 devtools_default 网络上,端口只绑定 127.0.0.1;--profile all 就是四组全起。

随书 compose.yaml 的服务:cache、db、mq、web 四组,各自的宿主机端口和卷

命令要在 compose.yaml 所在目录运行,或用 -f 指定文件。随书文件的服务都在组里,表中涉及全部服务的命令都带 --profile all;指名某个服务的命令(logs redis、exec redis)不用带,Compose 会自动启用它所在的组。

任务 命令 说明
启动与停止
后台启动全部服务 docker compose up -d 只起没标组的服务;有改动的服务会被重建
按组启动 docker compose --profile db up -d 多个组写多个 --profile,如 --profile db --profile cache
启动全部组 docker compose --profile all up -d 随书文件里 all 包含每个服务
只起一个服务 docker compose up -d redis 指名的服务不受组限制
启动并等健康检查通过 docker compose up -d --wait 脚本里用,服务都健康了命令才返回
停止和删除容器、网络 docker compose --profile all down 卷保留,数据还在;不带 --profile 只处理没标组的服务
连卷一起删 docker compose --profile all down -v 数据库、Redis 的数据全部删除,无法恢复
只停不删 docker compose --profile all stop 容器还在,start 原样再起
把停了的再起来 docker compose --profile all start 不会应用 compose 文件的改动,改动要 up -d
重启一个服务 docker compose restart redis 只重启进程,不读改过的 compose 文件
查看
列出本项目的容器 docker compose ps 看状态、健康检查结果和端口映射
连已停止的也列出 docker compose ps -a 状态列写着退出码
跟踪一个服务的日志 docker compose logs -f redis Ctrl+C 退出;不写服务名就是全部服务混在一起
只看最后 100 行 docker compose logs --tail 100 postgres 服务刚起就退出时先看这里
看容器里的进程 docker compose top 每个服务的进程列表
看本机所有 Compose 项目 docker compose ls 忘了项目在哪个目录时用
看合并后的完整配置 docker compose --profile all config 变量已替换、override 已合并,YAML 写错会在这里报出来
只列服务名 docker compose --profile all config --services 核对服务名拼写
进入与执行
在运行中的服务里执行命令 docker compose exec redis redis-cli 默认分配终端,不用加 -it
进服务的 shell docker compose exec postgres bash 精简镜像没有 bash 就用 sh
起一个临时容器执行命令 docker compose run --rm app npm test 用服务的配置新起一个容器,不映射端口,跑完删掉
镜像与构建
拉取最新镜像 docker compose --profile all pull 拉完要 up -d 才会换成新镜像
构建带 build: 的服务 docker compose build 改了 Dockerfile 或依赖后用
构建并启动 docker compose up -d --build 改了代码的日常用法
强制重建容器 docker compose up -d --force-recreate 配置没变也删掉重建
删掉文件里已去掉的服务的容器 docker compose up -d --remove-orphans 改了服务名后旧容器还在跑时用

随书的 compose/ 目录里有 compose.yaml,旁边是 Nginx、Caddy 用的 nginx/default.conf、caddy/Caddyfile 和网页目录 html/,四个文件的全文见附录 C 末尾“随书 compose 文件全文”。文件逐段是这样的:

  • name: devtools:项目名,容器、网络、卷名的前缀。
  • postgres:镜像 postgres:17,POSTGRES_PASSWORD: dev 是 postgres 用户的口令,不给这个变量镜像会直接退出;端口 127.0.0.1:5432:5432,数据在卷 pgdata。
  • mysql:镜像 mysql:8.4,MYSQL_ROOT_PASSWORD: dev 是 root 的口令;端口 3306,数据在卷 mysqldata。
  • mongo:镜像 mongo:8,不设口令;端口 27017,数据在卷 mongodata。
  • redis:镜像 redis:8,command: 打开 AOF 持久化、内存上限 256 MB、满了淘汰最久没用的键(第 25 章);端口 6379,数据在卷 redisdata。
  • rabbitmq:镜像 rabbitmq:4-management,5672 给程序连,15672 是网页管理界面,账号 guest/guest;hostname: rabbitmq 固定主机名(RabbitMQ 的数据目录按节点名区分,主机名变了就读不到原来的数据),数据在卷 rabbitmqdata。
  • kafka:镜像 apache/kafka:4.0.0,用环境变量配成单节点 KRaft(broker 和 controller 在同一进程);监听两个地址,容器之间连 kafka:19092,宿主机连 localhost:9092,客户端以后连的是 broker 公布的地址,端口 127.0.0.1:9092:9092;数据在卷 kafkadata。
  • nginx、caddy:分别映射到 8080 和 8081,:ro 只读挂载各自的配置文件和同一个 html/ 目录,两个浏览器窗口看到的是同一页。
  • 末尾的 volumes::声明六个命名卷,实际卷名是 devtools_pgdata 等。

每个服务的 profiles: 都有自己的组和 all;端口左边的 127.0.0.1: 让局域网里的其他机器连不进来。起服务和连接:

  1. 进入目录:cd compose。
  2. 起缓存组:docker compose --profile cache up -d。
  3. 连 Redis,PING 返回 PONG 就通了,详见第 25 章:docker compose exec redis redis-cli。
  4. 起数据库组,第一次要下载三个镜像:docker compose --profile db up -d。
  5. 确认三个数据库的 STATUS 列都以 Up 开头:docker compose ps。
  6. 连 PostgreSQL,容器里本地连接不用输口令,详见第 21 章:docker compose exec postgres psql -U postgres。
  7. 连 MySQL,提示输入口令时输 dev,详见第 22 章:docker compose exec mysql mysql -uroot -p。
  8. 连 MongoDB,不用口令,详见第 24 章:docker compose exec mongo mongosh。
  9. 起 Web 组,浏览器打开 http://localhost:8080(Nginx,第 16 章)和 http://localhost:8081(Caddy,第 17 章),看到“Web 服务器已经跑起来了”就对了:docker compose --profile web up -d。
  10. 起队列组,浏览器打开 http://localhost:15672 用 guest/guest 登录 RabbitMQ(第 27 章),程序连 localhost:9092 用 Kafka(第 28 章):docker compose --profile mq up -d。
  11. 用完全部停掉,数据留在卷里,下次 up 还在:docker compose --profile all down。

给自己的应用加一个 compose(应用加数据库)

Section titled “给自己的应用加一个 compose(应用加数据库)”

项目根目录已有第 12 章写好的 Dockerfile,应用监听 3000 端口,从环境变量 DATABASE_URL 读数据库地址:

  1. 在项目根目录新建 compose.yaml,内容照抄 13.5 的最小 compose,镜像名、端口、连接串按实际改。
  2. 新建 .env,写一行 DB_PASSWORD=<口令>,并把 .env 加进 .gitignore,口令不进仓库:echo ".env" >> .gitignore。
  3. 检查文件,变量替换后的结果和缩进错误都会在这里显示:docker compose config。
  4. 构建应用镜像并启动,db 通过健康检查后 app 才启动:docker compose up -d --build。
  5. 看状态,db 一行应显示 healthy:docker compose ps。
  6. 应用启动报错时看它的日志:docker compose logs -f app。
  7. 改了代码,只重新构建并重建应用,数据库不动:docker compose up -d --build app。
  8. 跑一次性的任务,如数据库迁移,跑完容器自动删除:docker compose run --rm app npm run migrate。

本机 3306 已被自己装的 MySQL 占用,想把随书的 MySQL 改到 3307,又不想改 compose.yaml:

  1. 在 compose.yaml 旁边新建 compose.override.yaml,内容见下面的片段。Compose 每次自动读取这个文件,合并到 compose.yaml 上。
  2. 看合并结果,mysql 的端口应只剩 3307 一条:docker compose --profile db config。
  3. 重建 MySQL 容器使改动生效:docker compose --profile db up -d。
  4. 确认 PORTS 列是 127.0.0.1:3307->3306/tcp,之后本机程序改连 3307:docker compose ps mysql。
  5. 不想要了就删掉这个文件,再执行一次第 3 步的 up -d。
services:
mysql:
# !override 表示整个替换;不加的话 ports 会和原文件的合并,3306 仍会被占用
ports: !override
- "127.0.0.1:3307:3306"

environment 按变量名合并,override 里写同名变量就会覆盖原值;ports 追加;volumes 按容器内路径合并,同一路径以 override 为准;要整个替换得加 !override。compose.override.yaml 只在本机生效,加进 .gitignore 别提交。

配置文件的位置:

配置项 位置 说明
compose 文件 项目目录下的 compose.yaml 旧名 docker-compose.yml 仍可用;两个都有时用 compose.yaml
本机覆盖文件 同目录的 compose.override.yaml 自动合并,不用 -f 指定
变量文件 同目录的 .env 给 compose 文件里的 ${变量} 提供值
容器的环境变量文件 服务里 env_file: 指定的文件 文件里的变量直接传进容器

值得用的写法:

配置项 写法 效果
name name: myapp 固定项目名,换目录名也不会起出第二套容器和卷
变量替换 ${DB_PASSWORD}、${PORT:-3000}、${DB_PASSWORD:?缺少口令} 值从 shell 环境变量或 .env 取,前者优先;:- 给默认值,:? 没有值就报错退出
env_file env_file: .env 把整个文件的变量传进容器;和变量替换是两回事,.env 的规则见第 15 章
restart restart: unless-stopped 容器崩溃或 Docker 重启后自动拉起,手动 stop 的不拉;默认 no
healthcheck test、interval、timeout、retries 定期在容器里跑一条检查命令,结果显示在 ps 的状态里
depends_on 长写法 condition: service_healthy 等依赖的服务健康了再启动本服务
端口 "127.0.0.1:5432:5432" 只让本机连;只写 "5432:5432" 局域网也能连

变量替换发生在 Compose 读文件时,.env 必须放在 compose 文件所在的目录;env_file 是在容器启动时把变量交给容器里的程序。

最小 compose,应用加 PostgreSQL,口令从 .env 的 DB_PASSWORD 取:

name: myapp # 项目名,容器和卷都以 myapp_ 开头
services:
app:
build: . # 用当前目录的 Dockerfile 构建
ports:
- "127.0.0.1:3000:3000" # 浏览器访问 localhost:3000
environment:
DATABASE_URL: postgres://postgres:${DB_PASSWORD}@db:5432/postgres # 主机写服务名 db
depends_on:
db:
condition: service_healthy # 等 db 健康检查通过再启动
restart: unless-stopped # 崩溃后自动重启
db:
image: postgres:17
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD:?在 .env 里设置 DB_PASSWORD} # 没设就报错,不起空口令的库
volumes:
- dbdata:/var/lib/postgresql/data # 数据放命名卷,down 不删
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -h 127.0.0.1"] # 能接受连接才算健康
interval: 5s # 每 5 秒检查一次
timeout: 3s # 单次检查超过 3 秒算失败
retries: 10 # 连续失败 10 次标为 unhealthy
restart: unless-stopped
volumes:
dbdata: # 声明命名卷,实际名字是 myapp_dbdata

数据库端口没有映射到宿主机,只有同一网络里的 app 能连;本机要用图形客户端连库,再给 db 加一行 ports: ["127.0.0.1:5432:5432"]。MySQL 的健康检查可以写 ["CMD", "mysqladmin", "ping", "-h", "localhost"]。

  • no configuration file provided: not found。 原因:当前目录没有 compose.yaml(或旧名 docker-compose.yml)。处理:cd 到 compose 文件所在目录,如随书的 compose/;或用 docker compose -f <路径>/compose.yaml up -d。
  • 运行了 docker compose up -d,docker compose ps 却是空的。 原因:随书文件的服务全标了组,没启用组的服务会被跳过。处理:docker compose --profile <组> up -d;停和删时同样带 --profile all。
  • 应用日志报 connect ECONNREFUSED 127.0.0.1:5432,数据库明明在跑。 原因:容器里的 localhost 是应用容器自己,不是数据库。处理:连接串的主机改成服务名,如 postgres:5432,端口用容器里的端口。
  • 数据库容器起来了,应用第一次连接仍然失败,重启应用又好了。 原因:depends_on 只写服务名时只等容器启动,数据库还在初始化。处理:给数据库加 healthcheck,应用里写 condition: service_healthy(13.5)。
  • down -v 之后再 up,表和数据全没了。 原因:-v 删掉了命名卷,数据就在卷里。处理:平时只用不带 -v 的 down;删之前先导出,PostgreSQL 用 docker compose exec -T postgres pg_dumpall -U postgres > all.sql。
  • 改了 compose 文件,restart 之后没变化。 原因:restart 和 start 只重启原有容器,不读新配置。处理:docker compose up -d,有改动的服务会被删掉重建;改了 Dockerfile 加 --build。
  • Bind for 127.0.0.1:3306 failed: port is already allocated。 原因:本机已有服务或另一个容器占着这个端口。处理:停掉占用的服务;或用 compose.override.yaml 把宿主机端口改成 3307(13.4),细节见前言和第 12 章。
  • 每条命令都先打出警告 …: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion。 原因:老教程的 compose 文件第一行写了 version: "3.8",这个字段已废弃。处理:删掉 version: 那一行。