本节摘要:docker-compose.yml 是 Compose 的核心,由 services、networks、volumes、configs、secrets 五个顶层块组成,其中 services 是唯一必填的。本节给出一个带逐行注释的最小完整示例,然后逐个拆解 build、ports、volumes、environment、depends_on、restart 等常用键的语义与坑,最后讲清顶层 networks、volumes、configs、secrets 块的写法,让读者拿到一份陌生模板时能流畅读懂。
Compose 文件的顶层结构可以想象成一个五斗柜:services 抽屉放服务定义,networks 抽屉放网络,volumes 抽屉放卷,configs 抽屉放配置文件,secrets 抽屉放密钥。五斗柜里 services 是必填的,其余四个抽屉可以空着——不需要就不声明,Compose 会自动补默认值。写文件时从 services 块开始,其余顶层块按需补齐,这也是新规范推荐的书写顺序。
五个顶层块的职责一句话各说各的:services 定义"跑什么",networks 定义"怎么连",volumes 定义"存哪去",configs 定义"读哪个配置",secrets 定义"密码放哪"。命名上有意做了区分:configs 是明文配置,secrets 是敏感信息,两者在挂载方式上几乎一样,但语义上分开写,安全审计时一眼能看出哪些是敏感项。
先看一张完整的配方卡。这是一个典型的 web 加 API 加数据库三件套,全部注释写在关键行后面:
services: web: image: nginx:1.25 # 拉取指定标签的官方镜像,不用 latest ports: - "80:80" # 宿主机 80 映射容器 80,外网入口 volumes: - ./html:/usr/share/nginx/html # 绑定挂载,改本机文件即生效 depends_on: - api # 先启动 api,再启动 web networks: - frontend api: build: context: ./api # 构建上下文目录 dockerfile: Dockerfile # 指定 Dockerfile 文件名 args: NODE_ENV: production # 构建参数,传给 Dockerfile environment: - DATABASE_URL=postgres://user:password@db:5432/mydb depends_on: db: condition: service_healthy # 等 db 健康检查通过再启动 networks: - backend db: image: postgres:13 volumes: - db_data:/var/lib/postgresql/data # 命名卷,数据不随容器消失 healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s timeout: 2s retries: 3 networks: - backend networks: frontend: driver: bridge backend: driver: bridge volumes: db_data:
这份文件里三层服务各司其职:web 是静态入口,靠绑定挂载直接服务本机 html 目录;api 需要从源码构建,还要求数据库健康后才启动;db 数据进命名卷,并配了健康检查让依赖方可以等待。注意 web 只在 frontend 网络,api 和 db 只在 backend 网络,web 与 api 之间没有直接网络交集——实际项目里如果需要,可以让 web 同时加入 backend,或者让 api 同时暴露到 frontend。这份示例覆盖了本书后续模板八成以上的常见结构,后面的章节会在此基础上按场景增减。
build 有两种写法。简单形式给一个路径,Compose 到该目录找名为 Dockerfile 的文件构建:
services: app: build: ./app
对象形式可以指定更多细节:
services: web: build: context: ./web dockerfile: Dockerfile.dev args: NODE_ENV: development
context 是构建上下文目录,Dockerfile 里所有 COPY 的路径都相对它;dockerfile 换掉默认文件名,常见于区分 Dockerfile.dev 与 Dockerfile.prod;args 传递构建参数,在 Dockerfile 里用 ARG 接收。image 与 build 同时出现时,Compose 构建后会给镜像打上 image 指定的名字,方便复用。
构建相关的两个习惯值得养成:一是 context 别指向包含大量无关文件的大目录,构建上下文会被整个打包发给 Docker 引擎,目录越大构建越慢,配合 .dockerignore 文件把 node_modules、.git 这类内容排除掉,构建速度能快一个量级;二是本地构建的镜像默认没有标签,docker compose ps 之外的镜像列表里会多出一堆无名镜像,建议 build 时同时指定 image 名,让镜像可辨识、可复用。
ports 把容器端口发布到宿主机,最常用的是"宿主机端口:容器端口":
services: web: ports: - "80:80" - "443:443" - "8080:8080"
也可以只写容器端口,让 Docker 自动分配一个随机宿主机端口,配合 docker compose port 查询;还能用范围批量映射:
services: app: ports: - "3000-3005:3000-3005"
这会把宿主机 3000 到 3005 的五个端口一一对应到容器同名端口,适合调试一组端口固定的服务。短格式映射还有一个变体:在宿主机端口前加 IP 前缀,比如 "127.0.0.1:8080:80",只允许本机访问,生产上不想对外暴露管理端口时很实用。
端口映射的典型报错是宿主机端口被占用:up 时报 bind: address already in use,说明 80 端口已被其他进程或其他容器占用。排查顺序是先用 docker compose ps 看是不是自己项目里的容器占着,再用系统命令查端口归属,最后决定改端口还是停掉冲突进程。另外注意映射字符串最好加引号,"80:80" 写成 80:80 在大多数情况下也能解析,但 YAML 对某些形态的解析可能出意外,统一加引号是零成本保险。
volumes 键支持三类挂载:绑定挂载、命名卷、tmpfs。绑定挂载直接连宿主机路径,双向实时同步;命名卷由 Docker 管理,独立于容器生命周期;tmpfs 是内存文件系统,速度最快但重启即失。
services: web: volumes: - ./html:/usr/share/nginx/html # 绑定挂载 - mydata:/data # 命名卷 - type: tmpfs target: /tmp # tmpfs 挂载,仅内存 volumes: mydata:
判断用哪种:需要持久且要备份的用命名卷,比如数据库;开发时改代码要即时生效的用绑定挂载;只放临时缓存、丢了也无所谓的用 tmpfs。
environment 可以用列表和映射两种形式,效果等价:
services: app: environment: - NODE_ENV=production - DATABASE_URL=postgres://user:password@db:5432/mydb - API_KEY=${API_KEY}
列表形式每行一个键值对,映射形式则像字典一样缩进写。${API_KEY} 表示从宿主机环境变量取值,宿主机没有这个变量时 Compose 会留空并给出警告。需要批量注入一批变量时,还可以用 env_file 指定一个文件,文件里每行一个键值对,比 environment 更适合存放大量配置。
depends_on 控制启动顺序,有两种写法。简单列表只保证"先启动",不保证"已就绪":
services: web: image: nginx:latest depends_on: - app
更强的写法带 condition,让服务等依赖方健康检查通过后才启动:
services: web: image: nginx:latest depends_on: app: condition: service_healthy app: build: ./app healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3000"] interval: 5s timeout: 2s retries: 3
数据库"已经启动但还没接受连接"是经典坑:postgres 容器起来了,端口却还没就绪,应用连接失败就退出。解决办法就是给 db 配 healthcheck,让依赖方用 condition: service_healthy 等待。depends_on 还有一个边界要避开:循环依赖。A 依赖 B、B 依赖 A 的写法会让 Compose 无法确定启动顺序,虽然多数实现不直接报错,但行为不可预测。真正的解法是重构服务边界——把相互依赖的逻辑拆开,或者让启动慢的一方自己实现重试连接,而不是用依赖关系硬撑。
restart 定义容器退出后的重启策略,取值四个:no 是默认,不自动重启;on-failure 只在非零退出码时重启;always 无论退出原因都重启,包括 Docker 重启后;unless-stopped 与 always 类似,但手动 stop 后不再拉起。服务器上跑的服务我倾向 always 或 unless-stopped。
其余高频键还有:command 覆盖镜像默认启动命令;entrypoint 覆盖入口点;expose 声明端口但不发布到宿主机;labels 给容器打标签供其他工具识别。这些键和上面讲的 build、ports、volumes、environment、depends_on 一起,覆盖了 90% 模板的阅读需求。
关于 command 与 entrypoint 的关系,值得多说一句:entrypoint 定义容器启动时第一个执行的程序,command 是传给它的参数,两者合起来才是完整的启动命令。镜像一般自带默认 entrypoint,command 的作用是替换默认参数。比如官方 nginx 镜像的 entrypoint 会加载 /etc/nginx/conf.d 下的配置,command 可以追加 -g 参数改运行配置。写配置时如果发现"我改了 command 但启动行为没变",先查镜像的 entrypoint 是不是把命令吃掉了。
Compose 文件是 YAML 格式,语法错误会在启动时报 YAML 解析失败,报错信息往往指向行号但不够直白。三个高频坑先排掉:第一,缩进必须一致,YAML 不允许混用 Tab 与空格,编辑器里把 Tab 展开成空格最稳妥;第二,值里的冒号后必须跟空格,services:web 这种写法直接解析失败;第三,特殊字符要引号,端口映射、带冒号的 URL、带星号的命令都要用引号包起来,否则 YAML 解析器会按自己的规则理解。
验证语法不用等启动:docker compose config 命令会解析并展开整个文件,语法问题在这里一步暴露,而且报错信息比 up 时友好得多。我的习惯是每次改完文件先跑一遍 config 再 up,十秒的检查能省下十分钟的排错。

networks 块声明项目自定义网络。默认驱动是 bridge,也可以标记外部网络:
networks: mynetwork: driver: bridge external_network: external: true
external: true 表示这张网在项目之外已经存在(比如由另一个项目或手工创建),Compose 只负责把服务接进去,不负责创建它。用外部网络可以在两个独立项目之间打通通信,前提是网络已经存在,否则启动报错。
volumes 块声明命名卷,常用配置是 driver 和 external:
volumes: db_data: driver: local named_volume:
driver 默认是 local,在宿主机目录存放卷数据;多机场景可以换 nfs 或 glusterfs 驱动。external: true 同样表示卷已由外部创建,Compose 只引用。
configs 与 secrets 顶层块都从文件读取内容,再挂载进容器:
configs: my_config: file: ./config.txt secrets: db_password: file: ./db_password.txt services: app: image: myapp configs: - source: my_config target: /app/config.txt secrets: - source: db_password target: /run/secrets/db_password
服务里用 source 指定来源,target 指定挂载目标路径。secrets 挂载后内容只出现在容器内存里,不进镜像层,比把密码写进环境变量更安全。注意 configs 和 secrets 由 Swarm 模式演进而来,本地 docker compose 对 file 形式的支持是完整的,日常单机部署也可以放心用。
| 键 | 作用 | 常见写法示例 | 坑 |
|---|---|---|---|
| image | 指定镜像 | postgres:13 | 别用 latest,内容不可控 |
| build | 从 Dockerfile 构建 | build: ./app | context 路径相对 Compose 文件位置 |
| ports | 映射端口到宿主机 | "8080:80" | 只暴露需要的端口 |
| expose | 声明端口不映射 | expose: ["5432"] | 同网络服务才能访问 |
| volumes | 挂载卷或目录 | db_data:/var/lib/postgresql/data | 路径写错会静默建目录 |
| environment | 设置环境变量 | NODE_ENV=production | 值里带特殊字符要引号 |
| depends_on | 控制启动顺序 | condition: service_healthy | 简单列表不保证就绪 |
| restart | 重启策略 | always | 调试期建议 no,防循环重启 |
| command | 覆盖启动命令 | command: python app.py | 会替换镜像默认命令 |
⚠️ volumes 路径是最容易静默出错的地方:冒号左边是宿主机路径或卷名,右边是容器路径。写错宿主机路径时 Compose 不会报错,而是默默创建空目录挂进去,容器里看到的就是空目录,排错半天找不到原因。
💡 把 depends_on 从列表升级成条件:凡是"启动后马上要连数据库"的服务,都值得给数据库配 healthcheck 并用 condition: service_healthy,一次配置,长期免踩"连接被拒"的坑。
文件语法学完,接下来让文件真正跑起来——1.4 节讲 up、down、start、stop、restart 五个命令的行为差异,以及一套典型的开发日操作序列。