概述
很多团队对 Docker Compose 的认知停留在"本地开发环境编排工具"。但事实上,在中小规模生产场景(单节点或少量节点)下,Compose 依然是性价比极高的方案。它语法简洁、学习成本低、不需要一整套 K8s 集群运维能力,就能完成多服务编排、依赖管理、健康检查、资源限制等核心工作。
本文不重复 Compose 基础语法,而是聚焦生产环境中的真实痛点:服务依赖怎么管才不会启动雪崩、健康检查怎么写才靠谱、密钥怎么不硬编码进 compose 文件、日志怎么不把磁盘写满、什么时候该从 Compose 迁移到 K8s。
本文基于 Docker Compose V2(
docker compose子命令),V1(docker-compose独立二进制)已停止维护。参考 Compose 规范
多服务编排
生产级 Compose 文件结构
一个典型的生产环境应用至少包含:应用服务、数据库、缓存、反向代理。下面是一个完整的 Web 应用编排示例:
# docker-compose.yml
name: myapp
services:
# ========== 反向代理 ==========
nginx:
image: nginx:1.25-alpine
container_name: myapp-nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- cert_data:/etc/letsencrypt:ro
- log_data:/var/log/nginx
depends_on:
web:
condition: service_healthy
networks:
- frontend
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
# ========== 应用服务 ==========
web:
build:
context: .
dockerfile: Dockerfile
args:
- GIT_COMMIT=${GIT_COMMIT:-unknown}
image: myapp/web:${TAG:-latest}
container_name: myapp-web
restart: unless-stopped
environment:
- DB_HOST=postgres
- DB_PORT=5432
- DB_NAME=${DB_NAME}
- REDIS_HOST=redis
- ENV=production
env_file:
- .env.production
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8080/health"]
interval: 10s
timeout: 5s
retries: 3
start_period: 30s
deploy:
resources:
limits:
cpus: "2.0"
memory: 1G
reservations:
cpus: "0.5"
memory: 256M
networks:
- frontend
- backend
logging:
driver: json-file
options:
max-size: "20m"
max-file: "5"
# ========== 数据库 ==========
postgres:
image: postgres:16-alpine
container_name: myapp-postgres
restart: unless-stopped
environment:
POSTGRES_DB: ${DB_NAME}
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
volumes:
- pg_data:/var/lib/postgresql/data
- ./sql/init:/docker-entrypoint-initdb.d:ro
secrets:
- db_password
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
networks:
- backend
logging:
driver: json-file
options:
max-size: "50m"
max-file: "3"
# ========== 缓存 ==========
redis:
image: redis:7-alpine
container_name: myapp-redis
restart: unless-stopped
command: >
redis-server
--requirepass ${REDIS_PASSWORD}
--maxmemory 256mb
--maxmemory-policy allkeys-lru
--appendonly yes
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 10s
timeout: 3s
retries: 3
networks:
- backend
networks:
frontend:
driver: bridge
backend:
driver: bridge
internal: true # 后端网络不暴露到宿主机
volumes:
pg_data:
driver: local
redis_data:
driver: local
cert_data:
driver: local
log_data:
driver: local
secrets:
db_password:
file: ./secrets/db_password.txt
网络隔离设计
上面的配置中使用了两个网络:
| 网络 | 用途 | 特点 |
|---|---|---|
frontend | Nginx ↔ Web | 暴露端口到宿主机 |
backend | Web ↔ DB/Redis | internal: true,不路由到宿主机外部 |
internal: true 是一个经常被忽略的安全加固手段。它阻止后端网络的容器访问外部网络,即使容器被攻破,攻击者也无法直接外连 C2 服务器或反弹 shell。
多环境管理
不要为每个环境维护一个完整的 compose 文件。使用 override 机制:
# 目录结构
# docker-compose.yml # 基础配置
# docker-compose.override.yml # 开发环境覆盖(自动加载)
# docker-compose.prod.yml # 生产环境覆盖
# docker-compose.staging.yml # 预发环境覆盖
# docker-compose.prod.yml
services:
web:
environment:
- ENV=production
deploy:
replicas: 3
resources:
limits:
cpus: "2.0"
memory: 1G
postgres:
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
deploy:
resources:
limits:
cpus: "4.0"
memory: 4G
# 启动生产环境
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
健康检查
为什么必须配健康检查
没有 healthcheck 的服务,depends_on 只能保证容器启动顺序,不能保证服务就绪。典型的坑:
web 容器启动 → 尝试连接 postgres → postgres 进程已启动但还没接受连接 → web 报错退出 → restart → 再连 → 循环
配置了健康检查后,depends_on 可以使用 condition: service_healthy,确保依赖的服务真正就绪后才启动。
健康检查写法要点
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8080/health"]
interval: 10s # 每10秒检查一次
timeout: 5s # 超时5秒视为失败
retries: 3 # 连续失败3次标记为unhealthy
start_period: 30s # 启动后30秒内不计入失败(给应用预热时间)
不同服务的健康检查命令:
| 服务类型 | 健康检查命令 | 说明 |
|---|---|---|
| HTTP API | wget --spider -q http://localhost:PORT/health | 检查 HTTP 状态码 |
| PostgreSQL | pg_isready -U user -d db | 官方就绪检查工具 |
| Redis | redis-cli -a password ping | 返回 PONG 即健康 |
| MySQL | mysqladmin ping -h localhost | 返回 mysqld is alive |
| MongoDB | mongosh --eval "db.adminCommand('ping')" | 健康检查命令 |
| gRPC | grpc_health_probe -addr=localhost:PORT | 需要 grpc_health_probe 工具 |
注意:健康检查的
test中使用CMD而不是CMD-SHELL,可以避免 shell 注入风险,且执行效率更高。CMD-SHELL只在需要 shell 特性(管道、变量展开)时使用。
/health 端点设计
健康检查端点不只是返回 200,应该做真正的依赖检查:
// Go 示例:深度健康检查
func healthHandler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()
// 检查数据库
if err := db.PingContext(ctx); err != nil {
http.Error(w, "database unreachable", http.StatusServiceUnavailable)
return
}
// 检查 Redis
if _, err := redis.Ping(ctx).Result(); err != nil {
http.Error(w, "redis unreachable", http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, "ok")
}
但要注意:深度健康检查不要太重。如果健康检查端点本身要去查下游服务的数据库,容易形成级联失败。生产建议区分 liveness 和 readiness:
/health/live:只检查进程是否活着(返回 200 即可)/health/ready:检查依赖是否就绪(DB、Redis 连通性)
依赖管理
depends_on 的三种 condition
depends_on:
postgres:
condition: service_started # 容器进程启动即满足(默认)
redis:
condition: service_healthy # 健康检查通过才满足
migration:
condition: service_completed_successfully # 容器执行完毕且退出码为0
service_completed_successfully 是一个很实用的条件,常用于数据库迁移:
services:
migration:
image: myapp/web:${TAG:-latest}
command: ["./migrate", "up"]
depends_on:
postgres:
condition: service_healthy
restart: "no" # 迁移完就退出,不重启
web:
depends_on:
migration:
condition: service_completed_successfully
依赖循环问题
Compose 不支持循环依赖。如果 A 依赖 B,B 又依赖 A,Compose 会报错。解决方法是引入一个初始化容器打破循环。
启动顺序陷阱
启动顺序:nginx → web → postgres → redis(错误!)
正确顺序:postgres/redis → web → nginx
依赖链必须是自底向上的:基础设施 → 应用 → 网关。上面的完整示例中依赖链是:
postgres/redis (无依赖)
↓
web (依赖 postgres/redis 健康)
↓
nginx (依赖 web 健康)
资源限制
deploy.resources vs docker-compose v2 资源限制
Compose V2 中,资源限制写在 deploy.resources 下,和 K8s 的资源配置非常相似:
deploy:
resources:
limits:
cpus: "2.0" # 最大2核
memory: 1G # 最大1G内存
pids: 100 # 最大进程数
reservations:
cpus: "0.5" # 预留0.5核
memory: 256M # 预留256M内存
| 配置项 | 作用 | 生产建议 |
|---|---|---|
limits.cpus | CPU 上限,超出则限流 | 应用 CPU 的 1.5-2 倍 |
limits.memory | 内存上限,超出则 OOM Kill | 应用内存峰值的 1.2-1.5 倍 |
limits.pids | 进程数上限,防 fork 炸弹 | 100-500 |
reservations.cpus | CPU 预留,保证最低算力 | 应用 CPU 均值的 0.5-1 倍 |
reservations.memory | 内存预留,保证最低内存 | 应用内存均值的 0.5-1 倍 |
注意:
reservations在非 Swarm 模式下只做软保证(cgroup 设置),不涉及资源调度。真正的硬保证需要 Swarm 或 K8s 调度器。
OOM 行为理解
容器内存超过 limits.memory 时,内核会 OOM Kill 该容器中内存占用最大的进程。容器内 PID 1 被杀后,容器退出,由 restart 策略决定是否重启。
# 查看容器是否被 OOM Kill
docker inspect myapp-web --format='{{.State.OOMKilled}}'
docker inspect myapp-web --format='{{.State.ExitCode}}' # 137 = 128+9(SIGKILL)
经验值:如果应用频繁被 OOM Kill,先排查内存泄漏,再考虑调大 limit。不要一上来就加内存——那只是在掩盖问题。
日志配置
日志驱动选择
logging:
driver: json-file # 默认,写本地文件
options:
max-size: "20m" # 单个日志文件最大20MB
max-file: "5" # 最多保留5个文件
| 驱动 | 适用场景 | 特点 |
|---|---|---|
json-file | 单节点/小规模 | 简单,需配 max-size/max-file |
fluentd | 集中式日志 | 实时推送到 Fluentd |
gelf | Graylog | GELF 格式 |
syslog | 传统日志系统 | RFC 5424 |
journald | systemd 环境 | 和 journald 集成 |
local | 优化存储 | 二进制格式,轮转高效 |
生产红线:
json-file驱动如果不配max-size和max-file,日志文件会无限增长。这是最常见的磁盘写满事故原因之一。全局配置可以在/etc/docker/daemon.json中设置默认值。
全局日志配置
// /etc/docker/daemon.json
{
"log-driver": "json-file",
"log-opts": {
"max-size": "20m",
"max-file": "5"
}
}
# 修改后重启 Docker
systemctl restart docker
日志集中化方案
单节点用 json-file 没问题,多节点建议统一收集。在 Compose 中集成 Fluentd:
services:
web:
logging:
driver: fluentd
options:
fluentd-address: "localhost:24224"
fluentd-async: "true"
fluentd-buffer-limit: "8192"
tag: "myapp.web"
fluentd:
image: fluent/fluentd:v1.16-debian
volumes:
- ./fluentd/conf:/fluentd/etc:ro
ports:
- "24224:24224"
- "24224:24224/udp"
密钥管理
三种方案对比
| 方案 | 安全性 | 复杂度 | 适用场景 |
|---|---|---|---|
| 环境变量 | 低 | 低 | 开发环境 |
.env 文件 | 中 | 低 | 小规模生产 |
| Docker Secrets | 高 | 中 | 生产环境 |
不要做的事
# ❌ 硬编码密码
environment:
- POSTGRES_PASSWORD=MySecret123
# ❌ 把 .env 文件提交到 Git
# .gitignore 中必须包含 .env*
Docker Secrets 使用
# 创建密钥文件
echo "my-super-secret-password" > ./secrets/db_password.txt
chmod 600 ./secrets/db_password.txt
# .gitignore
echo "secrets/" >> .gitignore
# docker-compose.yml
secrets:
db_password:
file: ./secrets/db_password.txt
services:
postgres:
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
容器内密钥挂载在 /run/secrets/<secret_name>,是一个 tmpfs 文件系统,不落盘。程序读取该文件获取密码:
func loadSecret(name string) (string, error) {
data, err := os.ReadFile(fmt.Sprintf("/run/secrets/%s", name))
if err != nil {
return "", err
}
return strings.TrimSpace(string(data)), nil
}
外部密钥管理
对于更大规模的部署,建议集成 Vault 或云厂商的密钥管理服务:
# 使用 dotenv 从 Vault 拉取密钥
services:
web:
env_file:
- .env.production # 由部署脚本从 Vault 动态生成
# 部署脚本示例:从 Vault 生成 .env
vault kv get -format=json secret/myapp/prod | \
jq -r '.data.data | to_entries[] | "\(.key)=\(.value)"' > .env.production
docker compose up -d
# 部署完成后删除
rm .env.production
Compose 与 Swarm
什么时候用 Swarm
| 场景 | 推荐方案 |
|---|---|
| 单节点,<10 容器 | Docker Compose |
| 2-5 节点,需高可用 | Docker Swarm |
| >5 节点,复杂调度 | Kubernetes |
Swarm 的优势在于学习成本极低——Compose 文件几乎可以直接用,只需要加几个 Swarm 特有字段。
Swarm 部署示例
# docker-compose.swarm.yml
services:
web:
image: myapp/web:${TAG:-latest}
deploy:
replicas: 3
update_config:
parallelism: 1
delay: 10s
failure_action: rollback
order: start-first
rollback_config:
parallelism: 1
order: start-first
restart_policy:
condition: on-failure
max_attempts: 3
placement:
constraints:
- node.role == worker
- node.labels.zone == east
resources:
limits:
cpus: "2.0"
memory: 1G
networks:
- overlay_net
networks:
overlay_net:
driver: overlay
attachable: true
# 初始化 Swarm
docker swarm init
# 部署
docker stack deploy -c docker-compose.swarm.yml myapp
# 查看服务
docker service ls
docker service ps myapp_web
# 滚动更新
docker service update --image myapp/web:v2 myapp_web
# 回滚
docker service rollback myapp_web
Swarm 的局限
- 没有原生 Ingress 控制器,只能用 Swarm routing mesh 做简单的负载均衡
- 没有原生 autoscaling,需要外部工具
- 没有Operator生态,高级运维能力弱
- 社区活跃度持续下降,新功能迭代缓慢
2024 年起,Mirantis(Swarm 商业支持方)将 Swarm 维护交还给社区。虽然不会立即停止维护,但建议新项目优先考虑 K8s。参考 Mirantis 官方声明
从 Compose 迁移到 K8s
迁移评估清单
迁移前问自己以下问题:
- 当前服务规模是否超过 Swarm 的舒适区(>5 节点 / >50 容器)?
- 是否需要 HPA 自动扩缩容?
- 是否需要多集群容灾?
- 团队是否有 K8s 运维能力?
- 是否能接受 K8s 的运维复杂度?
如果超过 2 个"是",就应该开始规划迁移。
使用 Kompose 自动转换
# 安装 Kompose
curl -L https://github.com/kubernetes/kompose/releases/download/v1.31.2/kompose-linux-amd64 -o kompose
chmod +x kompose
mv kompose /usr/local/bin/
# 转换
kompose convert -f docker-compose.yml -o k8s/
# 生成的文件
# k8s/web-deployment.yaml
# k8s/web-service.yaml
# k8s/postgres-deployment.yaml
# k8s/postgres-service.yaml
Kompose 转换的局限
Kompose 能完成 70% 的机械转换,但以下内容需要手动处理:
| Compose | K8s 对应 | 注意事项 |
|---|---|---|
depends_on | init container | K8s 没有原生服务依赖,用 init container 模拟 |
healthcheck | livenessProbe/readinessProbe | 语法不同,需手动改写 |
deploy.resources | resources.limits/requests | 字段名不同 |
secrets | Secret + volumeMount | 需要手动创建 Secret |
networks | NetworkPolicy | 默认 K8s 网络全通,需手动加策略 |
deploy.replicas | Deployment replicas | 直接对应 |
volumes | PersistentVolumeClaim | 需要定义 StorageClass |
手动迁移的关键差异
# Compose 的 depends_on → K8s 的 init container
# Compose 的 healthcheck → K8s 的 probe
# Compose 的 restart → K8s 的 deployment strategy
# Compose 的 deploy.replicas → K8s 的 deployment replicas
# Compose 的 networks → K8s 的 NetworkPolicy + Service
迁移后的 K8s 配置示例
# web-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-web
labels:
app: myapp
component: web
spec:
replicas: 3
selector:
matchLabels:
app: myapp
component: web
template:
metadata:
labels:
app: myapp
component: web
spec:
initContainers:
- name: wait-for-db
image: postgres:16-alpine
command: ['sh', '-c', 'until pg_isready -h postgres -U myuser; do sleep 2; done']
containers:
- name: web
image: myapp/web:latest
ports:
- containerPort: 8080
env:
- name: DB_HOST
value: postgres
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: myapp-secret
key: db-password
resources:
requests:
cpu: 500m
memory: 256Mi
limits:
cpu: "2"
memory: 1Gi
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
生产环境运维命令速查
# ========== 日常操作 ==========
docker compose up -d # 后台启动全部服务
docker compose up -d --build web # 重新构建并启动指定服务
docker compose down # 停止并删除容器、网络
docker compose down -v # 同时删除数据卷(慎用!)
docker compose restart web # 重启指定服务
docker compose stop web # 停止但不删除
docker compose start web # 启动已停止的服务
# ========== 查看状态 ==========
docker compose ps # 查看服务状态
docker compose ps --format json # JSON 格式输出
docker compose top # 查看容器内进程
docker compose logs -f web # 跟踪日志
docker compose logs --since 10m web # 最近10分钟日志
docker compose logs --tail 100 web # 最后100行
# ========== 调试 ==========
docker compose exec web sh # 进入容器
docker compose exec postgres psql -U myuser mydb # 连接数据库
docker compose run --rm web ./migrate up # 一次性运行命令
# ========== 配置验证 ==========
docker compose config # 查看最终合并后的配置
docker compose config --services # 列出所有服务名
docker compose config --volumes # 列出所有卷
常见生产事故与防范
1. 磁盘写满
原因:日志没有配 max-size/max-file,或数据卷无监控。
防范:
# 定期检查磁盘
df -h /var/lib/docker
# 配置告警
# 当 /var/lib/docker 使用率 > 80% 时告警
2. 容器频繁重启
原因:健康检查配置不当(start_period 太短),或依赖服务未就绪。
防范:
healthcheck:
start_period: 30s # 给足启动时间
retries: 5 # 多给几次机会
3. 密钥泄露
原因:.env 文件提交到 Git,或密码硬编码在 compose 文件。
防范:
# .gitignore
echo ".env*" >> .gitignore
echo "secrets/" >> .gitignore
# 用 docker compose config 检查是否有明文密钥
docker compose config | grep -i password
4. 版本不一致
原因:不同环境使用不同版本的 compose 文件,配置漂移。
防范:所有 compose 文件纳入版本控制,CI/CD 中用 docker compose config 校验配置。
总结
Docker Compose 在生产环境中完全可以用,关键在于配好以下几件事:
- 健康检查必须有:没有健康检查的依赖管理等于没有依赖管理。
start_period要给足,test命令要做真正的就绪检查而非简单的端口探测。 - 资源限制必须配:不配 limits 的容器就是定时炸弹,一个内存泄漏能拖垮整台宿主机。
- 日志必须限制大小:
max-size和max-file是底线,全局配置在daemon.json中兜底。 - 密钥不能硬编码:Docker Secrets 是最简单的方案,规模大了上 Vault。
- 网络要隔离:后端网络用
internal: true,不要图省事全用一个 bridge 网络。 - 迁移要有规划:当规模超过 5 节点或需要高级调度能力时,果断迁移到 K8s。Kompose 能完成 70% 的机械工作,剩下的手动改。
Compose 的核心价值在于简单。如果你的编排需求已经复杂到需要频繁查文档才能写出 compose 文件,那说明该迁移了。工具的选择应该匹配问题规模,而不是追逐技术潮流。
参考资料与致谢
本文在撰写过程中参考了以下资料,感谢原作者的贡献:
- Compose 规范 — GitHub 开源社区,参考了Compose 规范相关内容
- grpc_health_probe — GitHub 开源社区,参考了grpc_health_probe相关内容
- Mirantis 官方声明 — Mirantis,参考了Mirantis 官方声明相关内容