容器状态显示 running,只能说明主进程还在。它不保证端口能响应,也不保证数据库连接正常。健康检查把“可用”的判断写成可以重复执行的命令。
本项目有两层健康检查:
redis-cli ping,期待得到 PONG。/health;这个接口还会向 Redis 发送 PING。所以 API 进入 healthy 时,表示 HTTP 服务、容器 DNS 和 Redis 连接都正常。
健康检查常用参数:
可以把服务状态分成三层:
running。Docker healthcheck 主要回答第二层的一个周期性采样。它不是完整监控系统,不会统计延迟分位数、错误率或业务正确性,也不会自动修复所有故障。
一条好的检查应当:
wget、curl 或专用检查程序。本项目 API 检查 /health,接口向 Redis PING,覆盖关键依赖;但它也意味着 Redis 短暂抖动会让 API 标记 unhealthy。这是我们有意选择的“就绪语义”,不是唯一设计。
start_period 期间的失败通常不计入达到 unhealthy 所需的连续失败次数;服务一旦在此期间检查成功,就会被视为已启动。之后每隔 interval 发起检查,单次超过 timeout 视为失败,连续达到 retries 后状态变为 unhealthy。
因此最坏检测时间不能只看 retries × interval,还要考虑启动宽限、每次执行时长和调度时点。参数过短会制造误报,过长则延迟发现真实故障,应根据应用启动耗时和恢复目标设置。
$ docker compose ps
NAME SERVICE STATUS
docker-task-api-api-1 api Up 20 seconds (healthy)
docker-task-api-redis-1 redis Up 25 seconds (healthy)查看 API 最近一次健康检查:
$ docker inspect docker-task-api-api-1 \
--format '{{range .State.Health.Log}}{{.ExitCode}} {{.Output}}{{end}}'
0 {"status":"ok"}退出码 0 表示检查成功,输出中的 status: ok 来自 API 健康接口。若失败,.State.Health.Log 会保留最近几次检查的退出码和输出。
用更完整的格式查看状态和连续失败次数:
$ docker inspect docker-task-api-api-1 \
--format 'status={{.State.Health.Status}} failures={{.State.Health.FailingStreak}}'
status=healthy failures=0健康检查状态通常经历 starting → healthy,连续失败后可能变成 unhealthy。unhealthy 不等于容器自动停止;Docker 会记录状态,是否重建或摘除流量取决于更上层的运行策略。
depends_on 不是永久依赖监控下面的配置让 API 等到 Redis 首次健康后再启动:
depends_on:
redis:
condition: service_healthy它解决启动顺序,但不会在 Redis 后来故障时自动停止 API。应用仍应有连接超时、重试和明确错误响应。我们的 /health 会在 Redis 不可用时返回 503。
depends_on 的三种常见条件表达不同意图:
启动顺序只是降低首次连接竞态,不能把可靠性责任全部推给编排工具。Redis 可能在 API 启动十分钟后故障,所以应用仍需要连接超时、有限重试、错误返回和恢复能力。
暂停 Redis:
$ docker compose pause redis
$ curl -i http://localhost:8080/health
HTTP/1.1 503 Service Unavailable
Content-Type: application/json; charset=utf-8
{"status":"unhealthy"}恢复 Redis:
$ docker compose unpause redis
$ curl -s http://localhost:8080/health
{"status":"ok"}API 进程在整个过程中都存在,但健康接口从 200 变成 503,再恢复为 200。这正是“进程状态”和“服务状态”的区别。
同时查看 Compose 状态,可能需要等待几轮检查才显示 unhealthy:
$ docker compose ps
NAME SERVICE STATUS
docker-task-api-api-1 api Up ... (unhealthy)
docker-task-api-redis-1 redis Up ... (healthy)这里 Redis 被 pause 后,它的进程没有退出,容器甚至可能短时间仍显示 healthy,直到下一轮检查超时并累计失败。状态是按时间采样的,不是瞬时全知。
restart: unless-stopped 会在容器主进程意外退出后重新启动同一容器;它不会因为 healthcheck 变成 unhealthy 就自动重启。一个死锁但仍存在的进程可能长期 unhealthy,却不触发重启策略。
无限快速重试会在依赖故障时制造额外压力。更合理的做法是设置连接和请求超时,采用有限次数与退避间隔,并把最终失败清楚暴露给调用方和日志。对于写请求,还要考虑重试是否会造成重复写入。
Compose 知道 Redis healthcheck 是否成功,却不知道“创建任务”是否应该重试,也不知道数据是否满足业务一致性。把每层责任说清楚,才能避免把一个 depends_on 当成高可用设计。
把配置分成两类,排错会更快:
REDIS_ADDR、数据卷和重启策略。修改 main.go 或 Dockerfile 后,需要重新构建:
$ docker compose up -d --build api只修改 APP_PORT 或环境变量时,镜像内容没有变化,Compose 重建容器即可。
不要把密码写进 Dockerfile、镜像标签或提交到版本库的 .env。开发配置可以使用未提交的 .env;生产密钥应交给部署平台的密钥管理能力。
环境变量、挂载和端口在容器创建时写入配置。restart 只重新启动已有配置,不会重新读取 Compose 文件。docker compose up -d 会比较声明与现有对象,在需要时创建新容器,再用相同服务名加入网络。
.env 有两个容易误解的角色Compose 可以从 .env 读取变量,用于替换 Compose 文件中的 ${...};service 的 environment 或 env_file 则决定哪些变量真正进入容器。用于插值的变量不会自动全部注入容器。排错时应分别检查 docker compose config 与容器的实际环境。
环境变量虽然方便,但会出现在容器配置、进程环境或诊断输出中。高价值密钥应使用专门的 secrets 能力,并限制读取范围;不要因为 .env 未提交就认为内容不会泄露。
Compose 中配置了:
restart: unless-stopped强制结束 API 主进程:
$ docker kill docker-task-api-api-1
$ sleep 2
$ docker inspect docker-task-api-api-1 \
--format 'status={{.State.Status}} restarts={{.RestartCount}}'
status=running restarts=1Docker 根据策略重新启动了同一个容器,RestartCount 增加。重启策略适合处理意外退出,但不能修复错误配置;持续崩溃时,容器可能进入重启循环,需要查看日志。
策略不是越积极越好。错误配置导致程序每秒退出时,always 只会制造重启循环。可靠流程还要限制日志增长、提供退避、告警并修复根因。
更新或拆除服务时,Compose 会请求容器主进程退出。应用应停止接收新请求、等待正在处理的工作完成,并关闭依赖连接。若超过停止宽限期,进程可能被强制终止。
本项目当前用 log.Fatal(server.ListenAndServe()) 简化了代码,还没有实现完整的信号处理。真实服务应捕获 SIGTERM,调用 HTTP Server 的 Shutdown 并设置超时。Docker 配置能发出正确的停止信号,但业务是否安全收尾仍由应用实现。
这也解释了为什么 exec 形式 ENTRYPOINT ["task-api"] 更合适:主进程直接成为 PID 1,终止信号不必经过额外 shell 转发。