适用:使用 systemd 的 Linux 服务器、常规 rootful Docker Engine;Compose 示例使用
docker composeV2。本文为教学故障场景,错误文本和结果为示意,具体输出以现场为准。
一、遭遇情况:刚执行 run,容器就不见了
运行容器时返回一个 ID,看起来创建成功;紧接着 docker ps 却没有它。用 docker ps -a 才看到 Exited (0)。
本篇案例是把服务放到后台运行,启动 shell 随后退出。Docker 看到容器主进程结束,就认为容器应该停止。应用日志没有明显报错,反而容易让初学者误以为是 Docker 无故关闭容器。
二、原因分析:容器不是开机后永远存在的虚拟机
容器围绕指定的主进程运行。一次性任务执行完毕后退出是正常行为;持续提供服务的容器,则应让服务主进程持续在前台运行。
退出码 0 表示主进程按自己的语义成功结束,不保证你想要的 Web 服务一直可用。docker run -d 只是让客户端以分离模式运行容器,不会把一个会立即结束的命令变成长期服务。

三、排查思路:先看状态,再看真正执行的命令
假设容器叫 web:
docker ps -a --filter name=web
docker inspect web --format 'Status={{.State.Status}} Exit={{.State.ExitCode}} Error={{.State.Error}} OOM={{.State.OOMKilled}}'
docker inspect web --format 'Entrypoint={{json .Config.Entrypoint}} Cmd={{json .Config.Cmd}}'
docker inspect web --format 'Path={{.Path}} Args={{json .Args}}'
docker logs --tail 100 --timestamps web
镜像的 ENTRYPOINT、CMD 与运行时传入的参数共同决定实际命令。部署命令末尾多写一个 sh,可能就覆盖了本来正确的默认启动方式。
已退出的容器通常不能用 docker exec 进去。不要因为 exec 失败再推断镜像损坏;先通过 inspect、日志和同镜像的一次性诊断容器读取信息。
四、实践一:一次性任务退出是正常的
docker run --name dt08-once alpine:3.21 sh -c 'echo task-finished'
docker ps -a --filter name=dt08-once
docker inspect dt08-once --format '{{.State.ExitCode}}'
预期输出 task-finished,随后退出码为 0。它完成的是一个短任务,本来就不应该一直运行。备份任务、初始化任务和构建任务也经常如此。
这一步帮助我们先问清楚:“这个容器应该长期运行,还是本来只执行一次?”没有这个前提,退出与故障就容易混淆。
五、实践二:后台化服务导致容器停止
以下仅作实验,不要覆盖已有生产容器的启动命令:
docker run -d --name dt08-background nginx:stable-alpine sh -c 'nginx &'
docker ps -a --filter name=dt08-background
docker logs --tail 20 dt08-background
shell 把 Nginx 放到后台后结束,容器随主进程退出。日志多少和具体退出时机可能有差异,重点是观察实际命令与容器状态。
对官方 Nginx 镜像,可直接使用默认前台启动方式:
docker run -d --name dt08-foreground -p 127.0.0.1:18088:80 nginx:stable-alpine
docker ps --filter name=dt08-foreground
curl -I --max-time 5 http://127.0.0.1:18088
如果自行编写 Dockerfile,典型写法是:
FROM nginx:stable-alpine
CMD ["nginx", "-g", "daemon off;"]
对于需要先完成初始化再启动服务的 shell 脚本,可在最后用 exec 替换 shell:
#!/bin/sh
set -e
# 在这里进行必要且可重复的初始化。
exec nginx -g 'daemon off;'
exec 让服务成为对应主进程,便于接收停止信号。set -e 不是万能异常处理,复杂脚本仍需要明确判断关键步骤是否成功。
六、为什么 tail -f /dev/null 不是修复
加一个永不退出的占位进程,可能让容器继续显示 Up,但真正的服务已经死了。这样只是掩盖故障,健康检查与重启策略也容易围绕错误进程工作。
同样,重启策略无法把错误的启动命令变成正确服务,它可能只是不断运行同一个短任务。先修主进程,再考虑崩溃后的恢复策略。
如果退出码不是 0,应继续结合日志判断命令不存在、执行权限、应用报错或信号终止。不要把所有非零退出码都归类成“缺内存”,第 10 篇会讨论 137 的证据链。
七、验证与总结
修复后检查:实际启动参数符合设计、服务进程持续运行、业务入口返回正确响应、正常停止时没有长期卡住。实验容器结束后逐个清理,只处理本篇自己创建的名称。
这类问题的核心是主进程生命周期。看到 Exited (0),先确认任务类型和命令是否结束;看到 Up,也要继续验证服务,而不是让一个占位 shell 代替应用活着。