3.2 Docker Compose 生产部署实战
预计阅读时间:16 分钟
📖 目录
3.1:Docker 容器入门 介绍了 Docker Compose 的基本用法。生产环境中的 Compose 部署需要更多工程考量:健康检查依赖、资源限制、日志聚合、密钥管理和滚动更新。本文用一个完整的多服务应用示例,展示从 docker-compose.yml 到生产级部署的全过程。
学习目标
- 掌握用
healthcheck与depends_on的 condition 控制服务启动顺序 - 能通过
deploy.resources限制容器资源,防止单个服务耗尽宿主机 - 理解 env_file、Docker Secrets 与外部 KMS 的分层密钥管理
- 能配置日志大小限制与 Loki + Promtail 集中式日志采集
- 掌握命名卷持久化与可验证的备份恢复流程
- 能实现 Swarm 滚动更新与单机蓝绿部署的零停机发布
前置知识
- 3.1:Docker 容器入门 Docker 容器入门——镜像、容器与 Compose 基础
- 5.2:Docker 生产实践 Docker 生产实践——多阶段构建与镜像安全加固
- 4.8:集中式日志管理 集中式日志管理——Loki + Promtail 采集与查询
- 3.12:系统监控与告警 系统监控与告警——Prometheus + Grafana 指标监控
生产级 docker-compose.yml 模板
以下是一个 Web + PostgreSQL + Redis + Worker 的完整编排:
services:
web:
build: ./web
image: myapp/web:latest
ports:
- "8080:8080"
env_file: ./config/prod.env
environment:
- NODE_ENV=production
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
# 前提:web 镜像需内置 curl,否则探针永远失败
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
reservations:
cpus: "0.5"
memory: 256M
restart: unless-stopped
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
networks:
- frontend
- backend
worker:
build: ./worker
image: myapp/worker:latest
env_file: ./config/prod.env
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
deploy:
resources:
limits:
cpus: "2.0"
memory: 1G
restart: unless-stopped
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
networks:
- backend
db:
image: postgres:16-alpine
env_file: ./config/prod.env
volumes:
- pgdata:/var/lib/postgresql/data
- ./backup:/backup
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
deploy:
resources:
limits:
memory: 1G
restart: unless-stopped
networks:
- backend
redis:
image: redis:7-alpine
volumes:
- redisdata:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
deploy:
resources:
limits:
memory: 256M
restart: unless-stopped
networks:
- backend
volumes:
pgdata:
redisdata:
networks:
frontend:
backend:
1. 健康检查与启动顺序
depends_on 控制服务启动顺序。搭配 healthcheck 确保依赖服务完全就绪,而非仅进程启动。
| condition | 含义 |
|---|---|
| service_started | 容器进程启动即可(默认) |
| service_healthy | 等待健康检查通过 |
| service_completed_successfully | 等待一次性任务完成 |
depends_on 只控制 Compose 管理的容器启动顺序。如果数据库在外部(RDS),则无需在 Compose 中定义。1.1 healthcheck 指令详解
healthcheck 的三个阶段参数直接决定服务就绪判断的准确性:
| 参数 | 含义 | 推荐值 |
|---|---|---|
test | 健康检查命令。CMD 直接执行(exec 形式,无 shell),CMD-SHELL 经 shell 执行、支持管道与变量 | 用应用自带的就绪探针,不要只检查进程存活 |
interval | 两次检查间隔 | 10-30s(高频检查浪费资源) |
timeout | 单次检查超时,超时即失败 | 3-5s |
retries | 连续失败 N 次判定 unhealthy | 3-5 |
start_period | 启动宽限期,期间失败不计入 retries | 应用冷启动时间 + 30% 余量 |
# 错误示例:只检查进程存在(进程活着但服务未就绪也会通过;
# 且本专题数据库是 PostgreSQL,应检查 postgres 进程)
healthcheck:
test: ["CMD-SHELL", "pgrep -f postgres"]
# 正确示例:真正发起业务请求验证就绪
# 前提:web 镜像需内置 curl/wget(alpine 精简镜像默认没有,构建时需安装)
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://localhost:8080/health || exit 1"]
interval: 15s
timeout: 5s
retries: 3
start_period: 20s
# 依赖链联动:api 就绪 → web 才启动;web 就绪 → 网关才启动
# 配合 depends_on 形成"就绪链",避免启动风暴与连接竞态
services:
gateway:
depends_on:
web:
condition: service_healthy
api:
condition: service_healthy
2. 环境变量与密钥管理
# config/prod.env(不提交到 Git)
POSTGRES_USER=myapp
POSTGRES_PASSWORD=change_me_in_prod
POSTGRES_DB=myapp
DATABASE_URL=postgres://myapp:change_me_in_prod@db:5432/myapp
# 注意:不要定义用不上的变量(如 REDIS_PASSWORD)——Redis 未配置 --requirepass 时
# 该变量不会生效,反而造成"已加密"的错觉;需要密码就显式加 command
# Docker Compose 中引用
env_file: ./config/prod.env
生产建议:
- .env 文件:加入
.gitignore,仅管理员持有 - Docker Secrets:Swarm 模式用
secrets:挂载而非环境变量 - 外部密钥管理:Vault / AWS Secrets Manager / 阿里云 KMS(通过 init 容器拉取)
- 避免硬编码:docker-compose.yml 中不应出现明文密码
2.1 三种密钥载体对比
| 维度 | 环境变量 | Docker Secrets | .env 文件 |
|---|---|---|---|
| 可见性 | docker inspect / exec 可见 | 以文件挂载到 /run/secrets/,不在环境里 | 同环境变量 |
| 落盘方式 | 明文 | 明文文件(权限 0444) | 明文,须入 .gitignore |
| 动态更新 | 需重建容器 | Swarm 可滚动更新时注入 | 需重建容器 |
| 适用场景 | 非敏感配置(端口、开关) | 单机非 swarm 场景受限(Compose 3.1+ 可 secrets: file:) | 本地开发与部署变量 |
| 审计 | 差 | 中(文件系统可见) | 差 |
2.2 密钥轮换:泄露后的标准动作
密钥迟早会泄露(日志误打、员工离职、外包交接),轮换能力必须提前备好。轮换的要点是"新旧并存、逐步切换、确认后回收":
# 以数据库密码轮换为例(应用读取 DATABASE_URL 环境变量,本专题数据库为 PostgreSQL)
# 1. 在数据库中改密(或新建带新密码的账号)
psql "$DATABASE_URL" -c "ALTER USER myapp WITH PASSWORD 'new_strong_pass';"
# 2. 更新 prod.env 并逐个滚动重建服务
sed -i 's/change_me_in_prod/new_strong_pass/' config/prod.env
docker compose up -d --no-deps --force-recreate web worker
# 3. 观察窗口 10-30 分钟,确认无连接错误
docker compose logs --since 10m web | grep -i "auth\|password" | tail
# 4. 全部切换成功后回收旧密码,并记入轮换台账
psql "$DATABASE_URL" -c "ALTER USER myapp WITH PASSWORD 'retired_old_pass';" # 或 DROP ROLE
# 台账:谁、何时、为何轮换、影响范围
# 5. 轮换节奏:静态密钥 90 天一轮;疑似泄露立即轮换
3. 日志聚合与持久化
Docker 默认日志驱动 json-file 会将日志写入宿主机。生产环境需要集中式日志方案。
Loki + Promtail
# 在 docker-compose.yml 中增加日志服务(loki/promtail 当前主线为 3.x)
services:
loki:
image: grafana/loki:3.4
ports:
- "3100:3100"
volumes:
- ./loki-config.yaml:/etc/loki/config.yaml
command: -config.file=/etc/loki/config.yaml
promtail:
image: grafana/promtail:3.4
volumes:
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- /var/log:/var/log:ro
- ./promtail-config.yaml:/etc/promtail/config.yaml
command: -config.file=/etc/promtail/config.yaml
# 各业务服务使用 loki 日志驱动(需先安装驱动插件:
# docker plugin install grafana/loki-docker-driver)
logging:
driver: loki
options:
loki-url: "http://loki:3100/loki/api/v1/push"
loki-retry-max: "5"
# 注意:max-size 是 json-file 驱动的选项,loki 驱动不支持,
# 截断/轮转交给 Loki 侧 retention 配置
日志配置建议
- 每个服务限制日志文件大小(
max-size: 10m)和保留数量(max-file: 3),防止磁盘被日志写满 - 结构化日志(JSON 格式)便于 Loki 或 ELK 解析
- 敏感信息脱敏后再写入日志
3.1 logging driver 全局配置与 ELK 接入
日志驱动可以在 daemon.json 全局配置,避免每个服务重复书写;业务服务建议统一使用 json-file 本地限制 + 采集端(Promtail/Filebeat)读取,比直接把日志推到远端更抗故障(远端不可达不影响业务):
# /etc/docker/daemon.json —— 全局日志限制
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"log-level": "info"
}
# ELK 接入:Filebeat 采集容器日志发送到 Logstash/ES
# filebeat.yml(挂载到 filebeat 容器)
filebeat.inputs:
- type: container
paths:
- /var/lib/docker/containers/*/*-json.log
json.keys_under_root: true
output.elasticsearch:
hosts: ["http://elasticsearch:9200"]
# docker-compose.yml 中追加
filebeat:
image: docker.elastic.co/beats/filebeat:9.0.0
user: root
volumes:
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- ./filebeat.yml:/usr/share/filebeat/filebeat.yml:ro
command: filebeat -e -strict.perms=false
选型要点:已有 ES 集群选 Filebeat,轻量单节点选 Promtail + Loki;两者都支持从 -json.log 直接采集,无需侵入业务容器。
4. 数据持久化与备份
# 备份 PostgreSQL 数据库
# 在宿主机上执行或通过 cron
docker exec $(docker ps --filter name=db -q) pg_dump -U myapp myapp \
| gzip > /backup/myapp-$(date +%F).sql.gz
# 保留最近 30 天备份,删除旧的
find /backup -name "myapp-*.sql.gz" -mtime +30 -delete
# 数据库数据卷备份(需先停止写入)
docker run --rm -v pgdata:/source -v /backup:/dest alpine \
tar czf /dest/pgdata-$(date +%F).tar.gz -C /source .
数据卷管理原则:
- 有状态服务(DB、Redis)使用命名卷(
volumes:顶层定义),而非绑定挂载(./host:/container) - 定期测试恢复流程——能恢复的备份才有价值
- 关键数据同时备份到异地(云对象存储 S3/OSS)
5. 滚动更新
Compose 支持 deploy 配置中的更新策略(仅 Swarm 模式完整支持,单机 Compose 需手动):
deploy:
replicas: 3
update_config:
parallelism: 1 # 逐台更新
delay: 10s # 每台间隔
order: start-first # 先启动新容器再停旧容器
failure_action: rollback # 失败回滚
单机 Compose 的零停机策略:
# 使用 Nginx 反向代理后,切换上游端口
# 方案:蓝绿部署——同时运行两套 Compose,切换 Nginx 上游
cp docker-compose.yml docker-compose-blue.yml
cp docker-compose.yml docker-compose-green.yml
# 注意:两套文件必须在不同目录或用 -p 指定项目名,
# 否则 Compose 默认按目录名生成项目名,容器/网络/命名卷全部冲突,无法并存
docker compose -p todo-blue -f docker-compose-blue.yml up -d
# 修改 green 的端口映射(如 8081)与命名卷名后:
docker compose -p todo-green -f docker-compose-green.yml up -d
# 验证 green 版本正常后,修改 Nginx 上游指向新端口
# 停止 old 版本
docker compose -p todo-blue down
5.1 Swarm 回滚与监控
# 先部署到 Swarm:docker stack deploy -c docker-compose.yml todo
# 部署后服务名带栈名前缀(todo_web),以下命令按实际服务名执行
# 手动触发滚动更新(Swarm 模式)
docker service update --image myapp/web:1.2.3 --update-parallelism 1 todo_web
# 指定失败自动回滚策略
docker service update \
--update-failure-action rollback \
--update-monitor 30s \
--update-max-failure-ratio 0.3 \
todo_web
# 新版本有严重问题,一键回滚到上一个版本
docker service rollback todo_web
# 查看更新/回滚状态
docker service ps todo_web --no-trunc | head
回滚的本质是"镜像版本可回溯"。生产约定:每个发布镜像必须打不可变 tag(git SHA 或构建号),严禁覆盖 latest;回滚目标永远指向上一版不可变 tag,而不是重新构建。
6. 生产 Checklist
| 类别 | 检查项 |
|---|---|
| 资源 | 每个服务设置了 deploy.resources.limits,防止单个服务耗尽宿主机资源 |
| 重启 | 使用 restart: unless-stopped,确保意外退出后自动恢复 |
| 健康检查 | 关键服务配置 healthcheck,依赖服务使用 condition: service_healthy |
| 日志 | 限制日志文件大小和数量,配置集中式日志采集 |
| 密钥 | 密码/API Key 不硬编码,通过 env_file 或 Docker Secrets 注入 |
| 网络 | 使用自定义网络隔离服务,前端服务无需访问数据库 |
| 备份 | 数据库配置定时备份脚本,定期验证恢复 |
| 监控 | 服务暴露 /health 端点,配合 3.12:系统监控与告警 Prometheus + Grafana 监控 |
7. 多环境 Compose 文件管理
开发/测试/生产共用一份 Compose 定义,差异部分用 override 文件与变量插值表达,避免维护三份互相漂移的 yml:
# 基础文件 docker-compose.yml —— 所有环境一致
services:
web:
image: myapp/web:${WEB_TAG:-latest}
ports:
- "${WEB_PORT:-8080}:8080"
environment:
- LOG_LEVEL=${LOG_LEVEL:-info}
# 生产覆盖 docker-compose.prod.yml —— 只写差异
services:
web:
image: myapp/web:${WEB_TAG} # 强制要求显式 tag
deploy:
resources:
limits:
memory: 1G
restart: always
# 开发覆盖 docker-compose.override.yml —— 自动被 docker compose up 加载
services:
web:
build: ./web # 开发用源码构建替代镜像
volumes:
- ./web/src:/app/src # 热更新挂载
ports:
- "8080:8080"
# 不同环境启动方式
docker compose up -d # 开发(自动合并 override)
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d # 生产
docker compose --env-file .env.prod -f docker-compose.yml -f docker-compose.prod.yml up -d
# 查看合并后的最终配置(排查覆盖问题)
docker compose config
docker compose config --services
# 变量插值常用技巧
# ${VAR} —— 必填,缺失报错
# ${VAR:-default} —— 缺失用默认值
# ${VAR?err} —— 缺失时报错并提示自定义信息
常见错误
1. depends_on 不等待健康检查通过
症状:服务启动后连接数据库失败。原因:使用了 condition: service_started 而非 service_healthy,或目标服务未配置 healthcheck。
排查:执行 docker inspect --format='{{json .State.Health}}' <container> 查看健康检查状态。
2. 容器 OOMKilled 被系统杀掉
症状:docker ps -a 显示容器退出码 137。原因:内存超出 deploy.resources.limits 设定值。
排查:docker stats 实时观察内存使用,适当调高 limits 或优化应用内存占用。
3. env_file 路径错误导致环境变量缺失
症状:容器启动正常但应用报数据库连接失败。原因:env_file 使用相对路径时,执行 docker compose 的工作目录与文件实际位置不一致。
排查:执行 docker exec <container> env | grep POSTGRES 确认变量是否存在。
4. 日志文件撑满磁盘
症状:宿主机磁盘使用率飙升至 100%。原因:未配置 max-size 和 max-file,json-file 驱动默认不限制日志大小。
排查:du -sh /var/lib/docker/containers/*/ 查看各容器日志占用,立即清理后补上日志限制配置。
5. 有状态服务数据卷未挂载导致数据丢失
症状:容器重建后数据库数据消失。原因:未使用命名卷(named volume),或误用了 docker compose down -v。
排查:docker volume ls 确认命名卷是否存在,docker volume inspect pgdata 查看挂载详情。
6. 绑定挂载导致容器内文件权限错乱
症状:容器内进程报 Permission denied 或写入失败。原因:绑定挂载(./host:/container)把宿主机文件的所有权(UID/GID)直接暴露给容器,镜像内用户与宿主机用户不一致。
排查:docker exec <container> id 对比镜像内用户与宿主机文件属主;生产环境有状态服务一律用命名卷,避免把宿主机目录挂进容器。
7. 容器内无法解析服务名
症状:容器内 ping db 失败但 IP 可通。原因:容器未加入 Compose 自定义网络(默认桥接网络不做服务名解析),或与手动指定的 container_name 混用。
排查:确认服务在 networks: 列表里(自定义网络上 Docker DNS 按容器名/服务名解析);手动指定的 container_name 不影响服务名解析,访问时用 Compose 服务名(db)即可。
最佳实践
1. 始终为关键服务配置健康检查
Web、数据库、缓存等服务都应定义 healthcheck,并在 depends_on 中使用 condition: service_healthy,避免服务未就绪就启动下游。
2. 限制资源防止单点崩溃
每个服务必须设置 deploy.resources.limits,尤其是内存限制。无限制的容器可能耗尽宿主机全部资源,导致所有服务不可用。
3. 敏感信息走密钥管理而非环境变量
数据库密码、API Key 等通过 env_file 注入(.env 文件不入库),生产环境优先使用 Docker Secrets 或外部密钥管理系统(Vault、KMS)。
4. 网络隔离最小权限
前端服务只加入 frontend 网络,后端服务只加入 backend 网络。Web 服务同时加入两个网络作为网关,数据库和 Redis 不暴露到前端网络。
5. 定期验证备份可恢复性
备份脚本容易写,恢复流程很少测。建议每月至少做一次恢复演练,确保备份文件完整且恢复步骤正确。
6. 镜像使用不可变 tag 并保留回滚路径
发布用 git SHA 或构建号打 tag(myapp/web:1.2.3-a1b2c3d),永不覆盖 latest。回滚 = 切换回上一个不可变 tag,而不是重新构建——重新构建的产物可能与线上不一致。
7. 切换前预热、切换后观察
蓝绿/滚动更新切流量前,先在新端口上对新版本做健康检查与日志观察(预热缓存、确认无报错),再改 Nginx upstream 或 Swarm 权重;切换后保留旧版本容器 24 小时再清理,发现异常可秒级切回。
练习题
1. 完善健康检查配置
给下面的 docker-compose.yml 补充缺失的健康检查和资源限制:
services:
web:
image: nginx:alpine
# 补充 healthcheck(curl localhost:80 返回 200 即健康)
# 补充资源限制:最多 256M 内存、0.5 CPU
api:
image: node:22-alpine
# 补充 depends_on web(等待 service_healthy)
2. 设计备份策略
为一个包含 PostgreSQL + Redis 的 Compose 项目编写每日备份脚本要求:(a) pg_dump 导出数据库并 gzip 压缩;(b) 备份文件按日期命名,保留最近 15 天;(c) 备份完成后打印文件大小。
3. 滚动更新演练
在单机环境下,如何利用两个 Compose 文件实现蓝绿部署零停机?请写出关键步骤:创建 blue/green 两个文件、修改端口映射、切换 Nginx upstream、验证并下线旧版本。
4. 密钥轮换演练
在本地 Compose 环境完整执行一次数据库密码轮换:改密 → 更新 prod.env → 重建服务 → 验证连接 → 回收旧密码,记录每一步命令与观察点。
学习检查点
学完本章后,请检验自己是否掌握以下内容:
| 检查项 | 自测问题 | 验证方法 |
|---|---|---|
| 概念理解 | 能用自己的话解释 Docker Compose 健康检查的作用和原理 | 尝试向他人讲解 |
| 命令操作 | 能不查文档完成 docker-compose.yml 的健康检查配置和资源限制设置 | 在终端实际执行 |
| 原理掌握 | 能说出 Docker Secrets 与环境变量在密钥管理上的区别 | 画出流程图 |
| 故障排查 | 能独立排查容器启动顺序问题、OOMKilled、日志撑满磁盘等常见错误 | 模拟故障并修复 |
| 最佳实践 | 能说明为什么生产环境需要限制容器资源和配置日志大小限制 | 对比不同方案 |
本章总结
Compose 生产化的核心是把不确定性变成配置:健康检查定义就绪状态,资源限制划定故障边界,密钥与日志外置避免泄露和磁盘写满,命名卷加定期演练的备份保障数据安全。零停机更新可借助 Swarm 的 update_config 或单机蓝绿切换实现,最后以一份生产 Checklist 兜底常见工程遗漏。
延伸阅读
- 3.1:Docker 容器入门 Docker 容器入门(Compose 基础)
- 5.2:Docker 生产实践 Docker 生产实践(多阶段构建、安全加固)
- 4.8:集中式日志管理 集中式日志管理——Loki + Promtail
- 3.12:系统监控与告警 系统监控与告警——Prometheus + Grafana