Docker 容器与虚拟机最大的区别之一,就是容器的生命周期跟一个前台进程强绑定。很多新手在 docker run -it ubuntu bash 启动交互式容器后,习惯性地输入 exit 退出 Shell,结果容器也跟着变成了 Exited 状态。这个现象并不是 Docker 不稳定,而是容器设计机制决定的:主进程结束了,容器就没有继续运行的理由。理解这一点之后,再掌握退出但不停止容器的几个操作,就可以少走很多弯路。

一、容器为什么会随退出而停止?先看主进程模型
Docker 容器默认没有传统 Linux 系统的 init 进程守护一堆服务,它的生命周期由启动时指定的命令或镜像默认命令决定。这个命令会作为容器内的 PID 1 运行。只要 PID 1 还在运行,容器就处于 Up 状态;一旦 PID 1 退出,无论退出码是 0 还是非 0,容器都会进入 Exited 状态。这个机制可以简单理解成:容器没有前台进程,就没有存活意义。
以交互式容器为例,执行 docker run -it ubuntu bash 时,bash 就是 PID 1。当你在 Shell 里输入 exit 或按 Ctrl+D,实际上是让这个 bash 正常结束。此时容器没有其他前台进程可以接管,Docker 就会把容器停掉。即使 docker exec 进入过容器并启动了后台进程,只要它不是 PID 1,就不能让容器继续存活。因此,想让容器退出后仍然运行,核心思路只有两条:要么在启动时安排一个长期运行的前台命令,要么在退出交互会话时只分离而不结束主进程。
二、退出容器但不停止的三种操作方法
第一种方法是使用分离快捷键 Ctrl+P+Q。它适用于已经通过 docker run -it 进入交互式容器的场景。这里要强调顺序:先按住 Ctrl 键,再按 P 键,松开 P 键但不要松开 Ctrl,接着按 Q 键。这个操作会从当前交互会话中脱离,但不会给容器内的主进程发送结束信号。按下后终端会回到宿主机,而 docker ps 仍能看到容器处于 Up 状态。很多人误按 Ctrl+D 或 Ctrl+C,前者会让 Shell 收到 EOF 而结束,后者可能直接终止前台进程,这两者都会导致容器停止。
第二种方法是改用 docker exec 进入容器。如果你的容器本来就是 docker run -d 后台启动的,那进入容器时应优先使用 docker exec -it 容器名 bash,而不是 docker attach。exec 会在容器内新建一个进程,即使你在 exec 会话里执行 exit,也只是退出这个辅助进程,容器主进程完全不受影响。下面是一组完整示例:
# 后台启动一个 nginx 容器 docker run -d --name web nginx # 通过 exec 进入容器 docker exec -it web bash # 在容器内查看进程后,输入 exit 回到宿主机 exit # 宿主机再次确认容器状态 docker ps --filter name=web
第三种方式是启动时就指定一个不会结束的前台命令。有些镜像默认命令执行完就退出,例如 Ubuntu 镜像直接 docker run -d ubuntu 通常会很快 Exited,因为它没有交互终端和长期任务。如果需要让一个没有实际业务的容器挂起,可以用 sleep infinity 或 tail -f /dev/null 作为前台命令。例如:
# 让 Alpine 容器保持运行,不执行实际业务 docker run -d --name idle alpine sleep infinity # 另一个写法:使用 tail 占用前台 docker run -d --name idle2 alpine tail -f /dev/null
这两种写法都能让容器稳定处于 Up 状态,之后再通过 docker exec -it idle sh 进入容器进行调试、安装软件或查看文件。sleep infinity 更加直观,tail -f /dev/null 也是常见替代方案。
三、常见疑问:attach、restart 和退出码怎么理解
疑问一:docker attach 退出后为什么容器停了?attach 连接的是容器主进程的标准输入输出。如果容器是用交互式命令 bash 启动的,attach 进去后输入 exit,等于直接结束了 PID 1。对于后台启动的容器,虽然 attach 后 exit 不一定会杀主进程,但依然不建议用 attach 做日常进出,因为它的输入输出和信号直接作用于主进程,操作不当容易影响容器状态。更稳妥的习惯是:docker run -d 启动容器,之后一律用 docker exec 进行交互。
疑问二:docker run -d 是不是就等于常驻?不是。-d 只表示容器在后台运行,不占用当前终端,但容器是否持续运行仍要看前台命令。例如 docker run -d ubuntu 通常几秒内就会退出,因为 Ubuntu 镜像默认命令 bash 在非交互模式下没有输入就会结束。想要后台常驻,必须给一个长期前台命令,或者启动像 nginx、redis 这类自带常驻进程的镜像。
疑问三:restart policy 能不能替代退出操作?不能。restart policy 是容器停止后的自动重启策略,它解决的是意外退出后自动拉起,而不是避免退出。常见策略包括 no、on-failure、always 和 unless-stopped。如果希望容器因为错误退出时自动恢复,可以在启动时加 --restart unless-stopped。但需要注意,手动执行 docker stop 后,容器不会因为 restart policy 自动启动,除非 Docker 守护进程重启,并且策略为 always 时行为会更特殊。排查容器状态时,可以用 docker ps -a 看退出码:0 一般表示正常退出,137 常见于被 kill 或 OOM,非 0 错误通常要结合 docker logs 查看。
四、排查容器退出问题与少走弯路建议
遇到容器退出情况,先别急着反复启动,建议按顺序检查。第一步用 docker ps -a 查看容器状态和退出码,确认是 Exited (0) 还是 Exited (137)。第二步使用 docker logs 容器名 查看容器最后的输出,很多镜像会打印错误原因。第三步可以通过 docker inspect -f '{{.State.Status}} {{.State.ExitCode}} {{.State.OOMKilled}}' 容器名 获取结构化状态。这个命令会输出当前状态、退出码以及是否发生 OOM。
# 查看最近容器状态
docker ps -a
# 查看容器日志
docker logs web
# 查看退出状态和退出码
docker inspect -f '{{.State.Status}} {{.State.ExitCode}} {{.State.OOMKilled}}' web
如果退出码是 0,多半是主进程正常结束,比如镜像默认命令执行完、Shell 被 exit。如果退出码是 137,通常说明容器被强制终止,可能是内存不足触发 OOM,也可能是执行了 docker stop 或 docker kill。结合 docker inspect 中的 OOMKilled 字段可以进一步确认是否内存问题。
最后总结几个少走弯路的原则:不要在交互式容器里用 exit 作为离开方式,应该用 Ctrl+P+Q;不要用 docker attach 代替 docker exec;不要以为 docker run -d 可以无条件保活;启动无业务容器时给一个 sleep infinity 之类的长期命令。掌握这些点,就能避免大部分因退出操作导致的容器意外停止。