3.2 Docker Compose 生产部署实战

预计阅读时间:16 分钟

📖 目录

3.1:Docker 容器入门 介绍了 Docker Compose 的基本用法。生产环境中的 Compose 部署需要更多工程考量:健康检查依赖、资源限制、日志聚合、密钥管理和滚动更新。本文用一个完整的多服务应用示例,展示从 docker-compose.yml 到生产级部署的全过程。

学习目标

  • 掌握用 healthcheckdepends_on 的 condition 控制服务启动顺序
  • 能通过 deploy.resources 限制容器资源,防止单个服务耗尽宿主机
  • 理解 env_file、Docker Secrets 与外部 KMS 的分层密钥管理
  • 能配置日志大小限制与 Loki + Promtail 集中式日志采集
  • 掌握命名卷持久化与可验证的备份恢复流程
  • 能实现 Swarm 滚动更新与单机蓝绿部署的零停机发布

前置知识

生产级 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 次判定 unhealthy3-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:本地开发与部署变量
审计中(文件系统可见)
原则 密码/API Key 这类真正的密钥优先走外部 KMS 或 Vault;Docker Secrets 解决"不进环境变量"的问题但不解决"落盘明文"问题;开发环境用 .env 足够,生产环境一律升级。

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 天一轮;疑似泄露立即轮换
轮换的自动化 Vault 的 dynamic secret 或 AWS Secrets Manager 的自动轮换可省去人工步骤;即使先用手动流程,也要把脚本放进仓库(密钥本身不入库),让轮换"可排练"。

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}      —— 缺失时报错并提示自定义信息
命名约定 环境后缀(prod/staging/dev)跟随环境名,.env.prod 这类文件加入 .gitignore 或加密存储;CI 里只保留 compose 文件入 Git,密钥全部经环境变量注入。

常见错误

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-sizemax-filejson-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 兜底常见工程遗漏。

延伸阅读

↑ 回到顶部