Docker Compose 生产实践
Compose 不只是本地开发——中小项目用它上生产比 K8s 简单十倍。本文讲清 Compose 生产化:多环境(env_file/override)、健康检查、依赖顺序(depends_on health)、重启策略、日志驱动、资源限制、以及 swarm vs 独立 Compose 的取舍。
本文要点
- 主题聚焦:DevOps
- 适合人群:独立开发者、全栈工程师、AI 应用开发者
- 核心价值:快速理解背景、实践路径、常见误区和工程建议
很多人对 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 文件描述差异,然后按环境叠加。
.
├── docker-compose.yml # base:服务定义、网络、数据卷
├── docker-compose.override.yml # 默认 dev(Compose 自动加载)
└── docker-compose.prod.yml # 生产(用 -f 显式指定)
docker-compose.yml 只放骨架:
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 自动加载,挂源码、开热重载:
# docker-compose.override.yml
services:
app:
volumes:
- ./src:/app/src
command: pnpm dev
environment:
NODE_ENV: development
prod 用 -f 显式叠加:
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
# 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:
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:只在非零退出码时重启,适合批处理任务。- 不写:挂了就挂了,凌晨告警准时到。
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/磁盘满了,整台机器一起挂。这是血泪坑,生产必须配:
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。
services:
app:
logging:
driver: fluentd
options:
fluentd-address: localhost:24224
tag: app-service
选哪个看你的日志栈。但对绝大多数中小项目,json-file + 限额 是最省心的。
五、资源限制:别让一个服务 OOM 全家
容器共享内核,一个失控的服务(比如 Node 进程内存泄漏、Python 加载大模型)能吃光整机内存,触发 Linux OOM killer 乱杀进程。Compose 里通过 deploy.resources 限制:
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。如果你的版本不认,降级用老字段:
services:
app:
mem_limit: 1g
cpus: 1.0
limits 是硬上限(超了就 OOM kill 或 throttle),reservations 是软保底(保证最少能用这么多)。生产建议只设 limits,reservations 谨慎给——后者会让调度器预留资源,单机没意义反而限制密度。
六、安全:非 root、只读、最小权限
容器逃逸事故的根因八成是「以 root 跑 + 挂了不该挂的东西」。生产 compose 该上三道锁:
第一,非 root 用户。 Dockerfile 里创建专用用户:
RUN addgroup -S app && adduser -S app -G app
USER app
或在 compose 里覆盖:
services:
app:
user: "1000:1000"
第二,只读文件系统。 应用代码不该在运行时被改,就锁死:
services:
app:
read_only: true
tmpfs:
- /tmp
- /app/.next/cache
tmpfs 给运行时必须写的目录(临时文件、缓存)开个内存盘,既不破坏只读又能跑。
第三,能力裁剪。 默认 Docker 给一堆 capabilities,绝大多数用不上:
services:
app:
cap_drop:
- ALL
cap_add:
- CHOWN # 只加确实需要的
security_opt:
- no-new-privileges:true
no-new-privileges 阻止子进程通过 setuid 提权,几乎是免费的安全增益。
合起来看一个生产级服务定义:
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 生产化,核心就这几件事:
- base + override 分离环境,env_file 注入变量,一份 compose 走天下。
- healthcheck + depends_on service_healthy,消灭启动期的 race condition。
- restart: unless-stopped + 日志大小限制,自愈 + 防磁盘爆。
- 非 root、只读、cap_drop ALL,把容器做成最小可执行单元。
- 资源 limits 必须给,一个服务失控不该拖垮全家。
选型上:单机 Compose 是中小项目的甜蜜点,Swarm 是个尴尬中间态,K8s 留给真正需要多机编排的团队。
金句送给每一个想「上 K8s 显得专业」的团队:工具的先进性不在于它有多复杂,而在于它是否恰好匹配你的问题规模——用 K8s 跑一个 monolith 不是工程能力,是过度工程的借口。
Related
相关推荐
文章
Docker 部署 Next.js 社区项目的注意事项
围绕 DevOps、Next.js 的技术趋势、工程实践和独立开发视角。
文章
OpenTelemetry 如何帮助你观察 AI 应用?
围绕 DevOps、AI 的技术趋势、工程实践和独立开发视角。
文章
长运行工具调用:Operation、Poll 与 Cancel 的终态协议
初始请求超时后,工具可能仍在服务端运行;发出取消也不等于副作用已停止。本文为单次长运行工具调用建立 operation 身份、轮询水位、取消竞态和唯一终态接受点。
文章
Agent 记忆删除传播:从用户遗忘请求到可验证清除
删除主表一行不会清掉向量、摘要、缓存和恢复后的备份。本文用删除状态机、代际墓碑、派生血缘与端到端 canary 验证真正遗忘。