Docker Compose生产实践:健康检查、依赖顺序、资源限制的完整配置
Docker Compose生产实践:健康检查、依赖顺序、资源限制的完整配置
适读人群:使用Docker Compose部署的Java后端工程师 | 阅读时长:约20分钟 | 适用版本:Docker Compose v2.x
开篇故事
公司有个中台服务,用Docker Compose部署在一台物理机上,已经稳定跑了两年多了。去年有次机器重启(内存条松动,自动关机又开机),我睡得正香,凌晨三点多运维同学打来电话,说服务全挂了,报警短信刷屏。
我远程连上去一看,docker compose up -d确实在重启时自动执行了,但应用容器起来后报错:数据库连接失败。为什么?因为MySQL容器虽然状态是Up,但MySQL进程还在初始化,还没准备好接受连接,应用容器就已经狂扫8306端口了,失败后没有重试机制,直接退出了。
然后更惨的是:Redis容器因为之前没有设置内存限制,在机器内存紧张时被OOM killer干掉了,数据全丢了。这次事故直接推动我们把Compose配置翻新了一遍。
今天把我们生产上用的完整Compose配置方案分享出来,每一行配置都有其存在的理由。
一、核心问题分析
Docker Compose在生产中的三个经典问题
问题一:depends_on的假依赖
很多人以为depends_on能确保被依赖的服务完全就绪再启动自己,实际上它只保证容器的创建顺序,不保证服务可用性。MySQL容器启动了不等于MySQL进程ready,应用容器可能在MySQL还没初始化完成时就尝试连接,导致启动失败。
问题二:资源无限制导致的宿主机崩溃
没有内存限制的容器,在流量激增或内存泄漏时,会无限制地吃宿主机内存,最终触发OOM killer,不仅自己被干掉,还可能把宿主机上的其他服务一起拖死。这在共享机器的场景下尤其危险。
问题三:健康状态不透明
容器进程活着不等于服务正常。Java进程可能出现死锁、GC停顿、线程池耗尽等情况,进程照样存在,但服务实际上已经不可用了。没有健康检查,运维同学只能等到用户投诉才发现问题。
二、原理深度解析
depends_on的condition机制
Docker Compose从v2开始,depends_on支持condition字段,这才是解决启动顺序的正确姿势:
condition有三个值:service_started(默认,只等容器启动)、service_healthy(等健康检查通过)、service_completed_successfully(等容器执行完成退出,用于init job)。
生产环境必须用service_healthy,配合healthcheck才能真正解决启动顺序问题。
资源限制的cgroup实现
Docker的资源限制底层依靠Linux的cgroup(Control Groups)机制实现。mem_limit设置内存上限,达到上限时进程会被OOM kill;cpus限制可使用的CPU核数(支持小数,如0.5表示半个核);cpu_shares是相对权重,影响CPU争用时的分配比例。
在Compose v2中,资源限制写法有所变化,使用deploy.resources而不是顶层的mem_limit:
deploy:
resources:
limits:
cpus: '2.0'
memory: 2G
reservations:
cpus: '0.5'
memory: 512Mlimits是硬上限,reservations是预留(软下限),确保资源紧张时这个容器至少能获得这么多。
三、完整配置实现
生产级完整docker-compose.yml
# docker-compose.yml
# 适用场景:Spring Boot微服务 + MySQL + Redis + Nginx的标准部署
version: '3.9'
# 统一的网络定义
networks:
# 前端网络:Nginx到应用层
frontend:
driver: bridge
ipam:
config:
- subnet: 172.20.1.0/24
# 后端网络:应用层到数据层(不对外暴露)
backend:
driver: bridge
internal: false
ipam:
config:
- subnet: 172.20.2.0/24
# 统一的卷定义
volumes:
mysql_data:
driver: local
redis_data:
driver: local
app_logs:
driver: local
services:
# =============================================
# MySQL服务
# =============================================
mysql:
image: mysql:8.0.36
container_name: mysql-primary
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE:-appdb}
MYSQL_USER: ${MYSQL_USER:-appuser}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
# 设置时区
TZ: Asia/Shanghai
ports:
# 生产建议只绑定内网IP,不要绑定0.0.0.0
- "127.0.0.1:3306:3306"
volumes:
- mysql_data:/var/lib/mysql
- ./config/mysql/my.cnf:/etc/mysql/conf.d/my.cnf:ro
- ./init-sql:/docker-entrypoint-initdb.d:ro
networks:
- backend
# 健康检查:确保MySQL进程真正ready
healthcheck:
test: ["CMD", "mysqladmin", "ping",
"-h", "localhost",
"-u", "root",
"-p${MYSQL_ROOT_PASSWORD}"]
interval: 10s # 每10秒检查一次
timeout: 5s # 超时时间5秒
retries: 5 # 失败5次才标记为unhealthy
start_period: 60s # 启动后60秒内的失败不计入retries
# 资源限制
deploy:
resources:
limits:
cpus: '2.0'
memory: 2G
reservations:
cpus: '0.5'
memory: 512M
# 内核参数优化
sysctls:
net.core.somaxconn: 65535
ulimits:
nofile:
soft: 65536
hard: 65536
# =============================================
# Redis服务
# =============================================
redis:
image: redis:7.2-alpine
container_name: redis-cache
restart: unless-stopped
command: >
redis-server
--requirepass ${REDIS_PASSWORD}
--maxmemory 512mb
--maxmemory-policy allkeys-lru
--save 900 1
--save 300 10
--appendonly yes
--appendfsync everysec
ports:
- "127.0.0.1:6379:6379"
volumes:
- redis_data:/data
networks:
- backend
healthcheck:
test: ["CMD", "redis-cli",
"-a", "${REDIS_PASSWORD}",
"ping"]
interval: 10s
timeout: 3s
retries: 3
start_period: 20s
deploy:
resources:
limits:
cpus: '1.0'
memory: 768M
reservations:
cpus: '0.1'
memory: 256M
# =============================================
# Spring Boot应用服务
# =============================================
order-service:
image: registry.company.com/order-service:${APP_VERSION:-latest}
container_name: order-service
restart: unless-stopped
environment:
SPRING_PROFILES_ACTIVE: prod
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/${MYSQL_DATABASE:-appdb}?useSSL=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
SPRING_DATASOURCE_USERNAME: ${MYSQL_USER:-appuser}
SPRING_DATASOURCE_PASSWORD: ${MYSQL_PASSWORD}
SPRING_REDIS_HOST: redis
SPRING_REDIS_PASSWORD: ${REDIS_PASSWORD}
JAVA_OPTS: >-
-XX:+UseContainerSupport
-XX:MaxRAMPercentage=75.0
-XX:InitialRAMPercentage=50.0
-XX:+ExitOnOutOfMemoryError
-XX:+UseG1GC
-Djava.security.egd=file:/dev/./urandom
-Dnetworkaddress.cache.ttl=10
TZ: Asia/Shanghai
ports:
- "8080:8080"
volumes:
- app_logs:/app/logs
- ./config/app:/app/config:ro
networks:
- frontend
- backend
# 真正的依赖顺序控制
depends_on:
mysql:
condition: service_healthy # MySQL健康才启动
redis:
condition: service_healthy # Redis健康才启动
healthcheck:
# 调用Spring Boot Actuator健康端点
test: ["CMD-SHELL",
"curl -sf http://localhost:8080/actuator/health | grep -q '\"status\":\"UP\"' || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 120s # Spring Boot启动慢,给足时间
deploy:
resources:
limits:
cpus: '4.0'
memory: 4G
reservations:
cpus: '1.0'
memory: 1G
logging:
driver: "json-file"
options:
max-size: "100m"
max-file: "10"
# =============================================
# Nginx反向代理
# =============================================
nginx:
image: nginx:1.25-alpine
container_name: nginx-proxy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./config/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./config/nginx/conf.d:/etc/nginx/conf.d:ro
- ./ssl:/etc/nginx/ssl:ro
networks:
- frontend
depends_on:
order-service:
condition: service_healthy # 等应用健康才接流量
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost/health"]
interval: 10s
timeout: 5s
retries: 3
start_period: 10s
deploy:
resources:
limits:
cpus: '1.0'
memory: 256M
reservations:
cpus: '0.1'
memory: 64M
logging:
driver: "json-file"
options:
max-size: "50m"
max-file: "5"配套的.env文件
# .env - 敏感配置,不提交到git
MYSQL_ROOT_PASSWORD=your_strong_root_password_here
MYSQL_DATABASE=orderdb
MYSQL_USER=orderapp
MYSQL_PASSWORD=your_strong_app_password_here
REDIS_PASSWORD=your_strong_redis_password_here
APP_VERSION=1.2.3MySQL配置文件
# config/mysql/my.cnf
[mysqld]
# 基础配置
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
default-time-zone = '+8:00'
# 连接配置
max_connections = 500
max_connect_errors = 1000
wait_timeout = 600
interactive_timeout = 600
# InnoDB配置
innodb_buffer_pool_size = 1G
innodb_log_file_size = 256M
innodb_flush_log_at_trx_commit = 1
innodb_file_per_table = ON
# 慢查询日志
slow_query_log = ON
slow_query_log_file = /var/lib/mysql/slow.log
long_query_time = 2
# 二进制日志(如果需要主从复制)
# log_bin = mysql-bin
# binlog_format = ROW
# server_id = 1Spring Boot Actuator健康检查配置
# application-prod.yml
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: when-authorized
# 自定义健康检查组
group:
liveness:
include: ping,diskSpace
readiness:
include: ping,db,redis,diskSpace
health:
db:
enabled: true
redis:
enabled: true四、生产最佳实践
使用profiles管理多环境
# docker-compose.yml中的profiles用法
services:
order-service:
# 不指定profiles,始终启动
image: order-service:latest
# 只在debug profile下启动
adminer:
image: adminer:latest
profiles: ["debug"]
ports:
- "8081:8080"
depends_on:
- mysql# 生产环境只启动核心服务
docker compose up -d
# 调试时额外启动adminer
docker compose --profile debug up -d滚动更新策略
Docker Compose本身不支持真正的滚动更新,但可以用以下方式减少停机时间:
#!/bin/bash
# 蓝绿部署脚本(简化版)
set -e
NEW_VERSION=$1
SERVICE_NAME="order-service"
echo "开始更新 ${SERVICE_NAME} 到版本 ${NEW_VERSION}..."
# 更新镜像版本
export APP_VERSION=${NEW_VERSION}
# 先拉取新镜像
docker compose pull ${SERVICE_NAME}
# 重启服务(会有短暂停机,约30秒)
docker compose up -d --no-deps ${SERVICE_NAME}
# 等待健康检查通过
echo "等待服务健康检查..."
timeout 120 bash -c 'until docker compose ps order-service | grep -q "healthy"; do sleep 5; done'
echo "更新完成!"
docker compose ps日志聚合配置
# 统一的日志配置,方便日志收集
x-logging: &default-logging
driver: "json-file"
options:
max-size: "100m"
max-file: "10"
labels: "service_name"
tag: "{{.Name}}/{{.ID}}"
services:
order-service:
logging: *default-logging # 引用锚点
mysql:
logging: *default-logging五、踩坑实录
坑一:healthcheck的start_period设置不当导致死循环重启
有次部署一个新服务,healthcheck配置如下:
healthcheck:
test: ["CMD-SHELL", "curl -sf http://localhost:8080/actuator/health"]
interval: 10s
timeout: 5s
retries: 3
start_period: 30s # 问题所在!Spring Boot这个服务加载了大量配置,第一次启动需要将近90秒,但我把start_period设成了30秒。结果:30秒后开始计算retries,又过30秒(3次×10秒)就标记为unhealthy,然后restart: unless-stopped触发重启,重启后又没法在30秒内启动完……无限重启循环。
正确做法是把start_period设成至少比最慢启动时间多50%的余量,宁可设长,不能设短:
healthcheck:
start_period: 150s # 实际启动需要90s,给150s余量坑二:depends_on条件检查只在启动时生效
有个同事以为depends_on: condition: service_healthy是持续监控的,MySQL挂掉后应用容器也会自动重启。实际上完全不是这回事。
depends_on的condition检查只在容器启动时执行一次,用于决定是否允许该容器开始启动。一旦所有依赖都健康、应用容器已经启动,depends_on就完成使命了,后续MySQL挂了应用不会自动感知。
应用层面的重连逻辑必须在代码里实现,比如Spring的数据源连接池有重连配置,Redis客户端有重试配置。不能指望Compose帮你做应用层面的故障恢复。
坑三:deploy.resources在非Swarm模式下不生效
这是个坑了无数人的问题。deploy块下的资源限制配置,在docker compose up命令下默认是不生效的!它只在Docker Swarm模式(docker stack deploy)下有效。
解决方案一:使用--compatibility标志(已废弃,不推荐):
docker compose --compatibility up -d解决方案二:改用顶层的资源配置(Compose v2的正确写法):
从Compose规范3.9开始,在非Swarm模式下,deploy.resources是被识别并生效的,但需要确保Docker Compose版本足够新(v2.0+)。建议统一升级到最新的Docker Compose v2版本。
验证资源限制是否生效:
# 查看容器资源限制
docker inspect mysql-primary | grep -A5 "Memory\|CpuShares\|NanoCpus"
# 实时监控资源使用
docker stats mysql-primary redis-cache order-service六、总结
Docker Compose的生产配置,核心是三件事不能省:
健康检查必须配,而且start_period要给足,condition: service_healthy才能解决真正的依赖顺序问题。资源限制必须配,哪怕设得宽松一点,至少能防止单个容器把整台机器吃垮。日志限制必须配,不然磁盘迟早被日志撑爆,到时候修都没地方写临时文件。
这三件事做好了,Compose的单机部署场景基本上就能做到稳定运行。当然,如果业务规模上来了,该迁K8s还是得迁K8s,Compose的局限性在于缺乏跨机调度和自动故障迁移能力。
