Docker Compose 生产级部署实战指南

Docker Compose 生产级部署实战指南

目标读者 :有 Docker 基础的开发者

难度 :进阶

字数 :约 4000 字

适用场景:中小规模微服务、API 后端、Web 应用的生产环境部署


一、Docker Compose 基础结构回顾

Compose 的核心是一个 docker-compose.yml 文件,由四大顶层元素构成:

yaml 复制代码
version: "3.9"

services:
  app:
    image: myapp:latest
    ports:
      - "8080:8080"

networks:
  backend:
    driver: overlay

volumes:
  data:

configs:
  app_config:
    file: ./config/app.yml
元素 作用 生产要点
services 定义容器服务 镜像锁定 tag,避免用 latest
networks 定义容器间通信拓扑 使用 overlay 驱动(Swarm)或自定义 bridge
volumes 持久化存储声明 优先命名卷,避免匿名卷
configs 注入配置文件(Swarm only) 配合 secrets 管理敏感数据

⚠️ 生产第一原则 :所有镜像必须指定精确版本(如 postgres:15.4-alpine),杜绝 latest 标签。


二、多环境管理:Compose 文件拆分策略

2.1 三层拆分法

csharp 复制代码
docker-compose.yml         # 公共基础配置
docker-compose.override.yml # 开发环境(默认自动加载)
docker-compose.prod.yml     # 生产环境覆盖

生产启动命令

bash 复制代码
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

2.2 最佳实践示例

docker-compose.yml(公共部分):

yaml 复制代码
version: "3.9"

services:
  app:
    image: myapp:${APP_VERSION:-latest}
    restart: unless-stopped
    networks:
      - frontend
      - backend

  redis:
    image: redis:7.2-alpine
    restart: unless-stopped
    networks:
      - backend

networks:
  frontend:
  backend:

docker-compose.prod.yml(生产覆盖):

yaml 复制代码
version: "3.9"

services:
  app:
    deploy:
      replicas: 3
      resources:
        limits:
          cpus: "1.0"
          memory: 512M
        reservations:
          cpus: "0.25"
          memory: 128M
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

  redis:
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 3

volumes:
  redis_data:

💡 搭配 .env 文件区分环境变量,见第四章。


三、健康检查与依赖启动顺序控制

3.1 healthcheck 配置详解

yaml 复制代码
services:
  postgres:
    image: postgres:15.4-alpine
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 10s     # 每次检查间隔
      timeout: 5s        # 单次检查超时
      retries: 5         # 连续失败几次标记为 unhealthy
      start_period: 30s  # 启动宽限期,不计入 retries

3.2 depends_on 的三种模式

yaml 复制代码
services:
  app:
    depends_on:
      postgres:
        condition: service_healthy   # 等待健康检查通过
      redis:
        condition: service_started   # 只等启动(默认)
      migrator:
        condition: service_completed_successfully  # 等待一次性任务完成

⚠️ 常见坑depends_on 只控制启动顺序,不保证容器内部服务已就绪。必须配合 healthcheck 才能实现真正的依赖等待。

3.3 初始化容器的模式

yaml 复制代码
services:
  db_migrate:
    image: myapp:${APP_VERSION}
    command: ["./wait-for-it.sh", "postgres:5432", "--", "npm", "run", "migrate"]
    depends_on:
      postgres:
        condition: service_healthy
    profiles:
      - init   # 仅手动执行,不随主服务启动

手动运行:

bash 复制代码
docker compose --profile init run --rm db_migrate

四、环境变量管理最佳实践

4.1 分层变量体系

bash 复制代码
.env              # 公共变量(提交到 git 的模板)
.env.prod         # 生产敏感变量(不提交 git)
.env.local        # 开发环境覆盖(不提交 git)

4.2 .env 文件示例

bash 复制代码
# .env
APP_VERSION=1.5.2
APP_ENV=production
LOG_LEVEL=info
TZ=Asia/Shanghai

# .env.prod(不提交 git)
DB_PASSWORD=***
REDIS_PASSWORD=***
JWT_SECRET=your-2...here
API_KEY=***

4.3 在 Compose 中引用

yaml 复制代码
services:
  app:
    image: myapp:${APP_VERSION}
    environment:
      - NODE_ENV=${APP_ENV}
      - LOG_LEVEL=${LOG_LEVEL}
      - DB_PASSWORD=***
      - REDIS_PASSWORD=***
      - TZ=${TZ}
    env_file:
      - .env           # 自动加载

4.4 禁止的行为

❌ 禁止做法 ✅ 正确做法
在 Compose 中硬编码密码 使用 ${VAR} 引用 .env
将 .env.prod 提交到 git 添加到 .gitignore
同一个 .env 文件跨所有环境 分环境维护
在镜像中嵌入 secrets 使用 Docker secrets 或 env_file 注入

💡 高阶技巧 :使用 docker compose config 验证变量替换是否正确:

bash 复制代码
docker compose -f docker-compose.yml -f docker-compose.prod.yml config

五、数据持久化:三种方式的选择

5.1 命名卷(Named Volumes)--- 首选

yaml 复制代码
services:
  postgres:
    volumes:
      - pg_data:/var/lib/postgresql/data

volumes:
  pg_data:  # Docker 管理,存储在 /var/lib/docker/volumes/

适用场景:数据库、消息队列、任何需要 Docker 管理生命周期的数据。

优点 :自动备份友好,docker volume 命令可管理,跨节点迁移(Swarm)支持。

5.2 绑定挂载(Bind Mounts)--- 谨慎使用

yaml 复制代码
services:
  nginx:
    volumes:
      - type: bind
        source: /host/path/to/static
        target: /usr/share/nginx/html
        read_only: true   # 生产中务必只读

适用场景:开发热重载、日志收集、Nginx 静态文件。

风险:依赖宿主机路径、权限问题、不可跨 Swarm 节点。

5.3 tmpfs --- 内存级临时数据

yaml 复制代码
services:
  app:
    tmpfs:
      - /tmp:noexec,nosuid,size=128M
  # 或者长语法:
    volumes:
      - type: tmpfs
        target: /var/run/app
        tmpfs:
          size_mb: 64

适用场景:临时缓存、session 存储、敏感数据(不希望落盘)。

5.4 决策树

markdown 复制代码
数据需要持久化?
├─ 是 → 需要跨节点共享?
│    ├─ 是 → 网络存储(NFS/Ceph → 命名卷 + volume driver)
│    └─ 否 → 单节点 → 命名卷 ✅
└─ 不需要 → 数据可丢失?
     ├─ 是 → tmpfs
     └─ 否 → ... 那你需要持久化!

⚠️ 常见生产事故 :忘记声明 volumes: 顶层键,导致 Docker 创建匿名卷,docker compose down -v 时被一并删除。


六、网络模式与服务发现

6.1 自定义网络

yaml 复制代码
networks:
  frontend:
    driver: bridge
    ipam:
      config:
        - subnet: 172.20.0.0/16
          gateway: 172.20.0.1
  backend:
    driver: bridge
    internal: true   # 对外隔离,无法访问外网

6.2 服务发现与隔离

yaml 复制代码
services:
  nginx:
    networks:
      - frontend    # 暴露对外
      - backend     # 可访问 API

  app:
    networks:
      - backend
      - internal
    # 不接入 frontend,客户端无法直接访问

  postgres:
    networks:
      - internal    # 完全隔离

规则

  • 各服务按最小权限原则只加入必需的网络
  • 同网络内,服务名等同 DNS 主机名(app, redis, postgres
  • internal: true 的网络没有对外网关,提升安全性

6.3 固定 IP(需要时)

yaml 复制代码
services:
  nginx:
    networks:
      frontend:
        ipv4_address: 172.20.0.10

💡 生产中最常用的布局:一层反向代理网络(frontend)+ 一层业务网络(backend)+ 一层数据层网络(internal)。


七、日志配置

7.1 驱动选择对比

驱动 适用场景 存储位置
json-file 单机、简单调试 /var/lib/docker/containers/<id>/
local 性能敏感场景 Docker 管理(二进制格式)
syslog 集中式日志已有设施 syslog 服务器
fluentd 需要日志转发到 Elastic 等 Fluentd 收集器
gelf Graylog 集成 Graylog 服务器
loki Grafana 全家桶 Loki 实例
awslogs AWS 环境 CloudWatch

7.2 日志轮转配置(json-file)

yaml 复制代码
services:
  app:
    logging:
      driver: json-file
      options:
        max-size: "10m"      # 单个日志文件最大 10MB
        max-file: "3"        # 最多保留 3 个文件
        compress: "true"     # 轮转后 gzip 压缩
        tag: "{{.Name}}/{{.ID}}"  # 日志标签,便于区分

7.3 集中式日志(Fluentd 示例)

yaml 复制代码
services:
  app:
    logging:
      driver: fluentd
      options:
        fluentd-address: "localhost:24224"
        fluentd-async-connect: "true"
        tag: "myapp.{{.Name}}"

  fluentd:
    image: fluent/fluentd:v1.16
    volumes:
      - ./fluentd.conf:/fluentd/etc/fluent.conf
    ports:
      - "24224:24224"
    networks:
      - logging

全局默认配置(在 Compose 文件顶层设置):

yaml 复制代码
logging:
  driver: json-file
  options:
    max-size: "10m"
    max-file: "3"

⚠️ 常见坑 :日志驱动 none 会导致 docker logs 无法查看日志,生产排障时非常痛苦。除非磁盘空间极度紧张,否则不要用 none


八、安全最佳实践

8.1 非 root 运行

yaml 复制代码
services:
  app:
    image: node:18-alpine
    user: "1000:1000"        # 指定非 root 用户
    # 或者使用下面方式(更灵活):
    # user: "${UID:-1000}:${GID:-1000}"

Dockerfile 侧配合

dockerfile 复制代码
RUN addgroup -g 1000 -S appgroup && \
    adduser -u 1000 -S appuser -G appgroup
USER appuser

8.2 Secrets 管理

yaml 复制代码
# 生产 Swarm 模式
secrets:
  db_password:
    file: ./secrets/db_password.txt   # 仅 swarm 模式支持
  jwt_secret:
    external: true                     # 从外部已创建的 secret 获取

services:
  app:
    secrets:
      - db_password
      - jwt_secret
# 容器内路径:/run/secrets/db_password

单机替代方案(利用 tmpfs + env_file):

bash 复制代码
# 构建时注入,运行时不落盘
docker compose run --rm -e DB_PASSWORD=*** secrets/db_password.txt) app

8.3 资源限制

yaml 复制代码
services:
  app:
    deploy:
      resources:
        limits:
          cpus: "1.5"          # 最多 1.5 核
          memory: 512M         # 最多 512MB
          pids: 100            # 最多 100 个进程
        reservations:
          cpus: "0.5"          # 保证 0.5 核
          memory: 256M         # 保证 256MB
    oom_kill_disable: false    # OOM 时允许系统杀掉容器
    restart: unless-stopped

8.4 只读根文件系统

yaml 复制代码
services:
  app:
    read_only: true
    tmpfs:
      - /tmp
      - /var/run

⚠️ 配合 read_only: true 时,应用需要写入的目录(如 /tmp、缓存目录)必须用 tmpfs 或 volume 显式声明。

8.5 安全清单

  • 镜像使用 -alpine-slim 变体减少攻击面
  • 启用 read_only: true
  • 指定 user: 非 root
  • Secrets 不写入环境变量(通过文件注入)
  • 日志中不包含敏感信息
  • 网络采用最小权限隔离
  • 配置内存/CPU/进程数限制

九、部署流程

9.1 单机生产部署

bash 复制代码
# 1. 准备环境
mkdir -p /opt/myapp/{config,data,logs,secrets}
cd /opt/myapp

# 2. 拉取配置文件
git clone https://github.com/org/myapp-deploy.git .

# 3. 创建 secrets
echo "prod-db-password" > secrets/db_password.txt
chmod 600 secrets/db_password.txt

# 4. 预拉取镜像
docker compose -f docker-compose.yml -f docker-compose.prod.yml pull

# 5. 启动服务
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

# 6. 验证
docker compose ps
docker compose logs --tail=50

# 7. 滚动更新(有变更时)
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --no-deps --scale app=2
# 逐步替换旧容器...
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --no-deps --scale app=3

9.2 Docker Swarm 部署

yaml 复制代码
# docker-stack.yml(使用 deploy 块)
version: "3.9"

services:
  app:
    image: myapp:${APP_VERSION}
    deploy:
      mode: replicated
      replicas: 3
      update_config:
        parallelism: 1
        delay: 10s
        order: start-first   # 先启动新容器,再停旧容器
      rollback_config:
        parallelism: 1
        delay: 5s
        order: stop-first
      restart_policy:
        condition: any
        delay: 5s

部署命令

bash 复制代码
# 初始化 swarm(如未初始化)
docker swarm init --advertise-addr 192.168.1.100

# 部署 stack
docker stack deploy -c docker-compose.yml -c docker-compose.prod.yml myapp

# 查看服务
docker stack services myapp

# 滚动更新
docker service update --image myapp:1.6.0 myapp_app

# 回滚
docker service rollback myapp_app

9.3 零宕机更新策略

yaml 复制代码
deploy:
  update_config:
    parallelism: 2       # 每次并行更新 2 个副本
    delay: 10s           # 每组更新后等待 10 秒
    order: start-first   # 先启动新版本,再停止旧版本
    failure_action: rollback  # 更新失败自动回滚
    monitor: 30s         # 监控新容器 30s 健康状态

十、常见坑与调试技巧

10.1 经典问题速查表

问题 现象 解决方案
容器启动后立即退出 docker logs 无输出 检查 CMD 是否前台运行;检查 entrypoint 权限
服务间无法解析主机名 连接被拒绝 检查是否在同一网络;使用 docker compose exec app ping redis
Volume 权限拒绝 PostgreSQL 启动失败 容器用户 UID 与 volume 所有者不匹配;使用 user: "999:999"
.env 变量不生效 docker compose config 显示空值 检查 .env 文件位置(必须与 compose 同目录)
端口冲突 port is already allocated lsof -i :8080 找到占用进程;或外层使用反向代理
日志撑爆磁盘 docker system df 看到巨量日志 配置 max-size/max-file;定期 docker system prune

10.2 调试三板斧

bash 复制代码
# 1. 查看配置是否正确解析
docker compose -f docker-compose.yml -f docker-compose.prod.yml config

# 2. 查看实时日志(加时间戳、tail 追踪)
docker compose logs -f --tail=100 --timestamps app

# 3. 进入容器内部分析
docker compose exec app sh
# 或使用临时调试容器(共享网络)
docker run --rm -it --network myapp_backend nicolaka/netshoot

10.3 实用命令快速参考

bash 复制代码
# 清理:删除所有停止的容器 + 无用网络 + 悬空镜像 + 构建缓存
docker system prune -a --volumes

# 资源监控
docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}"

# 检查容器内的环境变量
docker compose exec app env | grep -E "DB_|REDIS_"

# 导出 compose 配置(调试变量替换)
docker compose -f docker-compose.yml -f docker-compose.prod.yml config > /tmp/resolved.yml

# 查看挂载的 volume 实际路径
docker volume inspect myapp_pg_data --format '{{.Mountpoint}}'

附录:生产级 Compose 模板

yaml 复制代码
# docker-compose.yml ------ 生产级模板
version: "3.9"

x-logging: &default-logging
  driver: json-file
  options:
    max-size: "10m"
    max-file: "3"
    compress: "true"

x-deploy: &default-deploy
  resources:
    limits:
      cpus: "1.0"
      memory: 512M
    reservations:
      cpus: "0.25"
      memory: 128M
  restart_policy:
    condition: any
    delay: 5s

services:
  app:
    image: myapp:${APP_VERSION:?APP_VERSION is required}
    user: "1000:1000"
    read_only: true
    tmpfs:
      - /tmp:size=64M
    environment:
      - NODE_ENV=${APP_ENV:-production}
      - LOG_LEVEL=${LOG_LEVEL:-info}
      - TZ=Asia/Shanghai
    env_file:
      - .env
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    networks:
      - backend
    logging: *default-logging
    deploy: *default-deploy

  postgres:
    image: postgres:15.4-alpine
    user: "999:999"
    volumes:
      - pg_data:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: ${DB_NAME}
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    networks:
      - internal
    logging: *default-logging

  redis:
    image: redis:7.2-alpine
    user: "999:999"
    volumes:
      - redis_data:/data
    command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}"]
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
      interval: 10s
      timeout: 5s
      retries: 3
    networks:
      - internal
    logging: *default-logging

networks:
  backend:
    driver: bridge
  internal:
    driver: bridge
    internal: true

volumes:
  pg_data:
  redis_data:

总结

维度 核心要点
镜像 锁定版本标签,减少攻击面,优先 -alpine
配置 拆分环境,.env + profiles 组合管理
健康 依赖服务必须配合 healthcheck,start_period 设置合理
存储 命名卷优先,绑定挂载有限使用,tmpfs 用于临时数据
网络 多网络分层隔离,internal: true 保护数据层
日志 配置轮转,生产环境接入集中式日志系统
安全 非 root 用户、只读文件系统、资源限制、secrets 文件注入
部署 Swarm stack 或 docker compose,配合更新策略实现零宕机

上一篇《Docker 容器化入门到实践》

作者CaoZH · 发布于 2026-06-18