#DevOps

Docker Compose 生产实践

Compose 不只是本地开发——中小项目用它上生产比 K8s 简单十倍。本文讲清 Compose 生产化:多环境(env_file/override)、健康检查、依赖顺序(depends_on health)、重启策略、日志驱动、资源限制、以及 swarm vs 独立 Compose 的取舍。

本文要点

  • 主题聚焦:DevOps
  • 适合人群:独立开发者、全栈工程师、AI 应用开发者
  • 核心价值:快速理解背景、实践路径、常见误区和工程建议
作者 @admin·发布于 2026年4月27日·更新于 2026年7月23日

很多人对 Docker Compose 的印象停留在「本地起一组服务方便联调」,一上生产就去找 K8s。我经手过不少中小项目(单机或两三台机器、QPS 不破千、团队 3-10 人),结论很明确:90% 的场景下,生产用 Compose 比 K8s 简单十倍,而且更稳。K8s 是为「几百个微服务、多团队、频繁滚动发布」设计的,你一个 monolith + Postgres + Redis + Nginx 的组合,搬一套 K8s 上去,光是 helm chart、ingress、PVC、CRD 就够你折腾一周。Compose 半天就能上线,排障也只需要 docker logs。

但「能跑」和「生产级」之间差的就是几道工序。下面把 Compose 生产化的关键点一次性讲透。

一、多环境:base + override 分离

生产第一个坑就是把 dev 和 prod 配置揉在一个 docker-compose.yml 里,改一个值就互相影响。正确做法是 base 文件描述共性,override 文件描述差异,然后按环境叠加。

text
.
├── docker-compose.yml          # base:服务定义、网络、数据卷
├── docker-compose.override.yml # 默认 dev(Compose 自动加载)
└── docker-compose.prod.yml     # 生产(用 -f 显式指定)

docker-compose.yml 只放骨架:

yaml
services:
  app:
    build: .
    env_file: .env
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started

  db:
    image: postgres:16
    volumes:
      - db-data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine

volumes:
  db-data:

dev override 自动加载,挂源码、开热重载:

yaml
# docker-compose.override.yml
services:
  app:
    volumes:
      - ./src:/app/src
    command: pnpm dev
    environment:
      NODE_ENV: development

prod 用 -f 显式叠加:

bash
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
yaml
# docker-compose.prod.yml
services:
  app:
    restart: unless-stopped
    environment:
      NODE_ENV: production

  db:
    restart: unless-stopped

环境变量用 env_file 注入,不要写死在 yml 里。.env 进 .gitignore,团队里每人本地一份;CI 里由 secret 注入。这样同一份 compose 文件能在 staging/prod 之间无缝切换,只换 .env。

二、健康检查 + 依赖顺序:让 DB 先就绪再起 app

Compose 默认的 depends_on 只保证「容器启动」,不管服务是否就绪。典型事故:app 启动比 Postgres 快,连不上数据库直接崩,然后进入重启循环。

解决办法是 给被依赖服务加 healthcheck,依赖方声明 condition: service_healthy:

yaml
services:
  db:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 5

  app:
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy

几个关键点:

  • start_period 给服务一个「预热窗口」,这段时间内的失败不计入 retries,对启动慢的 JVM/数据库尤其重要。
  • test 用真正能反映「业务就绪」的命令,而不是 curl localhost。Postgres 用 pg_isready,Redis 用 ping,MySQL 用 mysqladmin ping。
  • 注意 $$ 转义:compose 会把 $VAR 当作在 compose 这一层的变量插值,你想让 shell 在容器内展开就要写 $$VAR。

这一步配好,部署时的 race condition 基本消失。

三、重启策略:unless-stopped 是默认答案

容器挂了能自己拉起来,是生产最低门槛。四个值里我的建议:

  • unless-stopped:默认选它。容器异常退出自动重启;但你手动 stop 后不会因为 daemon 重启又被拉起——符合「我主动停的就是要停」的直觉。
  • always:daemon 重启后一律拉起,包括你手动 stop 的。常驻基础设施(数据库、代理)可以考虑,但容易让人困惑。
  • on-failure:只在非零退出码时重启,适合批处理任务。
  • 不写:挂了就挂了,凌晨告警准时到。
yaml
services:
  app:
    restart: unless-stopped

注意 restart 治不了「进程没死但卡死」的情况——那是 healthcheck 的活。两者配合才完整:healthcheck 判定 unhealthy,Docker 会标记但默认不重启;要让它真正自愈,在 Swarm 模式或外置 supervisor 里处理,单机 Compose 下可加一个 autoheal sidecar 监听 unhealthy 事件做 docker restart。

四、日志:json-file 限制必须配,否则磁盘迟早炸

Docker 默认日志驱动是 json-file,不限制大小。一个跑了几个月的服务,日志文件轻松涨到几十 GB,最后 inode/磁盘满了,整台机器一起挂。这是血泪坑,生产必须配:

yaml
services:
  app:
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

这样每个容器最多保留 3 个 10MB 的日志文件,滚动覆盖。单机项目这个配置就够了。

规模再大一点,考虑外置日志驱动:

  • fluentd / fluentbit:直接把日志推到日志聚合服务,Loki / Elasticsearch。
  • journald:走 systemd-journald,配合 journalctl 查询。
  • gelf:推到 Graylog。
yaml
services:
  app:
    logging:
      driver: fluentd
      options:
        fluentd-address: localhost:24224
        tag: app-service

选哪个看你的日志栈。但对绝大多数中小项目,json-file + 限额 是最省心的。

五、资源限制:别让一个服务 OOM 全家

容器共享内核,一个失控的服务(比如 Node 进程内存泄漏、Python 加载大模型)能吃光整机内存,触发 Linux OOM killer 乱杀进程。Compose 里通过 deploy.resources 限制:

yaml
services:
  app:
    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 1G
        reservations:
          memory: 256M

  db:
    deploy:
      resources:
        limits:
          cpus: "2.0"
          memory: 2G

这里有个反直觉的点:deploy 字段在文档里是给 Swarm 用的,但 docker compose up(非 Swarm)从某个版本起也认 limits/reservations。如果你的版本不认,降级用老字段:

yaml
services:
  app:
    mem_limit: 1g
    cpus: 1.0

limits 是硬上限(超了就 OOM kill 或 throttle),reservations 是软保底(保证最少能用这么多)。生产建议只设 limits,reservations 谨慎给——后者会让调度器预留资源,单机没意义反而限制密度。

六、安全:非 root、只读、最小权限

容器逃逸事故的根因八成是「以 root 跑 + 挂了不该挂的东西」。生产 compose 该上三道锁:

第一,非 root 用户。 Dockerfile 里创建专用用户:

dockerfile
RUN addgroup -S app && adduser -S app -G app
USER app

或在 compose 里覆盖:

yaml
services:
  app:
    user: "1000:1000"

第二,只读文件系统。 应用代码不该在运行时被改,就锁死:

yaml
services:
  app:
    read_only: true
    tmpfs:
      - /tmp
      - /app/.next/cache

tmpfs 给运行时必须写的目录(临时文件、缓存)开个内存盘,既不破坏只读又能跑。

第三,能力裁剪。 默认 Docker 给一堆 capabilities,绝大多数用不上:

yaml
services:
  app:
    cap_drop:
      - ALL
    cap_add:
      - CHOWN   # 只加确实需要的
    security_opt:
      - no-new-privileges:true

no-new-privileges 阻止子进程通过 setuid 提权,几乎是免费的安全增益。

合起来看一个生产级服务定义:

yaml
services:
  app:
    image: myapp:1.4.2      # 生产用固定 tag,别用 latest
    user: "1000:1000"
    read_only: true
    tmpfs:
      - /tmp
    cap_drop: [ALL]
    security_opt:
      - no-new-privileges:true
    restart: unless-stopped
    env_file: .env.prod
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:3000/healthz"]
      interval: 30s
      timeout: 5s
      retries: 3
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "5"
    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 1G
    depends_on:
      db:
        condition: service_healthy

七、单机 Compose vs Swarm vs K8s:别给自己加戏

聊完配置聊选型。三层台阶,按需上:

单机 Compose:一台机器跑一组服务。适合日活几万到几十万、单机能扛住的项目。运维心智成本极低,挂了重启、磁盘满清日志、升级 pull && up -d,十分钟搞定。绝大多数中小项目停在这里就够了。

Swarm:Compose 的多机扩展,docker swarm init 一行启用,compose 文件几乎不用改。听起来很美,但 Swarm 是一个「多出来的故障点」——你需要维护 raft 共识、manager 节点、跨机网络,而它的生态(监控、CI 集成、helm 之类的包管理)远不如 K8s 丰富。我的判断:Swarm 是个尴尬的中间态。如果你觉得单机不够,通常你应该直接上 K8s,而不是停在 Swarm。

K8s:多机编排的事实标准。学习曲线陡,组件多(api-server、etcd、scheduler、kube-proxy……),但它换来的是:声明式滚动发布、HPA 自动扩缩容、丰富的 operator 生态、跨可用区调度、团队间的多租户隔离。真要多机、多团队、频繁发布,再上 K8s。

一个可操作的判断标准:

场景选什么
单机能扛、团队小单机 Compose
要多机但量不大、不想学 K8s单机 Compose × N 台 + 反向代理轮询(比 Swarm 稳)
多团队、多服务、需要弹性扩缩K8s
临时多机、怕 Swarm 也怕 K8s先 Compose 单机扛着,业务验证后再迁

最后一条常被忽略:很多项目根本不需要多机。一台 16C32G 的机器跑一个 Next.js + Postgres + Redis,优化得当能扛几十万日活。先把单机榨干,再谈扩展,不要为了「看起来先进」上 K8s。

小结

把 Compose 生产化,核心就这几件事:

  1. base + override 分离环境,env_file 注入变量,一份 compose 走天下。
  2. healthcheck + depends_on service_healthy,消灭启动期的 race condition。
  3. restart: unless-stopped + 日志大小限制,自愈 + 防磁盘爆。
  4. 非 root、只读、cap_drop ALL,把容器做成最小可执行单元。
  5. 资源 limits 必须给,一个服务失控不该拖垮全家。

选型上:单机 Compose 是中小项目的甜蜜点,Swarm 是个尴尬中间态,K8s 留给真正需要多机编排的团队。

金句送给每一个想「上 K8s 显得专业」的团队:工具的先进性不在于它有多复杂,而在于它是否恰好匹配你的问题规模——用 K8s 跑一个 monolith 不是工程能力,是过度工程的借口。

Related

相关推荐