Docker Compose部署多服务依赖启动顺序乱了怎么控制
Docker Compose 部署多服务时,依赖启动顺序乱了怎么控制
Docker Compose 跑多服务时,经常会遇到一个现象:明明在 docker-compose.yml 里写了 depends_on,结果应用容器还是报 MySQL connection refused、Redis timeout、Kafka broker not available。看日志会发现,数据库容器确实先启动了,但业务服务启动得更快,连上去的时候数据库还没 ready。
这个问题本质上不是 Docker Compose “没按顺序启动”,而是很多人把“容器启动”和“服务可用”混在一起了。容器进程起来,不代表 MySQL 已经完成初始化,不代表 PostgreSQL 已经能接连接,也不代表 Elasticsearch 集群状态已经可用。
depends_on 只能解决一部分问题
在 Docker Compose 里,depends_on 最常见的写法是这样:
services:
app:
image: my-app:latest
depends_on:
- mysql
mysql:
image: mysql:8.0
这个配置的效果是:Compose 会先启动 mysql 容器,再启动 app 容器。注意,只是“先启动容器”,不是“等 MySQL 能正常处理 SQL 之后再启动 app”。
实际使用中发现,MySQL、PostgreSQL、Redis 这种服务,在本地开发环境可能问题不明显,因为机器快、数据少。但到了云服务器上,尤其是第一次初始化数据目录、挂载远程盘、加载大量数据时,服务 ready 时间可能会从几秒变成几十秒。业务容器一上来就连数据库,失败是很正常的。
Compose 版本差异也要注意
老一些的 Compose 文件里经常看到这种写法:
depends_on:
mysql:
condition: service_healthy
这个写法在 Docker Compose V2 里是可用的,但很多旧文档、旧项目、不同 compose file version 的行为容易让人混淆。现在更建议直接使用当前 Docker Compose 插件,也就是 docker compose,不要再混用老的 docker-compose 二进制版本。
可以用这个命令看版本:
docker compose version
如果线上机器是多年没维护的环境,建议先确认 Compose 版本。很多启动顺序问题,排查半天,最后发现是部署机上的 Compose 行为和本地不一致。
用 healthcheck 判断服务是否真的 ready
控制启动顺序时,比较稳的做法是给被依赖服务加 healthcheck,然后让业务服务依赖它的 healthy 状态。
以 MySQL 为例:
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: appdb
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-prootpass"]
interval: 5s
timeout: 3s
retries: 20
start_period: 30s
app:
image: my-app:latest
depends_on:
mysql:
condition: service_healthy
这里重点看 healthcheck。mysqladmin ping 成功后,mysql 容器才会被标记为 healthy。app 配了 condition: service_healthy,Compose 会等 mysql healthy 后再启动 app。
interval 是检查间隔,timeout 是单次检查超时时间,retries 是失败重试次数,start_period 是容器刚启动时的宽限时间。MySQL 第一次初始化可能比较慢,start_period 不要给太短,不然健康检查还没等服务起来就开始判失败。
Redis 的 healthcheck 写法
Redis 更简单,可以用 redis-cli ping:
services:
redis:
image: redis:7
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 3s
timeout: 2s
retries: 10
如果 Redis 开了密码,就不能直接 ping,需要加 auth:
test: ["CMD", "redis-cli", "-a", "your_password", "ping"]
多说一句,healthcheck 里不要写太重的检查逻辑。比如每次都跑复杂 SQL、查一堆业务表,这种会把健康检查变成负担。健康检查只需要判断服务进程、端口、基础认证、最小可用能力。
depends_on 不是服务异常后的自动编排工具
这里补充一点,很多人以为 app 依赖 mysql healthy 后,后面 mysql 如果挂了,Compose 会自动把 app 停掉或按顺序重启。这个理解不对。
depends_on 主要影响创建和启动阶段。服务运行过程中,某个依赖挂了,Compose 不会像 Kubernetes 那样做复杂的控制器编排。比如 MySQL 运行 10 分钟后崩了,app 容器通常还是继续跑,只是应用日志里会开始报数据库连接错误。
所以线上服务不能只靠 Compose 的启动顺序,业务应用自身也要有重连机制。数据库连接池、Redis client、MQ client 都应该能处理短暂断连。容器只是部署形态,应用的容错能力不能省。
业务容器里加等待脚本,适合兼容老项目
有些项目环境比较旧,Compose 版本不方便升级,或者依赖的服务没有合适的 healthcheck 命令。这种情况下,可以在业务容器启动前加 wait 脚本。
常见做法是用 wait-for-it.sh、dockerize、nc 这类工具等待端口可连接。
例如 app 启动前等 MySQL 3306 端口:
services:
app:
image: my-app:latest
command: ["./wait-for-it.sh", "mysql:3306", "--", "java", "-jar", "app.jar"]
depends_on:
- mysql
这种方式的好处是简单,哪怕 Compose 不支持 service_healthy 也能用。缺点也明显:等端口通,不等于服务完全可用。MySQL 端口起来了,但权限、初始化 SQL、schema migration 还没跑完,业务仍然可能失败。
如果只是开发环境,这种方式通常够用。生产环境建议还是 healthcheck + 应用重试一起上。
不要把数据库迁移和应用启动绑得太死
多服务部署里还有一个常见坑:app 启动时自动执行数据库迁移,比如 Flyway、Liquibase、Prisma migrate、Django migration。单实例时还好,多个 app 副本同时启动时,很容易出现迁移锁冲突,甚至重复执行。
在 Compose 场景里,可以把 migration 拆成单独服务:
services:
migrate:
image: my-app:latest
command: ["java", "-jar", "app.jar", "migrate"]
depends_on:
mysql:
condition: service_healthy
app:
image: my-app:latest
command: ["java", "-jar", "app.jar"]
depends_on:
migrate:
condition: service_completed_successfully
service_completed_successfully 适合一次性任务,比如数据库迁移、初始化索引、生成配置文件。migrate 服务成功退出后,再启动 app。
这里要注意,迁移失败时不要让 app 硬启动。否则应用跑起来后缺表、缺字段,错误更隐蔽。
restart 策略要配,但别指望它解决依赖关系
Compose 里常见 restart 策略有:
restart: "no":不自动重启,默认行为。
restart: always:容器退出就重启,Docker daemon 重启后也会拉起来。
restart: unless-stopped:手动停止后不再自动启动,比较适合长期运行服务。
restart: on-failure:非 0 退出码时重启,适合任务型进程。
实际线上用得比较多的是:
restart: unless-stopped
但 restart 只是让容器失败后再起来,它不理解“我要等 MySQL ready 后再重启 app”。如果 app 因为数据库没连上直接退出,restart 会让它不断重启,直到数据库可用。这个行为有时能救场,但日志会刷得很厉害,而且启动时序不可控。
更稳的做法是:依赖服务用 healthcheck,业务服务内部做重试,容器层用 restart 兜底。
实际排查时看这几个状态就够用了
看容器启动情况:
docker compose ps
如果配置了 healthcheck,会看到类似 healthy、starting、unhealthy 的状态。
看某个服务日志:
docker compose logs -f mysql
看业务服务是否在数据库 ready 前就启动:
docker compose logs -f app
看容器健康检查细节:
docker inspect 容器名
里面的 State.Health 会记录最近几次健康检查的输出。这个信息很有用,尤其是 healthcheck 命令写错、镜像里没有对应 client 工具、密码变量没传进去时,单看 Compose 日志不一定明显。
多服务放到云服务器上,还要考虑机器性能和网络
本地 Compose 一切正常,上云后启动顺序问题变多,除了配置原因,也可能是资源原因。CPU 被打满、磁盘 IO 慢、内存不足触发 OOM,都会导致数据库 ready 时间变长。MySQL 初始化 5 秒和 60 秒,对业务容器来说是完全不同的启动环境。
如果是游戏服、企业业务后台、海外访问服务这类场景,部署 Compose 时别只看容器配置,也要看宿主机线路、带宽、防护和磁盘性能。如果你也在找这种云服务器或高防服务器,可以看看129云,他们有高性能云服务器、G口大带宽服务器、高防服务器租用和海外云计算方案,客服热线 400-9177118。
比如面向东南亚用户的小型业务,可以考虑马来西亚-E型,16C CPU、16G DDR4 ECC、120G SSD、70Mbps 峰值带宽,三网优化,适合跑 Web、API、Redis、MySQL 这类中小规模 Compose 栈。
如果是欧洲方向轻量业务,德国双ISP-B型有 1Gbps 带宽、GTT直连、双ISP,适合对海外访问质量比较敏感的站点。
如果业务容易被 DDoS 打,比如游戏、活动页、登录网关、接口网关,十堰高防-A型的 600Gbps 单机防御更适合放入口服务,再把内部服务通过安全组或内网隔离起来。
生产环境里更推荐的 Compose 写法
下面这个结构在中小型项目里比较常见,MySQL、Redis、app 都有明确关系,启动时不乱,失败后也有基本兜底。
services:
mysql:
image: mysql:8.0
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: appdb
volumes:
- mysql_data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-prootpass"]
interval: 5s
timeout: 3s
retries: 20
start_period: 30s
redis:
image: redis:7
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 3s
timeout: 2s
retries: 10
app:
image: my-app:latest
restart: unless-stopped
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
environment:
DB_HOST: mysql
REDIS_HOST: redis
volumes:
mysql_data:
这个配置解决的是“启动时序”和“基础可用判断”。但业务代码里仍然要处理连接失败、连接池重建、请求超时。尤其是 Java、Go、Node.js 应用,数据库连接池初始化策略不同,有的启动时必须连上数据库,有的第一次请求才建连接。部署前要知道自己应用是哪种行为。
healthcheck 经常踩的坑
镜像里没有检查命令
比如想用 curl 检查 HTTP 服务,但镜像是 alpine 精简版,里面根本没有 curl。健康检查会一直失败。
可以换成 wget,或者在镜像里安装 curl,也可以用应用自带的轻量检查命令。
localhost 和服务名不要混用
在 mysql 容器内部检查自己,用 localhost 可以。app 连接 mysql,要用 Compose service name,也就是 mysql。很多配置把 app 的 DB_HOST 写成 localhost,结果 app 容器会去连自己容器里的 3306,自然连不上。
健康检查通过,不代表业务完全正常
HTTP 服务返回 200,只能说明进程和接口还活着。如果接口内部依赖数据库、缓存、对象存储,也要看检查接口怎么写。生产环境里常见做法是提供 /healthz 和 /readyz,一个看进程存活,一个看依赖是否 ready。
start_period 太短
数据库第一次启动、Elasticsearch 恢复索引、MinIO 扫描数据目录,都可能比较慢。start_period 给 5 秒,很多时候不够。可以根据实际日志调整到 30 秒、60 秒,甚至更长。
什么时候该考虑 Kubernetes
Compose 很适合单机、多容器、小团队部署,也适合开发测试环境。服务数量不多、依赖关系清晰、流量规模可控,用 Compose 没问题。
但如果已经出现这些情况:服务副本很多、需要滚动发布、自动扩缩容、跨主机调度、服务发现、配置中心、密钥管理、灰度发布,那 Compose 会越来越吃力。这时 Kubernetes 的 readinessProbe、livenessProbe、initContainers、Job、Deployment 会更适合。
不过别为了“看起来高级”就上 Kubernetes。很多企业内部系统、管理后台、轻量 SaaS,用 Compose 部署在稳定的云服务器上,配好 healthcheck、restart、备份、监控,维护成本反而更低。
一段可直接改的 app 等待逻辑
如果应用本身能改,建议在启动阶段加重试,而不是数据库连不上就直接退出。伪代码大概是这样:
maxRetry = 30
for i in range(maxRetry):
try:
connectDatabase()
connectRedis()
break
except Exception as e:
sleep(2)
else:
exit(1)
startHttpServer()
这个思路很朴素,但线上很管用。Compose 负责让依赖服务尽量先 ready,应用自己负责面对真实世界里的短暂不可用。云服务器重启、Docker daemon 重启、数据库慢启动、网络抖动,都能靠这层重试减少故障窗口。
如果是 Spring Boot,可以关注 HikariCP 的连接池初始化参数;如果是 Node.js,可以在 ORM 初始化外面包重试;如果是 Go,通常在启动依赖检查里循环 ping DB。不要让应用在依赖服务晚几秒 ready 时直接崩掉。
部署时可以按这个顺序操作
先给数据库、缓存、MQ 这类基础服务加 healthcheck。
再把 app 的 depends_on 改成 condition: service_healthy。
如果有 migration,把它拆成一次性服务,并用 service_completed_successfully 控制 app 启动。
然后给长期运行服务配置 restart: unless-stopped。
最后看 docker compose ps 和 docker compose logs -f,确认 app 日志里不再出现启动阶段的 connection refused。
Compose 文件改完后,可以用下面命令重新创建服务:
docker compose up -d --force-recreate
如果只想看配置是否解析正常:
docker compose config
这个命令会把最终生效的 Compose 配置展开,排查缩进、变量替换、depends_on 写法时很方便。