Docker Compose 配置文件(通常为 compose.yamldocker-compose.yml)是声明式定义多容器应用的核心。以下是按功能模块归纳的常用语法速查手册,基于当前主流的 Compose Specification 标准。


1. 顶层结构概览

name: my-project          # 项目名称(可选,默认取目录名)
services: {}              # 【核心】服务定义
networks: {}              # 自定义网络
volumes: {}               # 命名卷 / 外部卷
configs: {}               # 配置管理(Swarm/Compose v2.23+)
secrets: {}               # 敏感数据管理
x-common: &anchor         # 自定义扩展字段 + YAML锚点(复用配置)

⚠️ 现代 Compose 已不再强制要求 version 字段,建议省略以避免混淆。


name
name: ecommerce-prod
services:
  web:
    image: nginx
  • 解释:显式指定项目名称。所有容器、网络、卷都会以 ecommerce-prod- 为前缀(如 ecommerce-prod-web-1)。
  • 为什么用:避免在同一目录下运行多个 compose 文件时资源命名冲突;CI/CD 中动态注入项目名实现环境隔离。
  • 注意:若不设置,默认取 compose.yaml 所在目录名,重命名目录会导致旧资源 orphaned。
x-common (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 引用,<<: 合并映射。workerdeploy深层覆盖锚点值。
  • 为什么用:消除重复配置,修改一处全局生效。比多文件覆盖更轻量,适合单文件内的横向复用。
  • 注意: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拉取策略
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 端口映射
ports:
  - "8080:80"             # host:container 短格式写法
  - "127.0.0.1:3306:3306" # 仅本地访问
  - target: 80            #容器端口 长格式写法
    published: 8080       #宿主机端口
    protocol: tcp         # 长语法,更精确
    mode: host            #Swarm 集群模式下设为 ingress
2.3 环境变量
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 存储挂载
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 重启与生命周期
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 stopdocker 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

常见信号对比
信号编号默认行为适用场景
SIGTERM15请求终止,可捕获大多数应用默认,Nginx、PostgreSQL
SIGINT2中断,可捕获Node.js、Python (Flask/FastAPI)、Ctrl+C 等效
SIGQUIT3退出并 dump,可捕获Go 应用(pprof)、Java(thread dump)
SIGKILL9立即杀死,不可捕获⛔ 永远不要设为 stop_signal
为什么要改

不同语言/框架监听的信号不同:

  • Node.jsprocess.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 给应用子进程
实际例子
# ❌ 没有 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 健康检查
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_oncondition 一共有 3 个合法值:

condition 值含义典型场景
service_started等待依赖容器启动成功(即进入 running 状态)无健康检查的普通服务;仅需确保进程已拉起
service_healthy等待依赖容器的健康检查通过(healthcheck status = healthy)数据库、缓存、API 网关等需要确认可用后才连接的服务
service_completed_successfully等待依赖容器正常退出且退出码为 0数据库迁移、初始化脚本、一次性 setup 任务

⚠️ 关键注意事项

  1. service_healthy 的前提:被依赖的服务必须定义了 healthcheck,否则 Compose 会直接报错:
    service "db" has no healthcheck configured
    
  2. 没有 service_stopped / service_failed:不存在"等某个服务停止"或"等某个服务失败"的条件。如果需要这类逻辑,应使用外部编排工具或在应用层处理。
  3. 默认值:如果省略 condition,等价于 service_started
    # 以下两种写法完全等价
    depends_on:
      redis:
        condition: service_started
    
    depends_on:
      - redis   # 简写形式,隐式 = service_started
    
  4. service_completed_successfully 的行为细节
    • 该容器退出码为 0 → 条件满足,后续服务启动
    • 该容器退出码非 0 → 条件永不满足docker compose up 会一直等待直到超时或手动中断
    • 该容器仍在运行 → 继续等待
  5. 仅控制启动顺序,不控制停止/重启depends_on 只在 up 时生效。当依赖服务崩溃重启时,不会自动重启依赖它的服务——这需要通过 restart: unless-stopped + 应用层的重连/重试机制来保障。

💡 记忆口诀started(起来了)、healthy(活好了)、completed(干完了)——三个词覆盖所有启动依赖场景。

针对下面这个行的解释:

test: ["CMD-SHELL", "curl -sf http://localhost:8080/health || exit 1"]

这行配置是 Docker 健康检查的核心探测命令。它的作用是:每隔一段时间,在容器内部执行一次 HTTP 请求,根据返回结果判断服务是否存活。

下面逐层拆解:

1. 语法结构

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. 命令逐参数解析

curl -sf http://localhost:8080/health || exit 1
部分含义
curl容器内发起 HTTP 请求(前提是镜像中安装了 curl
-sSilent:静默模式,不输出进度条和错误信息,避免日志污染
-fFail fast:HTTP 4xx/5xx 时直接返回非零退出码(默认 curl 在 404 时仍返回 0)
http://localhost:8080/health探测目标:容器自身的 localhost,不是宿主机
|| exit 1如果 curl 失败(网络不通 / 超时 / 4xx / 5xx),显式返回退出码 1

3. 判定逻辑

Docker 只看退出码

curl 成功 (HTTP 2xx) → 退出码 0 → ✅ healthy
curl 失败 (任何原因) → 退出码 ≠ 0 → ❌ unhealthy

|| exit 1 的作用:确保 curl 失败时退出码一定是 1,而不是 curl 自身可能返回的各种奇怪错误码(如 7=连接拒绝、28=超时、22=HTTP错误等)。统一为 1 便于排查。

4. ⚠️ 四个常见坑

  1. 镜像里没有 curl
    Alpine 镜像默认不带 curl,健康检查会直接报 executable file not found

    # 解决方案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/
    
  2. localhost 是容器内部
    localhost:8080 指的是容器自己的 8080 端口,不是宿主机。如果服务监听在 0.0.0.0:8080,容器内 localhost:8080 可以访问;但如果服务只监听了外部 IP 或 Unix Socket,则需要调整 URL。

  3. -f 不能捕获所有异常
    -f 只对 HTTP 响应码生效。如果 DNS 解析失败、TCP 连接被拒绝等,curl 本身就会返回非零码,-f 不参与。所以 -sf 组合已经覆盖了绝大多数失败场景。

  4. 命令要轻量
    健康检查每 interval 执行一次。不要用 curl 去请求一个重接口(如全表查询),应专门暴露一个轻量的 /health 端点,只做内存级检查。

5. 更健壮的替代方案

对于生产环境,建议用专门的探针工具替代 curl:

# 使用 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 资源限制
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.resourcesdocker compose up 时生效(无需 Swarm);memory 不支持小数,单位 B/K/M/G;CPU 支持小数如 '0.25'
2.8 依赖与启动顺序
depends_on:
  db:
    condition: service_healthy     # ✅ 推荐:等健康检查通过
  redis:
    condition: service_started     # 仅等容器启动
  worker:
    condition: service_completed_successfully  # 等一次性任务成功完成
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 网络配置

networks:
  frontend:
    driver: bridge
    ipam:
      config:
        - subnet: 172.28.0.0/16
  backend:
    external: true                 # 使用已存在的网络
    name: shared-network

服务内引用:

services:
  web:
    networks:
      frontend:
        aliases: [web-api]        # 网络别名
      backend:
        ipv4_address: 172.28.0.10 # 固定IP
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,默认就是 bridgehostnone 模式下,ipamsubnet 等配置无效且会被忽略。


2. ipam 是什么?

IPAM = IP Address Management(IP 地址管理)

它定义了 Docker 如何为该网络分配 IP 地址段。核心子字段:

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/16192.168.x.0/20,如果你跑了多个 Compose 项目或与宿主机 VPN/内网冲突,就需要显式指定不重叠的网段。
  • 固定容器 IP:配合服务的 ipv4_address 使用时,必须先定义明确的 subnet。
  • 合规要求:某些企业环境要求容器网络必须落在特定审批过的网段内。
💡 大多数时候不需要配

如果你的项目只有一个 compose 文件、不与外部网络交互,完全可以省略整个 ipam,让 Docker 自动管理即可。


3. name 是什么意思?

name 用于显式指定网络在 Docker 引擎中的真实名称

默认行为(不设 name)

Docker 会自动生成名称:{项目名}_{网络key}

# 项目名为 my-project,网络 key 为 backend
# → 实际创建的网络名叫: my-project_backend
networks:
  backend:
    driver: bridge
设置 name 后
networks:
  backend:
    name: shared-network    # 实际创建的网络就叫 shared-network
两种核心用途
用途说明
跨项目共享网络项目 A 创建 name: shared-network,项目 B 用 external: true + name: shared-network 引用同一个网络,实现不同 Compose 项目间的容器互通
稳定可预测的名称避免项目目录改名导致网络名变化,方便脚本、CI/CD、外部工具引用
⚠️ nameexternal 的关系
# 场景1:自己创建网络,并指定名称
backend:
  name: shared-network       # docker network create shared-network

# 场景2:引用别人已创建的网络
backend:
  external: true
  name: shared-network       # 告诉 Compose:"别创建,去找叫这个名字的现有网络"

关键区别external: truename查找条件;非 external 时 name创建时的命名。两者含义不同但语法相同。


📌 速记总结

字段一句话记忆
driver“用什么方式连” → bridge/host/overlay/macvlan/ipvlan/none
ipam“IP 从哪来” → 定义网段、网关、分配池,防冲突用
name“叫什么名字” → 自定义真实网络名,跨项目共享必备

4. Volumes 卷管理

volumes:
  db_data:                        # 命名卷(自动创建)
    driver: local
    driver_opts:
      type: none
      o: bind
      device: /data/postgres      # 绑定到宿主机特定路径
  external_vol:
    external: true
    name: pre-existing-volume     # 引用外部卷
  • 解释driver_optstype: none + o: bind + device 组合实现了"命名卷语法 + bind mount 物理路径"的效果,兼具可移植性和路径可控性。
  • 为什么用:SSD/HDD 分离部署时,将数据库卷指向高性能磁盘;集群环境中预先创建卷并统一管理,compose 只引用不创建。
  • 注意device 路径必须事先存在,Docker 不会自动创建;此写法仅 local driver 支持;docker volume inspect pg_data 可查看实际挂载点。

5. 高级技巧

YAML 锚点复用配置
x-logging: &default-logging
  driver: json-file
  options:
    max-size: "10m"
    max-file: "3"

services:
  web:
    logging: *default-logging
  worker:
    logging: *default-logging
多文件覆盖
docker compose -f compose.yaml -f compose.prod.yaml up -d

后者同名 key 会覆盖前者,适合环境差异化配置。

# base: compose.yaml
# prod overlay: compose.prod.yaml
docker compose -f compose.yaml -f compose.prod.yaml config > merged.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 的所有变量。
变量插值
image: myapp:${TAG:-latest}
labels:
  com.example.version: "${VERSION:?VERSION is required}"  # 缺失时报错

支持的运算符:${VAR-default}${VAR:-default}${VAR:?err}${VAR:+alt}

变量插值完整示例
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
# ========== 定义 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. 容器内实际效果

# 短语法挂载的文件
$ 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. ⚠️ 关键注意事项

  1. 容器内默认路径是 /configs/<name>,不是 /etc/xxx。如果你的应用期望读取 /etc/nginx/nginx.conf必须用长语法指定 target
  2. 所有 config 文件在容器内都是只读的,应用无法修改。如果需要运行时写入,仍需用 volumes
  3. Config 内容变更不会自动热更新。修改宿主机文件后,必须 docker compose up -d 重建容器才能生效(Swarm 模式下可通过滚动更新实现零停机)。
  4. 敏感数据请用 secrets 而非 configs
    secrets:
      db_password:
        file: ./secrets/db_password.txt
    services:
      api:
        secrets:
          - db_password   # 挂载到 /run/secrets/db_password(tmpfs,不落盘)
    
    configssecrets 语法几乎相同,唯一区别是存储位置和安全性。
  5. Dockerfile 中不能用 COPY --from=config:config 只在运行时注入,构建阶段不可见。如需构建时使用配置文件,仍需 COPYARG

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-锚点复用

快速验证命令

# 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 回归验证兼容性。

更多推荐