第 13 章 · Docker Compose
一句话:把几个容器的镜像、端口、卷、环境变量和启动顺序写进一个
compose.yaml,一条命令全部起来、一条命令全部删掉。 用它还是用别的:本机开发要同时跑应用、数据库、缓存,或者照着随书文件起服务,用它;只跑一个容器、试一下镜像,直接docker run(第 12 章);多台机器上的生产集群用 Kubernetes 一类的编排平台,Compose 只管一台机器。 核对版本:Docker Compose v5(随 Docker Desktop 4.94),2026-10
13.1 安装与版本
Section titled “13.1 安装与版本”Windows 和 macOS:Docker Desktop 自带 Compose,按第 12 章装好 Docker Desktop 就能用,不用另装。
Linux(Debian/Ubuntu):已按第 12 章添加 Docker 官方 apt 源的,装 Compose 插件:
sudo apt updatesudo 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。
13.2 五分钟概念
Section titled “13.2 五分钟概念”- 服务(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 就是四组全起。
13.3 常用命令
Section titled “13.3 常用命令”命令要在 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 |
改了服务名后旧容器还在跑时用 |
13.4 典型工作流
Section titled “13.4 典型工作流”用随书 compose.yaml 起服务并连上
Section titled “用随书 compose.yaml 起服务并连上”随书的 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: 让局域网里的其他机器连不进来。起服务和连接:
- 进入目录:
cd compose。 - 起缓存组:
docker compose --profile cache up -d。 - 连 Redis,
PING返回PONG就通了,详见第 25 章:docker compose exec redis redis-cli。 - 起数据库组,第一次要下载三个镜像:
docker compose --profile db up -d。 - 确认三个数据库的 STATUS 列都以
Up开头:docker compose ps。 - 连 PostgreSQL,容器里本地连接不用输口令,详见第 21 章:
docker compose exec postgres psql -U postgres。 - 连 MySQL,提示输入口令时输
dev,详见第 22 章:docker compose exec mysql mysql -uroot -p。 - 连 MongoDB,不用口令,详见第 24 章:
docker compose exec mongo mongosh。 - 起 Web 组,浏览器打开
http://localhost:8080(Nginx,第 16 章)和http://localhost:8081(Caddy,第 17 章),看到“Web 服务器已经跑起来了”就对了:docker compose --profile web up -d。 - 起队列组,浏览器打开
http://localhost:15672用guest/guest登录 RabbitMQ(第 27 章),程序连localhost:9092用 Kafka(第 28 章):docker compose --profile mq up -d。 - 用完全部停掉,数据留在卷里,下次
up还在:docker compose --profile all down。
给自己的应用加一个 compose(应用加数据库)
Section titled “给自己的应用加一个 compose(应用加数据库)”项目根目录已有第 12 章写好的 Dockerfile,应用监听 3000 端口,从环境变量 DATABASE_URL 读数据库地址:
- 在项目根目录新建
compose.yaml,内容照抄 13.5 的最小 compose,镜像名、端口、连接串按实际改。 - 新建
.env,写一行DB_PASSWORD=<口令>,并把.env加进.gitignore,口令不进仓库:echo ".env" >> .gitignore。 - 检查文件,变量替换后的结果和缩进错误都会在这里显示:
docker compose config。 - 构建应用镜像并启动,
db通过健康检查后app才启动:docker compose up -d --build。 - 看状态,
db一行应显示healthy:docker compose ps。 - 应用启动报错时看它的日志:
docker compose logs -f app。 - 改了代码,只重新构建并重建应用,数据库不动:
docker compose up -d --build app。 - 跑一次性的任务,如数据库迁移,跑完容器自动删除:
docker compose run --rm app npm run migrate。
本机临时改配置,不动原文件
Section titled “本机临时改配置,不动原文件”本机 3306 已被自己装的 MySQL 占用,想把随书的 MySQL 改到 3307,又不想改 compose.yaml:
- 在
compose.yaml旁边新建compose.override.yaml,内容见下面的片段。Compose 每次自动读取这个文件,合并到compose.yaml上。 - 看合并结果,
mysql的端口应只剩 3307 一条:docker compose --profile db config。 - 重建 MySQL 容器使改动生效:
docker compose --profile db up -d。 - 确认 PORTS 列是
127.0.0.1:3307->3306/tcp,之后本机程序改连 3307:docker compose ps mysql。 - 不想要了就删掉这个文件,再执行一次第 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 别提交。
13.5 配置要点
Section titled “13.5 配置要点”配置文件的位置:
| 配置项 | 位置 | 说明 |
|---|---|---|
| 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-stoppedvolumes: dbdata: # 声明命名卷,实际名字是 myapp_dbdata数据库端口没有映射到宿主机,只有同一网络里的 app 能连;本机要用图形客户端连库,再给 db 加一行 ports: ["127.0.0.1:5432:5432"]。MySQL 的健康检查可以写 ["CMD", "mysqladmin", "ping", "-h", "localhost"]。
13.6 注意事项
Section titled “13.6 注意事项”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:那一行。
13.7 深入阅读
Section titled “13.7 深入阅读”- Docker Compose 文档:入门教程、网络、环境变量、多文件合并等专题,先看 How Compose works。
- Compose 文件参考:services、volumes、networks 下每个键的写法,depends_on、healthcheck 的全部选项查这里。
- Compose 命令参考:docker compose 每个子命令的全部参数和示例。
- 用 profiles 按组启动服务:profiles 的写法、多组同时启用、COMPOSE_PROFILES 环境变量。
- Awesome Compose 示例:Docker 官方维护的各种技术栈的 compose 示例,应用加数据库的写法可以照抄。