Docker Compose 配置文件(通常为 compose.yaml 或 docker-compose.yml)是声明式定义多容器应用的核心。以下是按功能模块 归纳的常用语法速查手册,基于当前主流的 Compose Specification 标准。
1. 顶层结构概览
yaml
name: my-project # 项目名称(可选,默认取目录名)
services: {} # 【核心】服务定义
networks: {} # 自定义网络
volumes: {} # 命名卷 / 外部卷
configs: {} # 配置管理(Swarm/Compose v2.23+)
secrets: {} # 敏感数据管理
x-common: &anchor # 自定义扩展字段 + YAML锚点(复用配置)
⚠️ 现代 Compose 已不再强制要求
version字段,建议省略以避免混淆。
name
yaml
name: ecommerce-prod
services:
web:
image: nginx
- 解释 :显式指定项目名称。所有容器、网络、卷都会以
ecommerce-prod-为前缀(如ecommerce-prod-web-1)。 - 为什么用:避免在同一目录下运行多个 compose 文件时资源命名冲突;CI/CD 中动态注入项目名实现环境隔离。
- 注意 :若不设置,默认取
compose.yaml所在目录名,重命名目录会导致旧资源 orphaned。
x-common (YAML 锚点)
yaml
x-base-service: &base
restart: unless-stopped
logging:
driver: json-file
options: { max-size: "10m", max-file: "3" }
deploy:
resources:
limits: { memory: 512M }
services:
api:
<<: *base # 展开锚点
image: node:20
worker:
<<: *base
image: python:3.12
deploy:
resources:
limits: { memory: 1G } # 覆盖锚点中的 memory
- 解释 :
&base定义锚点,*base引用,<<:合并映射。worker的deploy会深层覆盖锚点值。 - 为什么用:消除重复配置,修改一处全局生效。比多文件覆盖更轻量,适合单文件内的横向复用。
- 注意 :YAML 锚点是浅合并 ,嵌套对象需手动重新声明整个子块(如上例
deploy),不能只写limits.memory。
2. Services 核心语法(最常用)
2.1 镜像与构建
| 语法 | 说明 |
|---|---|
image: nginx:latest |
指定镜像 |
build: ./app |
从 Dockerfile 构建 |
build.context / dockerfile / args / target |
构建参数细化 |
platform: linux/amd64 |
指定平台架构(Apple Silicon 常用) |
pull_policy: always/if_not_present/never |
拉取策略 |
yaml
services:
app:
build:
context: ./backend
dockerfile: Dockerfile.prod # 指定非默认 Dockerfile
target: production # 多阶段构建的目标阶段
args:
NODE_VERSION: "20" # 传入 ARG
BUILD_DATE: "${BUILD_DATE}" # 引用环境变量
platform: linux/amd64 # Apple Silicon 上强制 x86
pull_policy: if_not_present # 本地有就不拉,节省带宽
- 解释 :
target配合多阶段构建,只打包最终产物;args在构建时可用${ARG_NAME}引用。 - 为什么用 :开发/生产共用一个 Dockerfile,通过
target区分;CI 中缓存镜像层,pull_policy避免重复拉取。 - 注意 :
args中的变量必须在 Dockerfile 中有对应ARG声明,否则静默忽略。修改args会触发重新构建。
2.2 端口映射
yaml
ports:
- "8080:80" # host:container 短格式写法
- "127.0.0.1:3306:3306" # 仅本地访问
- target: 80 #容器端口 长格式写法
published: 8080 #宿主机端口
protocol: tcp # 长语法,更精确
mode: host #Swarm 集群模式下设为 ingress
2.3 环境变量
yaml
environment:
- NODE_ENV=production
- DB_HOST=${DB_HOST:-localhost} # 支持默认值
- DB_PASS=${DB_PASS:?ERROR: DB_PASS is required} # 缺失则报错退出
env_file:
- .env #当只需要指定文件路径、不需要其他选项(如 required)时,可以直接使用字符串简写形式,path:可省略
- path: .env.local #只有当你需要配置 额外选项 时,才必须使用长语法:
required: false # 文件不存在时不报错
- 解释 :
${VAR:?msg}在变量未定义或为空时终止 compose 并打印 msg;required: false让可选配置文件不阻塞启动。 -
- 注意 :
environment优先级 高于env_file;.env文件不支持 shell 展开(如$(cmd)),只做简单替换。
- 注意 :
2.4 存储挂载
yaml
volumes:
- db_data:/var/lib/mysql # 命名卷
- ./config:/app/config:ro # 绑定挂载(只读)
- type: tmpfs # 内存文件系统
target: /tmp
tmpfs.size: 100M
- type: bind
source: ./uploads #宿主机路径
target: /app/uploads #容器内路径
bind:
create_host_path: true # 宿主机目录不存在时自动创建
- 注意 :命名卷由 Docker 管理生命周期,
docker compose down -v才会删除;bind mount 的宿主机路径必须是绝对路径或相对 compose 文件的路径。
2.5 重启与生命周期
yaml
restart: unless-stopped # no / always / on-failure / unless-stopped
stop_grace_period: 30s # SIGTERM 后等待时间
stop_signal: SIGINT # 自定义停止信号
init: true # 注入 tini 作为 PID 1,正确处理信号
- 解释 :
init: true在容器内插入一个轻量 init 进程(tini),负责回收僵尸进程和正确转发信号。 - 为什么用 :Node.js/Python 等应用默认不处理 SIGTERM,
stop_grace_period+init组合确保优雅退出;长时间运行的 worker 必须有unless-stopped防崩溃。 - 注意 :
stop_grace_period超时后会发 SIGKILL 强杀;init会增加约 1MB 镜像体积,但几乎无性能开销。
这三个配置项共同构成了 Docker 容器的优雅退出(Graceful Shutdown)机制 。它们决定了当执行 docker compose stop 或 docker compose down 时,容器内的应用是"安全地保存状态后退出",还是"被暴力杀死导致数据丢失"。
以下是逐项深度解析:
1. stop_grace_period: 30s
是什么
定义 Docker 在发送停止信号后,等待容器自行退出的最长时间 。默认值为 10s。
工作流程
docker compose stop
│
▼
发送 stop_signal (默认 SIGTERM) ──→ 应用收到信号,开始清理(关连接、刷缓存、排空队列)
│
▼
等待 stop_grace_period (30s)
│
├── 应用在 30s 内正常退出 → ✅ 容器停止
│
└── 30s 超时仍未退出 → ❌ 强制发送 SIGKILL,立即杀死
为什么需要调整
- 默认 10s 太短:Java/Spring Boot 应用关闭通常需要 15-30s(销毁 Bean、关闭连接池);Go/Node.js 排空 HTTP 长连接也可能超过 10s。
- 设置过长:如果应用已经死锁无法退出,过长的 grace period 会让部署/重启过程卡住。
⚠️ 关键注意
- 这个值必须 大于 应用实际完成清理所需的时间,否则等同于没配。
SIGKILL无法被捕获,超时强杀意味着所有未完成的写入、未提交的事务都会丢失。
2. stop_signal: SIGINT
是什么
指定 Docker 发送给容器 PID 1 进程的第一个停止信号 。默认是 SIGTERM。
常见信号对比
| 信号 | 编号 | 默认行为 | 适用场景 |
|---|---|---|---|
SIGTERM |
15 | 请求终止,可捕获 | 大多数应用默认,Nginx、PostgreSQL |
SIGINT |
2 | 中断,可捕获 | Node.js、Python (Flask/FastAPI)、Ctrl+C 等效 |
SIGQUIT |
3 | 退出并 dump,可捕获 | Go 应用(pprof)、Java(thread dump) |
SIGKILL |
9 | 立即杀死,不可捕获 | ⛔ 永远不要设为 stop_signal |
为什么要改
不同语言/框架监听的信号不同:
- Node.js :
process.on('SIGINT', ...)是惯用写法,很多框架默认只监听 SIGINT 而不处理 SIGTERM。如果用默认的 SIGTERM,应用可能直接忽略,等到 grace period 超时被 SIGKILL 强杀。 - Go 应用:某些框架用 SIGQUIT 触发 graceful shutdown 并同时输出 goroutine stack trace,方便排查关闭慢的原因。
💡 最佳实践
查阅你所用框架的文档,确认它监听哪个信号来做优雅关闭,然后将
stop_signal设为对应值。信号不匹配是"明明配了 grace period 但应用还是被强杀"的最常见原因。
3. init: true
是什么
在容器内注入一个轻量级 init 进程 (通常是 tini,约 10KB),作为真正的 PID 1,你的应用变成 PID 2+。
解决什么问题
在没有 init 的情况下,你的应用直接作为 PID 1 运行,会面临两个经典问题:
| 问题 | 无 init (应用=P1) | 有 init (tini=P1, 应用=P2+) |
|---|---|---|
| 僵尸进程 | PID 1 不负责回收子进程,已退出的子进程变 zombie,内存泄漏 | tini 自动 wait() 回收所有孤儿/僵尸进程 |
| 信号转发 | Linux 内核不给 PID 1 发送默认信号处理;应用若不显式注册 SIGTERM handler,信号会被丢弃 | tini 正确接收信号并 forward 给应用子进程 |
实际例子
yaml
# ❌ 没有 init:Node.js 作为 PID 1
# docker compose stop → 发 SIGTERM → Node 没注册 handler → 信号被忽略 → 等 30s → SIGKILL 强杀
# ✅ 有 init:tini 作为 PID 1
# docker compose stop → 发 SIGTERM → tini 收到 → 转发给 Node(PID 2) → Node 正常退出
services:
app:
image: node:20
init: true # 注入 tini
stop_signal: SIGINT # tini 转发 SIGINT 给 Node
stop_grace_period: 30s # 给 Node 足够时间清理
⚠️ 注意事项
init: true会增加约 10KB 镜像体积和极微小的启动开销,生产环境完全可以接受。- 如果你的 Dockerfile 中已经手动安装了 tini/dumb-init 并用
ENTRYPOINT ["/sbin/tini", "--"]启动,则不需要 再设init: true,否则会嵌套两层 init。 - Alpine 镜像自带
/sbin/tini;Debian/Ubuntu 基础镜像不含,但 Docker 会在运行时自动注入,无需修改 Dockerfile。
🔗 三者协同的完整生命周期
docker compose stop api
│
▼
tini (PID 1) 收到 SIGINT (stop_signal)
│
▼
tini 将 SIGINT 转发给应用 (PID 2)
│
▼
应用执行优雅关闭逻辑(关DB连接、排空请求、flush日志)
│
▼
应用在 30s (stop_grace_period) 内退出
│
▼
tini 回收应用进程 → 容器干净停止 ✅
一句话总结 :
init确保信号能送达且僵尸被回收,stop_signal确保发的是应用能识别的信号,stop_grace_period确保应用有足够时间完成清理。三者缺一不可,否则优雅退出就是纸上谈兵。
2.6 健康检查
yaml
services:
api:
healthcheck:
test: ["CMD-SHELL", "curl -sf http://localhost:8080/health || exit 1"]
interval: 15s
timeout: 5s
retries: 3
start_period: 30s # 启动后 30s 内失败不计入统计
depends_on:
db:
condition: service_healthy
- 解释 :
start_period是冷启动宽限期,期间健康检查仍执行但不影响容器状态判定;CMD-SHELL允许管道和逻辑运算符。 - 为什么用 :Java/Go 应用启动慢,没有
start_period会被误判 unhealthy 反复重启;depends_on.condition确保数据库就绪后再启动 API。 - 注意 :
test推荐用数组形式避免 shell 转义问题;健康检查命令应轻量,避免高频 curl 消耗资源;docker inspect --format='{``{.State.Health.Status}}' <container>可实时查看状态。
在 Docker Compose Specification 中,depends_on 的 condition 一共有 3 个合法值:
| condition 值 | 含义 | 典型场景 |
|---|---|---|
service_started |
等待依赖容器启动成功(即进入 running 状态) | 无健康检查的普通服务;仅需确保进程已拉起 |
service_healthy |
等待依赖容器的健康检查通过(healthcheck status = healthy) | 数据库、缓存、API 网关等需要确认可用后才连接的服务 |
service_completed_successfully |
等待依赖容器正常退出且退出码为 0 | 数据库迁移、初始化脚本、一次性 setup 任务 |
⚠️ 关键注意事项
-
service_healthy的前提 :被依赖的服务必须定义了healthcheck,否则 Compose 会直接报错:service "db" has no healthcheck configured -
没有
service_stopped/service_failed:不存在"等某个服务停止"或"等某个服务失败"的条件。如果需要这类逻辑,应使用外部编排工具或在应用层处理。 -
默认值 :如果省略
condition,等价于service_started:yaml# 以下两种写法完全等价 depends_on: redis: condition: service_started depends_on: - redis # 简写形式,隐式 = service_started -
service_completed_successfully的行为细节 :- 该容器退出码为 0 → 条件满足,后续服务启动
- 该容器退出码非 0 → 条件永不满足 ,
docker compose up会一直等待直到超时或手动中断 - 该容器仍在运行 → 继续等待
-
仅控制启动顺序,不控制停止/重启 :
depends_on只在up时生效。当依赖服务崩溃重启时,不会 自动重启依赖它的服务------这需要通过restart: unless-stopped+ 应用层的重连/重试机制来保障。
💡 记忆口诀 :started (起来了)、healthy (活好了)、completed(干完了)------三个词覆盖所有启动依赖场景。
针对下面这个行的解释:
yaml
test: ["CMD-SHELL", "curl -sf http://localhost:8080/health || exit 1"]
这行配置是 Docker 健康检查的核心探测命令 。它的作用是:每隔一段时间,在容器内部执行一次 HTTP 请求,根据返回结果判断服务是否存活。
下面逐层拆解:
1. 语法结构
yaml
test: ["CMD-SHELL", "curl -sf http://localhost:8080/health || exit 1"]
# ↑ ↑
# 执行模式 实际执行的 shell 命令字符串
test 支持两种形式:
| 形式 | 写法 | Shell 解析 | 适用场景 |
|---|---|---|---|
| CMD-SHELL | ["CMD-SHELL", "command string"] |
✅ 经过 /bin/sh -c 解析 |
需要管道 ` |
| CMD | ["CMD", "curl", "-sf", "http://..."] |
❌ 直接 exec,不经过 shell | 简单命令,无特殊字符,更安全 |
这里选择
CMD-SHELL是因为用到了||逻辑运算符。
2. 命令逐参数解析
bash
curl -sf http://localhost:8080/health || exit 1
| 部分 | 含义 |
|---|---|
curl |
容器内发起 HTTP 请求(前提是镜像中安装了 curl) |
-s |
Silent:静默模式,不输出进度条和错误信息,避免日志污染 |
-f |
Fail fast:HTTP 4xx/5xx 时直接返回非零退出码(默认 curl 在 404 时仍返回 0) |
http://localhost:8080/health |
探测目标:容器自身的 localhost,不是宿主机 |
| ` |
3. 判定逻辑
Docker 只看退出码:
curl 成功 (HTTP 2xx) → 退出码 0 → ✅ healthy
curl 失败 (任何原因) → 退出码 ≠ 0 → ❌ unhealthy
|| exit 1 的作用:确保 curl 失败时退出码一定是 1,而不是 curl 自身可能返回的各种奇怪错误码(如 7=连接拒绝、28=超时、22=HTTP错误等)。统一为 1 便于排查。
4. ⚠️ 四个常见坑
-
镜像里没有 curl
Alpine 镜像默认不带 curl,健康检查会直接报
executable file not found。dockerfile# 解决方案1:安装 curl RUN apk add --no-cache curl # 解决方案2:用 wget 替代(Alpine 自带) test: ["CMD-SHELL", "wget -qO- http://localhost:8080/health || exit 1"] # 解决方案3:用专用工具(推荐生产环境) # COPY --from=ghcr.io/klauspost/healthcheck /healthcheck /usr/local/bin/ -
localhost 是容器内部
localhost:8080指的是容器自己 的 8080 端口,不是宿主机。如果服务监听在0.0.0.0:8080,容器内localhost:8080可以访问;但如果服务只监听了外部 IP 或 Unix Socket,则需要调整 URL。 -
-f不能捕获所有异常-f只对 HTTP 响应码生效。如果 DNS 解析失败、TCP 连接被拒绝等,curl 本身就会返回非零码,-f不参与。所以-sf组合已经覆盖了绝大多数失败场景。 -
命令要轻量
健康检查每
interval执行一次。不要用curl去请求一个重接口(如全表查询),应专门暴露一个轻量的/health端点,只做内存级检查。
5. 更健壮的替代方案
对于生产环境,建议用专门的探针工具替代 curl:
yaml
# 使用 dockerize(无需安装 curl,二进制极小)
test: ["CMD", "dockerize", "-wait", "http://localhost:8080/health", "-timeout", "5s"]
# 或使用 grpc-health-probe(gRPC 服务)
test: ["CMD", "/bin/grpc_health_probe", "-addr=:50051"]
一句话总结 :
curl -sf ... || exit 1是"在容器内用最小开销验证 HTTP 服务是否真正可用"的标准写法,-s防日志噪音,-f让 HTTP 错误变为非零退出码,|| exit 1统一失败信号。使用前务必确认镜像中有 curl。
2.7 资源限制
yaml
services:
worker:
deploy:
resources:
limits:
cpus: '2.0'
memory: 2G
pids: 100 # 限制最大进程数,防 fork bomb
reservations:
cpus: '0.5'
memory: 512M
- 解释 :
limits是硬上限(超出 OOM kill / CPU throttle);reservations是软保障(调度器优先分配);pids防止恶意或 bug 导致的进程爆炸。 - 为什么用 :单机多服务共存时防止某个服务吃光资源;
pids是安全加固的重要手段。 - 注意 :Compose V2 中
deploy.resources在docker compose up时生效(无需 Swarm);memory不支持小数,单位 B/K/M/G;CPU 支持小数如'0.25'。
2.8 依赖与启动顺序
yaml
depends_on:
db:
condition: service_healthy # ✅ 推荐:等健康检查通过
redis:
condition: service_started # 仅等容器启动
worker:
condition: service_completed_successfully # 等一次性任务成功完成
yaml
services:
migrate:
image: my-app:migrate
command: ["python", "manage.py", "migrate"]
restart: "no" # 一次性任务,完成即停
api:
depends_on:
db:
condition: service_healthy
migrate:
condition: service_completed_successfully # 等迁移成功完成
redis:
condition: service_started # 仅需启动,无需健康
- 解释 :三种 condition 对应不同语义:
service_healthy等健康检查通过;service_completed_successfully等退出码 0;service_started仅等容器 running。 - 为什么用 :数据库迁移是一次性任务,API 必须等它成功才能启动;Redis 无健康检查端点时用
service_started兜底。 - 注意 :
depends_on不保证 服务永远可用,只保证启动时序;应用自身仍需实现重试逻辑。service_completed_successfully要求目标服务restart: "no"或自然退出。
3. Networks 网络配置
yaml
networks:
frontend:
driver: bridge
ipam:
config:
- subnet: 172.28.0.0/16
backend:
external: true # 使用已存在的网络
name: shared-network
服务内引用:
yaml
services:
web:
networks:
frontend:
aliases: [web-api] # 网络别名
backend:
ipv4_address: 172.28.0.10 # 固定IP
yaml
networks:
frontend:
driver: bridge
ipam:
config:
- subnet: 172.28.0.0/16
gateway: 172.28.0.1
monitoring:
external: true
name: grafana-net # 引用已存在的网络
services:
nginx:
networks:
frontend:
aliases: [web, api-gateway] # 同一网络内多个 DNS 名称
ipv4_address: 172.28.0.10 # 固定 IP(需在 subnet 范围内)
monitoring: # 跨网络通信
- 解释 :
aliases提供额外 DNS 解析名;固定 IP 需配合ipam.subnet使用;external引用宿主机上已创建的网络。 - 为什么用:微服务间通过别名解耦真实服务名;Legacy 应用硬编码了特定 IP 时需固定地址;Prometheus/Grafana 等监控栈通常独立部署,通过 external network 接入。
- 注意 :固定 IP 在
docker compose up --scale多实例时会冲突,仅适用于单实例服务;不同网络间的容器默认隔离,需同时加入两个网络才能互通。
针对你引用的 networks 配置片段,以下是三个关键概念的详细解析:
1. driver 的可选值
driver 指定 Docker 使用哪种网络驱动来创建该网络。常用值如下:
| driver 值 | 说明 | 典型场景 |
|---|---|---|
bridge |
默认值。创建独立的 Linux bridge,容器间通过虚拟网卡通信,与宿主机网络隔离 | 单机多容器应用(绝大多数 Compose 项目) |
host |
容器直接共享宿主机网络栈,无 NAT、无端口映射,性能最高但失去网络隔离 | 高性能网络应用、监控 Agent |
overlay |
跨多主机的加密 VXLAN 网络,依赖 Swarm 或手动配置 | Docker Swarm 集群服务发现 |
macvlan |
为每个容器分配独立 MAC 地址,表现为物理网络上的真实设备 | 需要直接接入局域网、遗留系统对接 |
ipvlan |
类似 macvlan 但共享宿主机 MAC,仅分配独立 IP | 对 MAC 数量有交换机限制的环境 |
none |
无任何网络连接,仅有 loopback | 安全沙箱、纯离线计算任务 |
⚠️ 注意 :在 Compose 中如果不写
driver,默认就是bridge。host和none模式下,ipam、subnet等配置无效且会被忽略。
2. ipam 是什么?
IPAM = IP Address Management(IP 地址管理)
它定义了 Docker 如何为该网络分配 IP 地址段。核心子字段:
yaml
ipam:
driver: default # IPAM 驱动,几乎总是 default
config:
- subnet: 172.28.0.0/16 # 整个网络的 CIDR 网段
gateway: 172.28.0.1 # 网关地址(可选,默认取网段第一个可用IP)
ip_range: 172.28.5.0/24 # 实际分配给容器的IP池(可选,必须是subnet的子集)
为什么要手动指定 IPAM?
- 避免网段冲突 :Docker 默认自动分配
172.x.0.0/16或192.168.x.0/20,如果你跑了多个 Compose 项目或与宿主机 VPN/内网冲突,就需要显式指定不重叠的网段。 - 固定容器 IP :配合服务的
ipv4_address使用时,必须先定义明确的 subnet。 - 合规要求:某些企业环境要求容器网络必须落在特定审批过的网段内。
💡 大多数时候不需要配
如果你的项目只有一个 compose 文件、不与外部网络交互,完全可以省略整个 ipam 块,让 Docker 自动管理即可。
3. name 是什么意思?
name 用于显式指定网络在 Docker 引擎中的真实名称。
默认行为(不设 name)
Docker 会自动生成名称:{项目名}_{网络key}
yaml
# 项目名为 my-project,网络 key 为 backend
# → 实际创建的网络名叫: my-project_backend
networks:
backend:
driver: bridge
设置 name 后
yaml
networks:
backend:
name: shared-network # 实际创建的网络就叫 shared-network
两种核心用途
| 用途 | 说明 |
|---|---|
| 跨项目共享网络 | 项目 A 创建 name: shared-network,项目 B 用 external: true + name: shared-network 引用同一个网络,实现不同 Compose 项目间的容器互通 |
| 稳定可预测的名称 | 避免项目目录改名导致网络名变化,方便脚本、CI/CD、外部工具引用 |
⚠️ name 与 external 的关系
yaml
# 场景1:自己创建网络,并指定名称
backend:
name: shared-network # docker network create shared-network
# 场景2:引用别人已创建的网络
backend:
external: true
name: shared-network # 告诉 Compose:"别创建,去找叫这个名字的现有网络"
关键区别 :
external: true时name是查找条件 ;非 external 时name是创建时的命名。两者含义不同但语法相同。
📌 速记总结
| 字段 | 一句话记忆 |
|---|---|
driver |
"用什么方式连" → bridge/host/overlay/macvlan/ipvlan/none |
ipam |
"IP 从哪来" → 定义网段、网关、分配池,防冲突用 |
name |
"叫什么名字" → 自定义真实网络名,跨项目共享必备 |
4. Volumes 卷管理
yaml
volumes:
db_data: # 命名卷(自动创建)
driver: local
driver_opts:
type: none
o: bind
device: /data/postgres # 绑定到宿主机特定路径
external_vol:
external: true
name: pre-existing-volume # 引用外部卷
- 解释 :
driver_opts中type: none+o: bind+device组合实现了"命名卷语法 + bind mount 物理路径"的效果,兼具可移植性和路径可控性。 - 为什么用:SSD/HDD 分离部署时,将数据库卷指向高性能磁盘;集群环境中预先创建卷并统一管理,compose 只引用不创建。
- 注意 :
device路径必须事先存在 ,Docker 不会自动创建;此写法仅localdriver 支持;docker volume inspect pg_data可查看实际挂载点。
5. 高级技巧
YAML 锚点复用配置
yaml
x-logging: &default-logging
driver: json-file
options:
max-size: "10m"
max-file: "3"
services:
web:
logging: *default-logging
worker:
logging: *default-logging
多文件覆盖
bash
docker compose -f compose.yaml -f compose.prod.yaml up -d
后者同名 key 会覆盖前者,适合环境差异化配置。
bash
# base: compose.yaml
# prod overlay: compose.prod.yaml
docker compose -f compose.yaml -f compose.prod.yaml config > merged.yaml
yaml
# compose.prod.yaml 只写差异部分
services:
api:
image: myapp:v2.3.1 # 覆盖镜像标签
environment:
- LOG_LEVEL=warn # 追加/覆盖环境变量
deploy:
replicas: 3 # 新增字段
- 解释 :后者文件同名 key 递归合并,列表类型(如 environment)整体替换 而非追加(除非用
-语法)。 - 为什么用:base 文件入 Git,prod/staging overlay 按需叠加;CI 中动态生成 overlay 注入版本号。
- 注意 :
docker compose config是调试合并结果的必备工具;环境变量列表若想追加而非替换,需在 overlay 中重复列出 base 的所有变量。
变量插值
yaml
image: myapp:${TAG:-latest}
labels:
com.example.version: "${VERSION:?VERSION is required}" # 缺失时报错
支持的运算符:${VAR-default}、${VAR:-default}、${VAR:?err}、${VAR:+alt}
变量插值完整示例
yaml
services:
app:
image: registry.example.com/myapp:${TAG:-latest}
labels:
version: "${VERSION:?Please set VERSION env var}"
branch: "${BRANCH:+feature/${BRANCH}}" # BRANCH 非空时才展开
- 解释 :
${VAR:-default}变量未定义或为空时用 default;${VAR:?msg}未定义或为空时报错;${VAR:+alt}变量非空时用 alt,否则为空。 - 为什么用:CI 流水线中 TAG 可能为空,fallback 到 latest;VERSION 是发布必填项,缺失立即失败;分支标签仅在 feature 构建时添加。
- 注意 :插值发生在 compose 解析阶段,早于 容器创建;
.env文件中的变量也参与插值,但$$可转义为字面量$。
config使用
config 是 Docker Compose 中容易被忽略但非常实用的功能。它的核心价值是:将配置文件作为独立对象管理,而不是把文件路径硬编码进 volume 挂载。
1. config vs volume 的本质区别
| 对比项 | volumes 挂载文件 |
configs |
|---|---|---|
| 来源 | 宿主机文件系统 | Compose 管理的命名对象 |
| 容器内路径 | 你指定的任意路径 | 固定 /configs/<name>(只读) |
| 权限控制 | 依赖宿主机文件权限 | 可指定 uid/gid/mode |
| Swarm/K8s 兼容 | ❌ 仅单机有效 | ✅ 原生支持集群分发 |
| 版本管理 | 无 | 同名更新会创建新版本(Swarm) |
| 适用场景 | 开发环境、大文件、需读写 | 生产配置、敏感文件、集群部署 |
2. 完整示例:Nginx + 自定义应用配置
docker-compose.yml
yaml
# ========== 定义 config 对象 ==========
configs:
nginx_conf: # config 名称(引用时用这个名字)
file: ./configs/nginx.conf # 宿主机源文件路径
app_settings:
file: ./configs/app.yaml
db_password:
file: ./secrets/db_password.txt # ⚠️ 注意:敏感内容建议用 secrets,这里仅作演示
services:
web:
image: nginx:alpine
configs:
# ✅ 方式1:短语法 → 挂载到 /configs/nginx_conf(只读)
- nginx_conf
# ✅ 方式2:长语法 → 自定义容器内路径和权限
- source: app_settings # 引用上面定义的 config 名称
target: /etc/app/settings.yaml # 容器内的目标路径
uid: "1000" # 文件所有者 UID
gid: "1000" # 文件所属组 GID
mode: 0440 # 文件权限(八进制)
api:
image: my-api:latest
configs:
- source: app_settings
target: /app/config.yaml
mode: 0400 # 仅 owner 可读
3. 容器内实际效果
bash
# 短语法挂载的文件
$ docker exec web cat /configs/nginx_conf
# → nginx.conf 的内容(只读)
# 长语法自定义路径的文件
$ docker exec web cat /etc/app/settings.yaml
# → app.yaml 的内容,权限为 0440,属主 uid=1000
$ docker exec web ls -la /etc/app/settings.yaml
# -r--r----- 1 1000 1000 ... settings.yaml
4. ⚠️ 关键注意事项
-
容器内默认路径是
/configs/<name>,不是/etc/xxx。如果你的应用期望读取/etc/nginx/nginx.conf,必须用长语法指定 target。 -
所有 config 文件在容器内都是只读的 ,应用无法修改。如果需要运行时写入,仍需用
volumes。 -
Config 内容变更不会自动热更新 。修改宿主机文件后,必须
docker compose up -d重建容器才能生效(Swarm 模式下可通过滚动更新实现零停机)。 -
敏感数据请用
secrets而非configs:yamlsecrets: db_password: file: ./secrets/db_password.txt services: api: secrets: - db_password # 挂载到 /run/secrets/db_password(tmpfs,不落盘)configs和secrets语法几乎相同,唯一区别是存储位置和安全性。 -
Dockerfile 中不能用
COPY --from=config:config 只在运行时注入,构建阶段不可见。如需构建时使用配置文件,仍需COPY或ARG。
5. 什么时候该用 config?
需要在容器中提供配置文件?
│
├── 开发环境 / 需要频繁修改 / 文件很大 → volumes
│
├── 生产环境 / 集群部署 / 需要精细权限控制 → ✅ configs
│
└── 密码 / Token / 证书私钥等敏感数据 → ✅ secrets
一句话总结 :
configs让你像声明变量一样声明配置文件------定义一次、多处引用、权限可控、与宿主机路径解耦。它是从"单机开发"迈向"生产级编排"的关键一步。
6. 常见反模式 ⚠️
| ❌ 避免 | ✅ 推荐 |
|---|---|
version: '3.8' |
省略 version 字段 |
links: |
使用 networks + 服务名 DNS |
depends_on 无条件等待 |
配合 healthcheck + condition |
| 硬编码密码到 environment | 使用 secrets 或 .env + gitignore |
restart: always 用于调试 |
开发时用 no,生产用 unless-stopped |
| 单个巨大 compose 文件 | 拆分 + 多文件组合 + x-锚点复用 |
快速验证命令
bash
# 1. 渲染最终配置(含所有变量替换、锚点展开、多文件合并)
docker compose config
# 输出完整的、无变量的 YAML,用于 code review 和 CI 校验
# 2. 静默校验语法
docker compose config --quiet && echo "✅ Valid" || echo "❌ Invalid"
# CI pipeline 中作为 gate check
# 3. 预览变更(不实际操作)
docker compose up --dry-run
# 显示哪些容器会 create/recreate/start,类似 terraform plan
# 4. 检查单个服务的最终配置
docker compose config | yq '.services.api'
# 配合 yq/jq 快速定位某个服务的合并结果
💡 最佳实践 :将
compose.yaml视为基础设施代码,纳入版本控制;敏感信息通过.env(不入仓库)或 secrets 注入;始终为生产服务配置healthcheck+restart: unless-stopped+ 资源限制三件套。
💡 终极建议 :将以上所有示例整合到一个compose.reference.yaml文件中作为团队模板,新服务直接复制裁剪,比文档更高效。每次升级 Compose 版本后,用docker compose config --quiet回归验证兼容性。