服务器重启后容器没跟着起来、容器崩了没人拉、或者反过来——你明明 docker stop 了它却又自己活过来。这三个现象背后都是同一个配置在起作用:重启策略(restart policy)。
很多人图省事一律写 restart: always,结果在"手动停止后宿主机重启,容器又自己跑起来"这种场景踩坑。这篇把四种策略的行为差异讲透,给出一张行为对照表和选型建议。
四种策略
| 策略 | 含义 |
|---|---|
| no | 默认值。任何情况都不自动重启 |
| on-failure[:次数] | 仅当容器非 0 退出码退出时重启,可限制最大重试次数 |
| always | 无论怎么停的都重启;宿主机/Docker 守护进程重启后也拉起 |
| unless-stopped | 类似 always,但手动 stop 过的容器,守护进程重启后不再拉起 |
设置方式:
docker run -d --restart unless-stopped --name app myapp:1.0 # on-failure 限制重试 5 次 docker run -d --restart on-failure:5 --name worker myworker:1.0Compose:
services: app: image: myapp:1.0 restart: unless-stopped行为对照表(核心)
四种策略在三个关键场景下的表现:
| 场景 | no | on-failure | always | unless-stopped |
|---|---|---|---|---|
| 容器崩溃(非 0 退出) | 不重启 | 重启 | 重启 | 重启 |
| 容器正常退出(退出码 0) | 不重启 | 不重启 | 重启 | 重启 |
| 手动 docker stop 后 | 保持停 | 保持停 | 保持停 | 保持停 |
| 手动 stop 后宿主机重启 | 保持停 | 保持停 | 自动拉起 | 保持停 |
| 未手动 stop,宿主机重启 | 保持停 | 保持停 | 自动拉起 | 自动拉起 |
always 和 unless-stopped 的唯一区别就在第四行:手动 stop 之后宿主机重启,always 会把容器再拉起来(因为它只认"是否被 stop 过"在 daemon 重启时被重置),unless-stopped 会记住"你手动停过",保持停止。
这个差异在运维时很要命:你为了维护手动 stop 了一个容器,重启服务器后发现它又跑起来了(always),可能干扰维护窗口或抢占端口。用 unless-stopped 就不会。
on-failure 的"正常退出不重启":如果容器以退出码 0 结束(比如一个跑完就退的批处理任务),on-failure 不会重启它——这正是批处理想要的行为。而 always 会把它再拉起来无限循环跑,批处理任务用 always 是灾难。
查看和修改现有容器的策略
查看:
docker inspect <容器名> --format '{{.HostConfig.RestartPolicy.Name}} {{.HostConfig.RestartPolicy.MaximumRetryCount}}' # 输出如:unless-stopped 0 # on-failure 5批量看所有容器:
docker ps -a --format "{{.Names}}\t{{.Status}}" for c in $(docker ps -aq); do echo -n "$(docker inspect $c --format '{{.Name}} ')"; docker inspect $c --format '{{.HostConfig.RestartPolicy.Name}}'; done修改运行中的容器(不用重建):
docker update --restart unless-stopped <容器名> # 取消自动重启 docker update --restart no <容器名>docker update 对重启策略立即生效,不用重启容器,运维改策略很方便。
选型建议
长期运行的服务(Web、数据库、中间件)→ unless-stopped
理由:崩溃要自动拉起,宿主机重启要自动恢复,但尊重运维的手动停止。这是生产环境最稳妥的默认值。
批处理 / 一次性任务(跑完就退)→ no 或 on-failure:N
理由:正常退出(码 0)不该重启。失败时希望重试几次就用 on-failure:3,避免无限重试。绝不要用 always。
容易抖动但希望有限重试的服务→ on-failure:N
理由:限制重试次数,避免 crash loop 把资源耗死或把日志刷爆。超过次数后保持停止,等人介入。
开发 / 测试容器→ no
理由:开发时你经常手动 stop/start,自动重启会干扰调试。
重启的退避机制(backoff)
Docker 对自动重启有指数退避:第一次崩溃后等 100ms 重启,再崩等 200ms、400ms、800ms……最长到 1 分钟。目的是避免 crash loop 瞬间打满 CPU 和日志。
这意味着:容器崩溃后不会立刻复活,docker ps 里可能短暂看到 Restarting 状态:
docker ps # STATUS: Restarting (1) 10 seconds agoRestarting (1) 里的 1 是退出码。看到这个状态说明容器在反复崩溃-重启循环,用 docker logs 看崩溃原因,而不是怪重启策略。
容器成功运行 10 秒后,退避计数器重置。所以一个稳定跑了很久再崩的容器,重启会很快(从 100ms 重新开始)。
几个常见误区
误区一:restart 能修复崩溃。不能。重启策略只是"崩了再拉起来",崩溃的根因(配置错、OOM、依赖没就绪)还在。容器反复 Restarting 时先查 docker logs,别指望重启策略兜底。
误区二:on-failure 会对任何停止都重试。只对非 0 退出码。正常退出(0)不重试。想"无论如何都拉起"用 always/unless-stopped。
误区三:docker stop 后策略就失效了。stop 之后容器是 stopped 状态,策略不触发(没在崩)。但宿主机重启后,always 会无视你之前的 stop 把容器拉起,unless-stopped 才不会。这是两者关键差异。
误区四:Compose 的 restart 和 deploy.restart_policy 是一回事。单机 docker compose up 用 restart: 字段;swarm 模式用 deploy.restart_policy。非 swarm 场景写 restart: 就够了,两个都写可能冲突。
与依赖顺序的配合
重启策略只管"单个容器崩了拉起来",不管容器之间的启动顺序。如果 app 依赖 mysql,宿主机重启后两者都被拉起,但 mysql 可能还没就绪 app 就启动连库失败。
配合健康检查依赖(Compose)才能解决顺序问题:
services: mysql: image: mysql:8.4 restart: unless-stopped healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] interval: 10s retries: 10 app: image: myapp:1.0 restart: unless-stopped depends_on: mysql: condition: service_healthy这样宿主机重启后,app 会等 mysql 健康检查通过才启动,避免"重启后应用连不上库"的经典问题。重启策略 + 健康检查依赖,两者配合才是完整的自愈方案。
小结
四种策略一句话记:no 不管、on-failure 只管崩、always 永远拉、unless-stopped 永远拉但尊重手动停止。
选型:长期服务用 unless-stopped(生产默认),批处理用 no 或 on-failure:N,开发用 no。查看用 docker inspect 的 RestartPolicy 字段,修改用 docker update --restart 不用重建。
记住两个关键点:always 和 unless-stopped 的区别只在"手动 stop 后宿主机重启"这一场景;重启策略不修复崩溃也不管启动顺序,崩溃循环查日志、顺序问题配健康检查依赖。把这两点搞清楚,重启策略就不再是玄学配置。