docker-compose 里 depends_on 写了不管用,启动顺序得靠 healthcheck

举报
phantomxjc 发表于 2026/10/16 08:53:14 2026/10/16
【摘要】 家里那台小机上跑了几个自建服务,其中一个 Python 应用要连 PostgreSQL。每次重启机器之后,应用容器总是起不来,日志第一行就是连不上数据库: 但神奇的是,我手动 docker compose restart app 一次就好了。一开始我以为是玄学,后来发现是我把 dependson 理解错了。 我的 compose 文件里明明写了依赖: dependson 到底保证了什么 查了一圈才

家里那台小机上跑了几个自建服务,其中一个 Python 应用要连 PostgreSQL。每次重启机器之后,应用容器总是起不来,日志第一行就是连不上数据库:

psycopg2.OperationalError: could not connect to server: Connection refused
	Is the server running on host "db" and accepting TCP/IP connections?

但神奇的是,我手动 docker compose restart app 一次就好了。一开始我以为是玄学,后来发现是我把 depends_on 理解错了。

我的 compose 文件里明明写了依赖:

services:
  app:
    build: .
    depends_on:
      - db
  db:
    image: postgres:16

depends_on 到底保证了什么

查了一圈才反应过来:depends_on 只保证容器被创建并启动的顺序,也就是 compose 先去 start db 这个容器,再去 start app。它完全不关心 db 里面的 PostgreSQL 进程有没有初始化完、有没有开始监听端口。

而 PostgreSQL 首次启动要跑 initdb,数据目录大的话还要做一轮恢复,十几二十秒很常见。这段时间里容器已经是 running 状态了,但 5432 端口还没人接。app 恰好在这时候去连,自然被拒。

我手动 restart 一次能好,就是因为那时候数据库已经就绪了。

顺带一提,短语法 - db 和长语法写法不一样,只有长语法能加条件:

depends_on:
  db:
    condition: service_started   # 默认就是这个,等价于短语法

service_started 是默认行为,所以它跟我原来写的完全等价,加了也没用。

让它真的等:healthcheck

要真正等到"服务可用",得让 compose 知道 db 什么时候算健康。这就是 healthcheck 干的事。

数据库这类官方镜像,一般都有自带的探测命令:

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: example
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"]
      interval: 5s
      timeout: 5s
      retries: 10
      start_period: 30s

pg_isready 是 PostgreSQL 自带的小工具,只做连接性检查,很轻。test 用上面这种数组形式是最稳的,不会踩到 shell 引号的坑。

MySQL / MariaDB 对应的是:

healthcheck:
  test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -p$$MYSQL_ROOT_PASSWORD --silent"]

注意 compose 文件里写 $$ 才是字面的 $,不然会被 compose 自己先做一轮变量替换。这个我第一次写的时候漏了,healthcheck 一直失败,查了半天才发现密码根本没传进去。

Redis 更简单:

healthcheck:
  test: ["CMD-SHELL", "redis-cli ping | grep PONG"]

然后把 depends_on 的条件改掉

光有 healthcheck 还不够,依赖方那边要跟着改:

services:
  app:
    build: .
    depends_on:
      db:
        condition: service_healthy
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: example
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"]
      interval: 5s
      timeout: 5s
      retries: 10
      start_period: 30s

service_healthy 的意思是:等 db 的健康检查通过之后,才启动 app。改完之后我连续重启了几次机器,应用再没报过 connection refused。

几个参数的含义顺便记一下,调的时候心里有数:

  • interval:两次检查之间的间隔
  • timeout:单次检查超过这个时间就算失败
  • retries:连续失败几次才判定为 unhealthy
  • start_period:启动初期的宽限期,这段时间里的失败不计入 retries,专门给初始化慢的服务用的

start_period 是我最后才加的。之前数据库恢复数据比较慢,开头几次检查全失败,retries 很快用完被判 unhealthy,结果 app 反而永远起不来了。加上宽限期之后就正常了。

自己写的应用怎么暴露健康状态

如果是自己 build 的镜像,最好在 Dockerfile 里就带上:

HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
  CMD curl -f http://localhost:8000/healthz || exit 1

应用那边加一个 /healthz 接口,别只返回 200,顺手查一下数据库还能不能连:

@app.get("/healthz")
def healthz():
    with engine.connect() as conn:
        conn.execute(text("SELECT 1"))
    return {"ok": True}

这样上游依赖挂掉的时候,这个容器自己也会变成 unhealthy,别的服务就不会再往它身上发请求了。

这里有个坑:很多精简镜像(尤其是 alpine 系)没有装 curl,CMD curl -f ... 会直接 command not found,然后一直失败。要么在镜像里装一下,要么换成镜像里本来就有的 python:

CMD python -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://localhost:8000/healthz').status==200 else 1)"

我就在这上面栽过一次,健康检查永远 unhealthy,但应用本身跑得好好的。

怎么确认它到底健康不健康

不要靠猜,直接看:

docker compose ps

输出里 db 那行会带状态,像 Up 2 minutes (healthy) 或者 (health: starting)。一直卡在 starting 就说明检查没过。

想看每次检查的具体输出:

docker inspect --format '{{json .State.Health}}' <容器名> | python -m json.tool

里面的 Log 数组会列出最近几次检查的 ExitCode 和 Output,命令写错了、密码不对、端口不通,在这里全都看得见。前面那个 $$ 的问题就是靠这条命令定位到的。

小结

现在我的习惯是:compose 里两个服务只要有依赖关系,就给被依赖的那个配 healthcheck,再把依赖方写成 condition: service_healthy。多写五六行,换来的是重启之后不用再手动补一次 restart。

depends_on 本身没毛病,只是它管的是容器启动顺序,不是服务的就绪状态——把这两件事分开想,这个问题就不容易再犯了。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。