Docker Compose 多服务编排指南 – 优易云科技技术博客

Docker Compose 多服务编排指南:从开发到生产的完整实践

分类:后端与运维 | 标签:Docker, Compose, 微服务, DevOps, 容器化

发布时间:2026-04-18 | 作者:优易云科技运维团队

引言

在我们的日常工作中,Docker Compose 是不可或缺的编排工具。从本地开发环境搭建、CI/CD 流水线中的集成测试,到小型生产环境的部署,Compose 几乎覆盖了容器化全生命周期的每一个阶段。过去两年,我们团队维护了 20+ 个 Compose 编排配置,涵盖了从简单的 Web 应用到包含 15 个微服务的复杂物联网平台。本文将系统性地分享我们在实际项目中积累的 Compose 最佳实践。

Compose 文件结构最佳实践

一个组织良好的 Compose 文件是可维护性的基础。我们推荐使用 YAML 锚点和多文件覆盖模式来管理不同环境的配置差异。

基础配置文件 + 环境覆盖文件

# docker-compose.yml — 基础配置(所有环境共享)
version: "3.9"

x-common-env: &common-env
  TZ: Asia/Shanghai
  NODE_ENV: production
  LOG_LEVEL: info

x-logging: &default-logging
  driver: json-file
  options:
    max-size: "50m"
    max-file: "5"
    tag: "{{.Name}}/{{.ID}}"

services:
  # --- API 网关 ---
  gateway:
    image: youyiyun/gateway:${IMAGE_TAG:-latest}
    container_name: yy-gateway
    restart: unless-stopped
    environment:
      <<: *common-env
      PORT: 8080
      UPSTREAM_URL: http://backend:3000
    ports:
      - "${GATEWAY_PORT:-80}:8080"
    networks:
      - frontend
      - backend
    depends_on:
      backend:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s
    logging: *default-logging

  # --- 后端服务 ---
  backend:
    image: youyiyun/backend:${IMAGE_TAG:-latest}
    container_name: yy-backend
    restart: unless-stopped
    environment:
      <<: *common-env
      DATABASE_URL: postgresql://app:${DB_PASSWORD}@postgres:5432/youyiyun
      REDIS_URL: redis://redis:6379/0
      MQTT_BROKER: mqtt://emqx:1883
    networks:
      - backend
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
      emqx:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 20s
    logging: *default-logging

  # --- PostgreSQL 数据库 ---
  postgres:
    image: postgres:16-alpine
    container_name: yy-postgres
    restart: unless-stopped
    environment:
      POSTGRES_DB: youyiyun
      POSTGRES_USER: app
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      PGDATA: /var/lib/postgresql/data/pgdata
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - backend
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d youyiyun"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    logging: *default-logging

  # --- Redis 缓存 ---
  redis:
    image: redis:7-alpine
    container_name: yy-redis
    restart: unless-stopped
    command: redis-server --appendonly yes --maxmemory 512mb --maxmemory-policy allkeys-lru
    volumes:
      - redis_data:/data
    networks:
      - backend
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 3
    logging: *default-logging

  # --- EMQX MQTT Broker ---
  emqx:
    image: emqx/emqx:5.7.0
    container_name: yy-emqx
    restart: unless-stopped
    environment:
      EMQX_LISTENERS__TCP__DEFAULT__BIND: "0.0.0.0:1883"
      EMQX_LISTENERS__WS__DEFAULT__BIND: "0.0.0.0:8083"
      EMQX_LOADED_PLUGINS: "emqx_auth_http,emqx_management"
    volumes:
      - emqx_data:/opt/emqx/data
      - emqx_log:/opt/emqx/log
    ports:
      - "1883:1883"
      - "8083:8083"
      - "18083:18083"
    networks:
      - backend
    healthcheck:
      test: ["CMD", "emqx", "ping"]
      interval: 15s
      timeout: 10s
      retries: 3
      start_period: 30s
    logging: *default-logging

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
    internal: true  # 内部网络,不暴露到宿主机

volumes:
  postgres_data:
    driver: local
  redis_data:
    driver: local
  emqx_data:
    driver: local
  emqx_log:
    driver: local
# docker-compose.override.yml — 开发环境覆盖(自动合并)
version: "3.9"

services:
  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile.dev
    environment:
      NODE_ENV: development
      LOG_LEVEL: debug
    volumes:
      - ./backend/src:/app/src  # 热重载
      - ./backend/package.json:/app/package.json
    command: npm run dev

  postgres:
    ports:
      - "5432:5432"  # 开发时暴露端口方便本地工具连接
# docker-compose.prod.yml — 生产环境覆盖
version: "3.9"

services:
  gateway:
    ports:
      - "443:8443"
    environment:
      SSL_CERT: /etc/ssl/certs/server.crt
      SSL_KEY: /etc/ssl/private/server.key
    volumes:
      - /etc/ssl/certs:/etc/ssl/certs:ro
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: "1.0"
        reservations:
          memory: 256M
          cpus: "0.5"

  backend:
    deploy:
      resources:
        limits:
          memory: 2G
          cpus: "2.0"
        reservations:
          memory: 1G
          cpus: "1.0"
      replicas: 2  # 生产环境 2 实例

  postgres:
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}  # 必须通过环境变量传入
    deploy:
      resources:
        limits:
          memory: 4G
          cpus: "2.0"

# 使用方式:
# 开发:docker compose up -d  (自动合并 override)
# 生产:docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

多阶段构建优化镜像体积

在生产环境中,镜像体积直接影响部署速度和存储成本。我们在 Node.js 和 Go 项目中全面采用多阶段构建,将镜像体积控制在合理范围内。

# --- Node.js 项目多阶段构建 Dockerfile ---

# ========== 阶段1:依赖安装 ==========
FROM node:20-alpine AS deps
WORKDIR /app

# 先复制依赖描述文件,利用 Docker 层缓存
COPY package.json package-lock.json ./
# 只安装生产依赖
RUN npm ci --omit=dev && 
    npm cache clean --force

# ========== 阶段2:构建 ==========
FROM node:20-alpine AS builder
WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci

COPY . .

# 构建产物
RUN npm run build && 
    # 清理不需要的文件
    rm -rf node_modules/.cache

# ========== 阶段3:运行 ==========
FROM node:20-alpine AS runner

# 安全:使用非 root 用户
RUN addgroup --system --gid 1001 appgroup && 
    adduser --system --uid 1001 appuser

WORKDIR /app

# 只复制必要的文件
COPY --from=builder --chown=appuser:appgroup /app/dist ./dist
COPY --from=deps --chown=appuser:appgroup /app/node_modules ./node_modules
COPY --from=builder --chown=appuser:appgroup /app/package.json ./

# 设置环境变量
ENV NODE_ENV=production
ENV PORT=3000

# 切换到非 root 用户
USER appuser

EXPOSE 3000

# 健康检查
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 
  CMD wget -qO- http://localhost:3000/api/health || exit 1

CMD ["node", "dist/server.js"]
# --- Go 项目多阶段构建 Dockerfile ---

# ========== 阶段1:构建 ==========
FROM golang:1.22-alpine AS builder

WORKDIR /build

# 利用 Go module 缓存
COPY go.mod go.sum ./
RUN go mod download

# 编译
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 
    go build -ldflags="-w -s -X main.version=${VERSION:-unknown}" 
    -o /build/server ./cmd/server

# ========== 阶段2:运行(scratch 基础镜像,仅 ~15MB)==========
FROM scratch

# CA 证书(HTTPS 请求需要)
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

# 时区数据
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
ENV TZ=Asia/Shanghai

COPY --from=builder /build/server /server

EXPOSE 8080

ENTRYPOINT ["/server"]

Docker多阶段构建流程示意图

镜像体积优化数据对比

在我们的实际项目中,多阶段构建带来的体积缩减效果显著:

  • Node.js 后端服务:单阶段 1.2GB → 多阶段 280MB(减少 77%)
  • Go 微服务:单阶段 850MB → 多阶段 15MB(减少 98%)
  • Python 数据处理服务:单阶段 1.5GB → 多阶段 420MB(减少 72%)

健康检查配置详解

健康检查是生产环境稳定运行的保障。没有健康检查的容器,Docker 无法判断服务是否真正可用,这会导致错误的依赖判断和负载均衡问题。

# 不同服务的健康检查策略

# PostgreSQL:使用 pg_isready
healthcheck:
  test: ["CMD-SHELL", "pg_isready -U app -d youyiyun -q"]
  interval: 10s
  timeout: 5s
  retries: 5
  start_period: 30s  # 数据库初始化需要较长时间

# Redis:使用 redis-cli ping
healthcheck:
  test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
  interval: 10s
  timeout: 3s
  retries: 3
  start_period: 10s

# Node.js 后端:HTTP 健康检查
healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
  interval: 30s
  timeout: 5s
  retries: 3
  start_period: 20s

# Go 服务:使用内置的健康端点
healthcheck:
  test: ["CMD", "wget", "-qO-", "http://localhost:8080/healthz"]
  interval: 15s
  timeout: 3s
  retries: 3
  start_period: 5s  # Go 编译的二进制启动很快

# Nginx:检查进程是否存在
healthcheck:
  test: ["CMD-SHELL", "pidof nginx || exit 1"]
  interval: 30s
  timeout: 3s
  retries: 2

健康检查参数选择指南

在我们的经验中,以下参数配置适用于大多数场景:

  • interval(检查间隔):数据库类 10s,应用类 30s,静态资源服务 60s
  • timeout(超时时间):通常设为 3-5s,超过此时间视为失败
  • retries(重试次数):3 次是合理的默认值,避免单次网络抖动导致服务重启
  • start_period(启动宽限期):根据服务启动时间设置。Node.js 应用通常 20-30s,Go 服务 5-10s,数据库 30-60s

网络模式选择

Docker 提供了多种网络模式,选择不当会导致性能瓶颈或安全问题。

Bridge 网络(默认)

适用于大多数场景。每个容器通过 veth pair 连接到 Docker 网桥,容器间通过服务名通信。我们推荐为前端服务和后端服务分别创建网络,后端网络标记为 internal,增强安全性。

Host 网络模式

容器直接使用宿主机网络栈,无 NAT 开销,性能最佳。适用于对网络延迟极度敏感的场景(如 MQTT Broker、时序数据库)。但需要注意端口冲突和隔离性降低的问题。

# EMQX 使用 host 网络模式以获得最佳性能
emqx:
  image: emqx/emqx:5.7.0
  network_mode: host
  # 端口直接映射到宿主机,无需 ports 配置
  # 注意:host 模式下容器间不能通过服务名通信

Overlay 网络(Swarm 模式)

用于多节点集群的跨主机容器通信。我们在需要 Docker Swarm 部署的生产环境中使用 Overlay 网络。配合 Swarm 的负载均衡,可以实现透明的服务发现和跨节点路由。

# Docker Swarm 部署配置
networks:
  overlay-net:
    driver: overlay
    attachable: true  # 允许独立容器连接
    driver_opts:
      encrypted: true  # 启用加密通信

数据持久化:Volumes vs Bind Mounts

这是初学者最容易混淆的概念。我们总结了选择规则:

Named Volumes(命名卷)

适用场景:数据库数据、应用状态文件、缓存数据。Docker 完全管理存储位置和权限,可移植性好。

volumes:
  postgres_data:    # Docker 自动创建和管理
  redis_data:
  emqx_data:

Bind Mounts(绑定挂载)

适用场景:开发时的代码热重载、配置文件注入、日志输出目录。直接映射宿主机路径,需要注意权限问题。

# 开发环境:代码热重载
volumes:
  - ./backend/src:/app/src

# 配置文件注入(只读)
volumes:
  - ./config/nginx.conf:/etc/nginx/nginx.conf:ro
  - ./config/ssl:/etc/nginx/ssl:ro

# 注意:生产环境避免使用 bind mount 存储数据
# 因为文件权限和 SELinux/AppArmor 策略可能导致问题

tmpfs(临时文件系统)

适用于完全不需要持久化的临时数据(如 Redis 缓存、会话存储),数据存储在内存中,容器停止后自动清除。

滚动更新与零停机部署

在生产环境中,我们需要在不中断用户访问的情况下更新服务。Docker Compose 配合 deploy 配置可以实现简单的滚动更新。

# 滚动更新配置
services:
  backend:
    image: youyiyun/backend:${IMAGE_TAG}
    deploy:
      replicas: 3
      update_config:
        parallelism: 1       # 每次更新 1 个容器
        delay: 15s           # 每个容器更新间隔
        order: start-first   # 先启动新容器再停止旧容器(零停机)
        failure_action: rollback  # 更新失败自动回滚
      rollback_config:
        parallelism: 1
        order: stop-first    # 回滚时先停新容器
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3

# 滚动更新命令
# 1. 拉取新镜像
docker compose pull backend

# 2. 滚动更新(Swarm 模式)
docker compose up -d --no-deps --build backend

# 3. 或使用 deploy 命令
docker compose deploy --with-registry-auth

# 查看更新状态
docker compose ps
docker compose logs -f backend

Nginx 负载均衡配合

# nginx.conf 配置示例
upstream backend_servers {
    least_conn;  # 最少连接负载均衡
    
    # Docker Compose 使用服务名解析
    server backend_1:3000 max_fails=3 fail_timeout=30s;
    server backend_2:3000 max_fails=3 fail_timeout=30s;
    server backend_3:3000 max_fails=3 fail_timeout=30s;
    
    # 健康检查(需要 nginx_plus 或第三方模块)
    # health_check interval=10s fails=3 passes=2;
}

server {
    listen 443 ssl http2;
    server_name api.youyiyun.com;
    
    ssl_certificate /etc/nginx/ssl/server.crt;
    ssl_certificate_key /etc/nginx/ssl/server.key;
    
    # 零停机:先检查后端是否存活
    location / {
        proxy_pass http://backend_servers;
        proxy_next_upstream error timeout http_502 http_503;
        proxy_next_upstream_tries 3;
        
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        
        # WebSocket 支持
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        
        # 超时设置
        proxy_connect_timeout 10s;
        proxy_read_timeout 300s;
        proxy_send_timeout 300s;
    }
}

日志管理与监控

容器日志管理是生产运维的关键环节。默认的 json-file 驱动如果不加限制,日志文件会无限增长,最终耗尽磁盘空间。

# 全局 Docker daemon 日志配置 /etc/docker/daemon.json
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "50m",
    "max-file": "5",
    "compress": "true"
  },
  "storage-driver": "overlay2"
}

# 单服务覆盖日志配置
services:
  backend:
    logging:
      driver: json-file
      options:
        max-size: "100m"
        max-file: "10"
        tag: "youyiyun-backend/{{.Name}}"
        env: "NODE_ENV,LOG_LEVEL"
        labels: "service"

# 高级:使用 Loki 收集日志
services:
  loki:
    image: grafana/loki:2.9.0
    volumes:
      - loki_data:/loki
    networks:
      - monitoring

  promtail:
    image: grafana/promtail:2.9.0
    volumes:
      - /var/log:/var/log:ro
      - /var/lib/docker/containers:/var/lib/docker/containers:ro
      - ./config/promtail.yml:/etc/promtail/config.yml:ro
    command: -config.file=/etc/promtail/config.yml
    networks:
      - monitoring
    depends_on:
      - loki

# promtail.yml 配置
server:
  http_listen_port: 9080
  grpc_listen_port: 0

positions:
  filename: /tmp/positions.yaml

clients:
  - url: http://loki:3100/loki/api/v1/push

scrape_configs:
  - job_name: docker
    docker_sd_configs:
      - host: unix:///var/run/docker.sock
        refresh_interval: 5s
        filters:
          - name: label
            values: ["logging=promtail"]
    relabel_configs:
      - source_labels: ['__meta_docker_container_name']
        target_label: 'container'

生产环境安全加固

容器安全是生产环境的底线。以下是我们在生产环境中实施的安全加固措施。

# 1. 非 root 用户运行
# Dockerfile 中已添加 appuser
USER appuser

# 2. 只读文件系统(只读根文件系统)
services:
  backend:
    read_only: true
    tmpfs:
      - /tmp
      - /app/logs  # 日志写入临时目录

# 3. 资源限制(防止资源耗尽)
services:
  backend:
    deploy:
      resources:
        limits:
          memory: 2G
          cpus: "2.0"
        reservations:
          memory: 512M
          cpus: "0.5"
    memswap_limit: 2G  # 禁止使用 swap
    pids_limit: 100    # 限制进程数量

# 4. 安全选项
services:
  backend:
    security_opt:
      - no-new-privileges:true    # 禁止提权
      - apparmor:docker-default   # 启用 AppArmor 配置
    cap_drop:
      - ALL                       # 丢弃所有能力
    cap_add:
      - NET_BIND_SERVICE          # 仅保留绑定 <1024 端口的能力

# 5. 敏感信息管理
# ❌ 不要在 docker-compose.yml 中硬编码密码
# ✅ 使用 Docker secrets(Swarm 模式)或 .env 文件

# .env 文件(加入 .gitignore)
DB_PASSWORD=your_secure_password_here
REDIS_PASSWORD=your_redis_password
JWT_SECRET=your_jwt_secret
SMTP_PASSWORD=your_smtp_password

# docker-compose.yml 中引用
environment:
  POSTGRES_PASSWORD: ${DB_PASSWORD}

# 6. 网络隔离
networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
    internal: true  # 后端网络不暴露到宿主机
  monitoring:
    driver: bridge
    internal: true

# 7. 镜像安全扫描(CI/CD 集成)
# 使用 Trivy 扫描镜像漏洞
# trivy image --severity HIGH,CRITICAL youyiyun/backend:latest

# 8. Docker daemon 安全配置 /etc/docker/daemon.json
{
  "userns-remap": "default",       # 用户命名空间重映射
  "live-restore": true,            # 容器不中断 daemon 重启
  "userland-proxy": false,         # 减少攻击面
  "no-new-privileges": true        # 全局禁止提权
}

总结

Docker Compose 是一个非常强大的工具,但”能用”和”用好”之间存在巨大的差距。通过合理的文件组织、多阶段构建优化、完善的健康检查、恰当的网络和数据持久化策略、安全的滚动更新机制以及全面的安全加固,我们可以构建出稳定、高效且安全的容器化部署方案。这些实践经验在我们的 20+ 个项目中反复验证,希望能为你的容器化之旅提供有价值的参考。