Docker Compose 最佳实践
Docker Compose 最擅长的事情其实很明确:把一组容器怎么一起跑描述清楚,然后让开发环境、测试环境、临时环境尽量一致。
它非常适合本地开发、集成测试、一次性任务,也适合在单机上快速复现某个问题。但很多团队的问题恰恰出在这里:明明 Compose 只是环境编排工具,却硬把它往生产编排平台上用,最后 YAML 越堆越多,边界越来越模糊。
这篇文章就讲一件事:把 Compose 放回它擅长的位置。
先把定位摆正:Compose 是环境定义,不是生产调度器
Compose 很适合这些场景:
- 本地开发的应用 + 数据库 + 缓存
- 集成测试依赖环境
- 单机上的预览环境
- migration、seed、admin task 这类一次性任务
- 带真实依赖的本地问题复现
不适合这些场景:
- 多节点生产编排
- 自动扩缩容
- 复杂发布策略
- 跨主机调度
- 长期严肃的 secret 管理
如果你的生产环境需要滚动发布、实例调度、故障迁移、自动扩容,那 Compose 不是“还差一点”,而是工具选错了。
用 v2 插件,命令统一成 docker compose
现在的主流用法是 Docker Compose v2 插件:
docker compose version优先使用:
docker compose ...不要再把 docker-compose ... 当主要写法。
另外,新的 Compose 文件也不要再写顶层 version: 字段。现代 Compose 已经不需要它,继续写只会让人误以为它还参与版本选择。
目录结构越普通越好
建议保持简单:
.
├── compose.yaml
├── compose.override.yaml
├── compose.ci.yaml
├── .env.example
├── app/
├── infra/
│ ├── postgres/
│ └── nginx/
└── scripts/比较实用的约定是:
compose.yaml:公共基础配置compose.override.yaml:本地开发覆盖,自动加载compose.ci.yaml:CI 场景覆盖.env.example:变量说明,不放真凭据
不要动不动拆七八个 Compose 文件。文件层次一多,最后 docker compose config 展开出来谁都不敢看。
从一个小而稳的基础文件开始
例如:
services:
api:
build:
context: .
dockerfile: app/Dockerfile
command: ["./bin/api"]
environment:
APP_ENV: development
DATABASE_URL: postgres://app:app@db:5432/app?sslmode=disable
REDIS_URL: redis://redis:6379/0
ports:
- "8080:8080"
volumes:
- .:/workspace
working_dir: /workspace
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
db:
image: postgres:16
environment:
POSTGRES_DB: app
POSTGRES_USER: app
POSTGRES_PASSWORD: app
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 5s
timeout: 3s
retries: 10
redis:
image: redis:7
command: ["redis-server", "--save", "", "--appendonly", "no"]
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 10
volumes:
postgres-data:没有 version:,这就是现在推荐的写法。
override 文件是为了开发便利,不是为了藏核心差异
compose.override.yaml 很适合放本地开发专用配置,比如:
- bind mount
- 调试端口
- 热更新命令
- 更详细的日志级别
例如:
services:
api:
command: ["air", "-c", ".air.toml"]
environment:
LOG_LEVEL: debug
ports:
- "2345:2345"开发同学直接跑:
docker compose upCI 则显式指定:
docker compose -f compose.yaml -f compose.ci.yaml up --abort-on-container-exit --exit-code-from testoverride 的用途应该是“覆盖环境细节”,不是“同一个服务在不同环境下其实是两套完全不同的契约”。
.env 要用明白,不然很容易自找麻烦
Compose 的环境变量处理经常让人混乱,核心要分清三件事:
.env主要是给 Compose 文件里的${...}做变量替换env_file:是把变量注入到容器环境.env.example可以提交,真实.env不要提交
例如:
services:
api:
env_file:
- .env.local
environment:
APP_ENV: development
HTTP_PORT: ${HTTP_PORT:-8080}几个容易遇到问题的点:
.env会影响 Compose 渲染env_file会进入容器内部环境- shell 里的环境变量可能覆盖 Compose 替换结果
- 引号、空格、多余换行都可能出问题
怀疑变量没生效时,先跑:
docker compose config先看最终展开结果,再去怀疑 Docker。
即使是本地环境,也别把 secrets 当儿戏
很多泄漏事故都始于一句“这只是开发环境”。
本地开发至少做到:
- 用低权限或假凭据
- 不把真实密钥写进仓库
- 能走环境变量就别硬编码
- 配置文件挂载尽量只读
例如:
services:
api:
environment:
STRIPE_API_BASE: https://api.stripe.com
STRIPE_API_KEY: ${STRIPE_API_KEY}
volumes:
- ./infra/api/config.dev.yaml:/workspace/config.yaml:ro如果到了生产级 secret 管理,Compose 本身不是答案。该用 secret manager、平台能力,就老老实实用。
healthcheck 很值,不要嫌麻烦
很多服务“容器启动了”不等于“服务可用了”。
典型例子:
- 数据库进程起来了,但还没 ready
- 应用还在跑 migration
- HTTP 端口监听了,但依赖还没连上
所以重要服务建议都加 healthcheck。
例如:
services:
api:
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8080/healthz"]
interval: 10s
timeout: 3s
retries: 10
start_period: 15s健康检查应当满足:
- 开销低
- 在容器内可执行
- 能代表真实可用性
- 不依赖外部不稳定网络
不要把健康检查写成访问第三方公网接口,那测出来的是外网状况,不是你的服务。
depends_on 只能管启动顺序,管不了应用健壮性
这个点需要说得直接一点:depends_on 不是应用重试机制的替代品。
即使写了:
depends_on:
db:
condition: service_healthy也只能改善启动阶段顺序,解决不了这些问题:
- 数据库后续又变得不健康
- migration 还没跑完
- 初始化数据没准备好
- 权限或租户数据还没建完
Compose 只能帮你编排“先后”,不能保证你的应用永远“已经准备好”。
所以应用本身该做的重连、退避、超时处理,还是要做。
持久化数据优先用 named volume
数据库这类容器管理的状态数据,优先用 named volume:
services:
db:
volumes:
- postgres-data:/var/lib/postgresql/data
volumes:
postgres-data:源码、配置、脚本这类主机上要编辑的内容,才适合 bind mount:
services:
api:
volumes:
- .:/workspace经验上可以这么分:
- named volume:服务数据
- bind mount:开发工作区
尽量别把数据库数据目录直接 bind mount 到宿主机本地路径,权限、性能、文件系统差异都可能让你遇到问题。
默认网络已经够用,别没事乱建
Compose 会自动给项目创建默认网络,服务之间可以直接用服务名互联:
db:5432redis:6379api:8080
很多本地环境其实完全够用了。
只有在你明确需要隔离拓扑时,再建自定义网络。
例如:
services:
nginx:
image: nginx:1.27
networks:
- edge
api:
build: .
networks:
- edge
- backend
db:
image: postgres:16
networks:
- backend
networks:
edge:
backend:
internal: trueinternal: true 对后端专用网络比较实用,能避免不必要的外部连通性。
profile 比“注释 YAML”靠谱得多
有些服务不是每次都需要,比如:
- MailHog
- Jaeger
- Prometheus
- 本地对象存储模拟器
这时候用 profile 很合适:
services:
api:
build: .
mailhog:
image: mailhog/mailhog
profiles: ["dev"]
jaeger:
image: jaegertracing/all-in-one:1.57
profiles: ["observability"]默认跑基础栈:
docker compose up带上 MailHog:
docker compose --profile dev up带上观测工具:
docker compose --profile observability up这比让大家去手工注释服务块靠谱太多。
一次性任务要显式建服务
migration、seed、admin job 这类操作很适合通过 Compose 统一入口。
例如:
services:
migrate:
build:
context: .
dockerfile: app/Dockerfile
command: ["./bin/migrate", "up"]
environment:
DATABASE_URL: postgres://app:app@db:5432/app?sslmode=disable
depends_on:
db:
condition: service_healthy
profiles: ["ops"]执行时:
docker compose --profile ops run --rm migrate比起把一长串 docker run ... 命令丢在 wiki 里,这种方式更稳,也更容易复用。
日志要容易看,出了问题能马上抓
本地和 CI 场景下,容器标准输出通常已经够用。
常用命令基本就这些:
docker compose logs -f api
docker compose logs --tail=100 db
docker compose ps如果某些服务日志太多,可以做简单限制:
services:
api:
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"但也别过度设计。真到了需要集中采集、长期保留、跨主机检索的程度,你已经不在 Compose 的舒适区了。
restart 策略有用,但别把它当稳定性方案
本地依赖服务可以适度用:
- 一次性 job 通常不需要 restart
- 长驻依赖服务可以考虑
unless-stopped - 不要让 restart 掩盖 crash loop
例如:
services:
db:
image: postgres:16
restart: unless-stopped要注意的是:过度自动重启会掩盖真实问题。表面上看“容器都在”,实际上服务可能一直在反复崩。
CI 里通常更不建议乱加 restart,因为测试失败就应该立刻暴露。
Compose 很适合 CI 和集成测试
Compose 在 CI 里最大的价值,就是把依赖环境拉起来,然后跑完就扔。
例如:
services:
test:
build:
context: .
dockerfile: app/Dockerfile
command: ["go", "test", "./...", "-count=1"]
environment:
DATABASE_URL: postgres://app:app@db:5432/app?sslmode=disable
depends_on:
db:
condition: service_healthy
db:
image: postgres:16
environment:
POSTGRES_DB: app
POSTGRES_USER: app
POSTGRES_PASSWORD: app
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 5s
timeout: 3s
retries: 10CI 里可以这样跑:
docker compose -f compose.yaml -f compose.ci.yaml up --build --abort-on-container-exit --exit-code-from test结束后记得清理:
docker compose down -v几个比较实用的习惯:
- 用
-p指定独立 project name,避免冲突 - 任务结束后清理网络和卷
- 测试数据尽量确定性
- 不依赖上一次 job 留下来的状态
这些命令最常用,别记花的
日常真正常用的也就这些:
docker compose up -d --build
docker compose down
docker compose down -v
docker compose ps
docker compose logs -f api
docker compose exec api sh
docker compose run --rm migrate
docker compose config尤其是 docker compose config,非常值得常用。文件 merge、变量替换、profile 展开之后到底长什么样,看它比猜靠谱。
几个常见反模式
1. 把 Compose 当长期生产编排平台
极小规模、单机场景也许还能凑合,但只要开始追求可靠发布和弹性调度,就不该继续硬撑。
2. 把真实 secrets 写进 Compose 文件
如果 compose.yaml 里放着生产密码,问题不在 YAML 写法,而在流程本身。
3. 指望 depends_on 解决一切 readiness 问题
它只能帮启动顺序,应用该做的容错还是得做。
4. 什么都 bind mount
源码挂载可以,数据库目录、运行时目录、缓存目录全从宿主机挂进去,通常会把问题越挂越多。
5. 文件和 profile 过多
如果没人说得清“本地启动到底该用哪几个文件、开哪几个 profile”,说明设计已经过度了。
6. 过度依赖 container_name
Compose 本身已经提供基于 service name 的服务发现。强行写死容器名,常常只会带来命名冲突和灵活性下降。
一份够用的检查清单
一个状态比较好的 Compose 项目,通常具备这些特点:
- 使用
docker compose,不是旧版 v1 命令 - 没有过时的
version:字段 - 一个基础文件,少量覆盖文件,职责清楚
- 关键依赖有 healthcheck
- 有状态服务用 named volume
- bind mount 只用在确实影响开发效率的地方
- 可选服务通过 profile 控制
- 仓库里没有真实 secrets
- CI 环境可创建、可销毁、可清理
- 团队没人假装它是生产调度平台
Compose 不需要“高级”,它需要的是稳定、可猜、低摩擦。只要把边界守住,它会是非常顺手的工具。