Docker 容器启动即退出排查:exit code、logs 与前台进程

    发布时间:2026-09-23 04:31 更新时间:2026-09-23 04:31 阅读量:0

    刚接触 Docker 的站长常遇到一个场景:docker run 敲下去,终端一闪就回到提示符,docker ps 里空空如也,只有 docker ps -a 能看到一个状态为 Exited (0) 或 Exited (1) 的容器。不少人第一反应是"镜像坏了",其实绝大多数情况下镜像没问题,问题出在容器内没有一个持续运行的前台进程,或者进程刚启动就因报错退出。搞清楚退出码的含义、日志为什么是空的,基本能覆盖八成以上的此类故障。

    先看懂 exit code:容器为什么"死"了

    容器的生命周期跟着 PID 1 走。容器里的 1 号进程结束,容器就随之停止,退出码就是 1 号进程的返回值。所以查退出码不是看热闹,而是看内核和 Docker 给出的死因分类。

    0 表示进程正常结束。对容器来说这很尴尬:脚本跑完就退、后台服务被 daemon 化、命令只是打印了一行帮助信息,都会以 0 退出。这类"成功退出"最容易被误判成故障,本质是镜像的启动命令不是长驻进程。

    1 通常是应用自身报错,比如配置文件缺失、数据库连不上、端口被占用、依赖库找不到。具体原因必须看日志,不能只看这个数字。

    126 和 127 多半和入口脚本有关:126 往往是命令存在但没有可执行权限,127 是命令根本找不到,常见于自定义 ENTRYPOINT 脚本没加执行位,或者基础镜像里没有这个二进制。

    137 是 128+9,说明进程被 SIGKILL 强杀,最常见的原因是内存超限触发 cgroup OOM Killer,也可能是 docker stop 超时后 Docker 补的刀。143 是 128+15,即进程收到了 SIGTERM,一般是 docker stop 或编排系统正常发信号停止,属于预期行为。

    # 查看所有容器(含已退出)的状态与退出码
    docker ps -a --format "table {{.Names}}\t{{.Status}}\t{{.Image}}"
    
    

    精确查看某个容器的退出码与启动命令

    docker inspect --format 'ExitCode={{.State.ExitCode}} Error={{.State.Error}}' 容器名 docker inspect --format 'Cmd={{.Config.Cmd}} Entrypoint={{.Config.Entrypoint}}' 容器名

    如果 .State.Error 非空,通常说明容器连进程都没拉起来,比如挂载路径不存在、可执行文件格式不匹配,这类信息比退出码更直接。

    docker logs 为空?分清 stdout 与日志文件

    很多人卡在"容器退了但 docker logs 什么都没有"。这里有个前提要先明确:docker logs 只采集容器内 1 号进程及其子进程写到 stdout/stderr 的内容。如果应用把日志写进容器内的文件,比如 /var/log/app/error.log,那 docker logs 自然是空的,得进容器或从挂载卷里翻文件。

    反过来,如果应用是后台化运行的,日志被重定向到文件,同时前台进程立刻退出,容器就会秒退且日志空白。这也是下面要讲的前台进程误区的典型表现。

    排查时可以按顺序做几件事:一是加 --tail 看最后几行,避免被海量输出淹没;二是临时覆盖启动命令,绕过入口脚本直接开个 shell 进去看现场;三是检查应用是不是把日志写到了别处。

    # 查看日志最后 50 行并带时间戳
    docker logs --tail 50 -t 容器名
    
    

    用交互 shell 覆盖默认 CMD,进容器手动复现启动过程

    docker run --rm -it --entrypoint /bin/sh 镜像名

    若镜像连 sh 都没有(精简镜像),改用临时容器挂载其文件系统查看

    具体可用命令以镜像实际提供为准

    进入容器后,手动执行镜像里的启动脚本,比如 /usr/local/bin/start.sh,报错信息会直接打在屏幕上,比隔着一层 docker logs 更好定位。注意 --entrypoint 会覆盖镜像的 ENTRYPOINT,如果原入口脚本承担了环境初始化工作,这样进去可能看到的是"未初始化"的状态,需要自行判断。

    前台进程误区:容器不是虚拟机

    这是新手最容易踩的坑。在虚拟机里,systemd 会负责把服务拉起并托管,进程可以 daemon 化后返回;但容器里没有 systemd(除非你专门构建了带 init 的镜像),容器只认 PID 1。你的启动命令一旦返回,容器就结束。

    典型的错误写法是在 Dockerfile 里写 CMD service nginx start,或者 CMD nginx && tail -f /dev/null 这种拼凑。前者依赖 init 脚本,行为不确定;后者看似保住了容器,实际是拿一个无关进程硬撑,nginx 真的崩了容器也不会退出,监控告警全部失效。

    正确做法是让真正的主进程以前台模式运行,成为 PID 1。以 Nginx 为例,官方镜像的 CMD 是 nginx -g 'daemon off;',即关闭后台模式,让 master 进程待在前台。自己写 Dockerfile 时,入口命令也要遵循同样的原则。

    # 错误示范:命令执行完就返回,容器随即退出
    CMD ["sh", "-c", "python3 app.py &"]
    
    

    正确示范:应用保持前台运行,成为 PID 1

    CMD ["python3", "app.py"]

    Nginx 场景

    CMD ["nginx", "-g", "daemon off;"]

    如果确实需要在容器里跑多个进程,比如一个 Web 服务和一个小型定时任务,建议拆成两个容器,用 Compose 编排并通过网络互访;实在要合并,也应使用 tini、dumb-init 这类轻量 init,负责转发信号和回收僵尸进程,而不是随手加个 sleep infinity 糊弄过去。此外还要注意 docker run 镜像名 某个命令 的写法:命令行末尾的命令会覆盖镜像的 CMD,如果你只是想调试,别忘了它会改变默认启动行为。

    小结与下一步

    容器启动即退出,排查顺序可以固定成三步:先用 docker inspect 看 ExitCode 和 Error 判断死因大类,再用 docker logs 或临时 shell 找具体报错,最后回到镜像的启动命令,确认它是不是前台长驻进程。0 退出往往意味着命令本身就不该放在容器里跑,137 优先查内存限制,1 和 127 则基本要靠日志说话。把这套流程走顺,再去处理健康检查、重启策略和日志轮转,就会顺畅很多。

    继续阅读

    📑 📅
    MySQL单表过亿后的分页优化:延迟关联与覆盖索引 2026-09-23
    Nginx 反代下 Cookie 域与 Path 错乱排查实战 2026-09-23
    Linux 改完 fstab 重启起不来:UUID 混用与救援恢复 2026-09-22
    MySQL备份文件损坏怎么验证:一致性校验与恢复演练 2026-09-22
    Nginx WebSocket 与 SSE 并存:proxy_buffering 冲突排查 2026-09-22
    Linux 服务器 CPU 软中断 si 偏高排查:网卡多队列、RPS 与内核参数调整 2026-09-23
    Nginx客户端与后端长连接谁在复用:端口耗尽排查顺序 2026-09-23
    Linux conntrack 表满导致丢包:现场定位与容量评估 2026-09-23
    rsyslog 与 journald 双写日志重复或丢失排查 2026-09-23
    PostgreSQL表膨胀与autovacuum不生效排查实战 2026-09-23