很多项目的 compose.yaml 最初只有几行:应用容器、数据库容器、端口映射,再加一句 docker compose up -d。开发环境里这已经足够,但到了长期运行的服务器上,很快会遇到新的问题:

  • 数据库启动了,应用却因为数据库尚未就绪而反复报错。
  • 容器进程还活着,接口实际已经无法提供服务。
  • 日志无限增长,把服务器磁盘写满。
  • 配置、密码和镜像版本混在一个文件里。
  • 发布新版本时直接停掉全部服务。
  • 数据卷存在,却从来没有验证过恢复流程。

Compose 可以用于单机和小规模生产部署,但前提是把它当成运行编排工具,而不是批量执行 docker run 的快捷方式。本文给出一套从“能启动”走向“可维护”的配置思路。

1. 先明确 Compose 的适用边界

Docker Compose 比较适合:

  • 单机部署的中小型服务。
  • 内部工具、管理后台和低到中等流量应用。
  • 对复杂弹性伸缩和跨节点调度要求不高的系统。
  • 希望保持部署简单、成本可控的团队。

如果系统需要跨主机调度、自动扩缩容、复杂滚动发布、节点故障迁移和大规模服务治理,通常应该评估 Kubernetes、Nomad 或云平台托管服务。

选择 Compose 并不等于“不专业”。真正重要的是:当前工具能否覆盖故障模型、可用性目标和团队维护能力。

2. 不要使用浮动镜像标签

下面的配置看起来方便:

services:
  api:
    image: example/api:latest

latest 并不代表最新,也不能保证两次部署拉取到同一个镜像。生产环境应使用明确版本:

services:
  api:
    image: example/api:1.8.3

要求更严格时,可以固定镜像摘要:

services:
  api:
    image: example/api@sha256:xxxxxxxxxxxxxxxx

固定版本带来的价值是可审计、可复现、可回滚。发布系统应该明确记录“哪个版本在什么时间被部署到了哪台机器”。

3. depends_on 不等于服务已经可用

常见写法:

services:
  api:
    depends_on:
      - db

它只能表达启动顺序,不能保证数据库已经完成初始化并接受连接。更可靠的做法是为依赖服务增加健康检查:

services:
  db:
    image: postgres:17.5
    environment:
      POSTGRES_DB: app
      POSTGRES_USER: app
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d app"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 20s

  api:
    image: example/api:1.8.3
    depends_on:
      db:
        condition: service_healthy

即使配置了依赖健康状态,应用本身仍应具备连接重试能力。因为数据库可能在系统运行期间重启,启动顺序无法解决运行时故障。

4. 为应用定义真正有意义的健康检查

健康检查不应该只验证进程存在,也不应该无条件返回 200。一个合理的接口可以分成两类:

  • 存活检查:进程是否还能够处理请求。
  • 就绪检查:数据库、缓存或必要依赖是否达到服务条件。

Compose 示例:

services:
  api:
    image: example/api:1.8.3
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:8080/health/ready"]
      interval: 15s
      timeout: 3s
      retries: 3
      start_period: 30s

注意镜像中必须实际存在 curl。如果为了健康检查额外安装大量工具并不划算,可以使用应用自带的命令、轻量探针程序,或者由反向代理从外部检查。

健康检查还要避免执行昂贵 SQL,否则检查本身可能成为压力来源。

5. 配置重启策略,但不要掩盖持续崩溃

常用配置:

services:
  api:
    restart: unless-stopped

它能在进程异常退出或 Docker 服务重启后恢复容器。但重启策略不是异常处理系统。如果应用因为配置错误每隔几秒崩溃一次,无限重启只会制造大量日志。

生产环境至少要监控:

  • 容器重启次数。
  • 最近一次退出码。
  • 健康状态变化。
  • OOM 和磁盘不足事件。
  • 启动失败后的连续退避情况。

自动恢复解决的是短暂故障,告警解决的是持续故障。

6. 给资源设置边界

没有资源限制的容器,可以占满整台主机的内存或 CPU。对于 Compose,本地运行时可以使用服务级资源配置:

services:
  api:
    image: example/api:1.8.3
    cpus: 1.5
    mem_limit: 1g
    mem_reservation: 512m

具体支持项与 Compose 实现、运行模式有关,部署前应使用当前环境的 docker compose config 检查最终配置,并通过压力测试验证限制是否生效。

资源限制不能凭感觉设置。建议结合:

  • 稳态内存和峰值内存。
  • GC 或运行时堆配置。
  • 单请求 CPU 消耗。
  • 并发连接数与线程数。
  • 主机上其他服务的资源需求。

内存上限过低会频繁 OOM,过高则无法保护主机。比较稳妥的做法是先观测,再逐步收紧。

7. 只暴露真正需要的端口

数据库通常不需要直接暴露到公网:

services:
  db:
    image: postgres:17.5
    expose:
      - "5432"

对外入口交给反向代理:

services:
  proxy:
    image: nginx:1.28-alpine
    ports:
      - "80:80"
      - "443:443"

  api:
    expose:
      - "8080"

Compose 默认会为项目创建网络,服务可通过服务名互相访问。可以进一步拆分前端网络和数据网络,让代理不能直接访问数据库:

networks:
  frontend:
  backend:
    internal: true

网络隔离不能替代主机防火墙、云安全组和数据库权限控制,但能减少不必要的暴露面。

8. 配置与秘密信息分开管理

不要把密码直接提交到仓库:

environment:
  DB_PASSWORD: super-secret

可以通过环境文件注入非敏感配置:

env_file:
  - .env.production

对于密码、令牌和证书,更适合使用只读文件挂载、Compose secrets,或者外部秘密管理服务:

secrets:
  db_password:
    file: ./secrets/db_password.txt

services:
  api:
    secrets:
      - db_password

应用读取 /run/secrets/db_password,可以减少秘密出现在命令行参数和普通环境变量中的机会。秘密文件本身仍需设置严格权限,并确保不会进入 Git、镜像构建上下文和备份日志。

9. 文件系统尽量只读

如果应用不需要写入根文件系统,可以配置:

services:
  api:
    read_only: true
    tmpfs:
      - /tmp

需要持久化的数据应写入明确的数据卷:

volumes:
  - app_uploads:/app/uploads

这样能减少程序误写、攻击落地文件和临时数据污染镜像层的风险。启用只读前,要确认应用的缓存、PID、临时文件和上传目录分别应该放在哪里。

还可以配合:

security_opt:
  - no-new-privileges:true

并尽量使用非 root 用户运行应用。容器不是安全边界的终点,但每一层约束都会降低风险。

10. 限制日志大小

默认日志长期增长,是单机 Compose 最常见的事故之一。可以使用日志轮转:

services:
  api:
    logging:
      driver: json-file
      options:
        max-size: "20m"
        max-file: "5"

应用日志建议写到标准输出和标准错误,由容器运行时统一收集。不要同时在容器内部写无限增长的日志文件。

需要集中检索时,可以接入 Loki、Elasticsearch、云日志服务或其他采集方案。无论使用哪种工具,都应在日志中保留:

  • 时间戳和日志级别。
  • 请求 ID 或 Trace ID。
  • 服务名与版本号。
  • 错误类型和关键上下文。

同时避免记录密码、访问令牌和完整个人敏感信息。

11. 数据卷不等于备份

下面的配置只能保证容器删除后数据仍保存在卷中:

volumes:
  db_data:

services:
  db:
    volumes:
      - db_data:/var/lib/postgresql/data

它不能防止误删除、文件损坏、磁盘故障或主机丢失。真正的备份方案至少要明确:

  1. 备份方式:逻辑备份、物理备份或快照。
  2. 备份频率与保留周期。
  3. 备份文件存放在另一台机器或对象存储。
  4. 加密、访问权限和完整性校验。
  5. 定期执行恢复演练。

没有恢复验证的备份,只是一份尚未证明可用的文件。

12. 优雅停止决定发布是否丢请求

容器停止时,Docker 会先发送终止信号,再等待一段时间后强制结束。应用需要正确处理信号:

  • 停止接收新请求。
  • 等待正在执行的请求完成。
  • 停止消费新的队列消息。
  • 提交或回滚事务。
  • 关闭数据库连接和文件句柄。

Compose 中可以配置:

services:
  api:
    stop_grace_period: 30s

宽限时间要大于正常请求或任务的收尾时间,但不能无限延长。对于长任务,通常应该设计可恢复进度、幂等消费和超时机制,而不是只依赖更长的停止等待。

13. Compose 本身不会自动提供无损滚动发布

直接执行:

docker compose up -d

可能会重建发生变化的容器。单实例服务在切换期间可能出现短暂中断。需要更高可用性时,可以采用:

  • 反向代理前运行两个应用实例,逐个替换。
  • 蓝绿部署:新旧两套 Compose 项目并行运行,验证后切换入口。
  • 在负载均衡器中先摘除旧实例,再停止容器。
  • 数据库变更使用向前兼容的扩展与收缩策略。

例如新增字段时,先发布兼容新旧结构的代码,再执行迁移,最后清理旧字段。不要让应用发布与不可逆数据库变更绑定成一次豪赌。

14. 一份更完整的 Compose 骨架

name: demo-prod

services:
  proxy:
    image: nginx:1.28-alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    networks:
      - frontend
    depends_on:
      api:
        condition: service_healthy
    logging:
      driver: json-file
      options:
        max-size: "20m"
        max-file: "5"

  api:
    image: example/api:1.8.3
    restart: unless-stopped
    env_file:
      - .env.production
    secrets:
      - db_password
    expose:
      - "8080"
    networks:
      - frontend
      - backend
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "/app/healthcheck"]
      interval: 15s
      timeout: 3s
      retries: 3
      start_period: 30s
    read_only: true
    tmpfs:
      - /tmp
    security_opt:
      - no-new-privileges:true
    cpus: 1.5
    mem_limit: 1g
    stop_grace_period: 30s
    logging:
      driver: json-file
      options:
        max-size: "20m"
        max-file: "5"

  db:
    image: postgres:17.5
    restart: unless-stopped
    environment:
      POSTGRES_DB: app
      POSTGRES_USER: app
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
    volumes:
      - db_data:/var/lib/postgresql/data
    networks:
      - backend
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d app"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 20s
    logging:
      driver: json-file
      options:
        max-size: "20m"
        max-file: "5"

networks:
  frontend:
  backend:
    internal: true

volumes:
  db_data:

secrets:
  db_password:
    file: ./secrets/db_password.txt

这份骨架仍然需要根据应用调整,但它已经覆盖了版本固定、健康检查、网络隔离、秘密文件、资源边界、只读文件系统、优雅停止和日志轮转等关键问题。

部署前可以先执行:

docker compose config
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail=200

docker compose config 能帮助发现变量未定义、合并结果异常和配置格式问题,但不能替代真实环境验证。

15. 上线前 Checklist

  • 镜像是否固定版本,并能快速回滚?
  • 应用和依赖是否都有健康检查?
  • 应用是否能处理依赖在运行期间重启?
  • 端口是否只暴露必要入口?
  • 密码和证书是否脱离仓库与镜像?
  • 容器是否使用非 root、只读文件系统和最小权限?
  • CPU、内存和日志是否有上限?
  • 数据是否有异机备份,并完成恢复演练?
  • 应用能否优雅处理停止信号?
  • 发布和数据库迁移是否向前兼容?
  • 是否监控主机磁盘、容器重启、健康状态和接口延迟?

总结

生产化 Compose 的重点,不是把 YAML 写得更长,而是提前回答这些问题:服务何时算真正可用、故障后如何恢复、资源耗尽时谁被保护、数据丢失后如何恢复、发布过程中怎样避免中断。

当镜像版本、健康检查、资源限制、网络边界、日志轮转、备份恢复和发布策略都被明确下来,Compose 才从一个启动工具变成一套可维护的运行方案。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐