容器起不来,是运维日常里最频繁的求助之一。表现可能有很多种:docker ps 看不到该容器,docker ps -a 显示 Exited (1),日志里只有一行莫名报错,或者容器起来了但几秒后又自动退出。更麻烦的是,很多容器启动失败时你根本没机会进去看——因为它还没运行就已经挂了。这篇文章给出一套固定的排查流程:先用退出码和状态定位方向,再按常见原因逐类验证,配合 docker inspect、docker events、--entrypoint sh 这些工具把问题揪出来。核心目标是让你从"看到报错就懵"变成"按步骤排查、十分钟内定位"。
一、第一步永远是看状态和退出码
容器启动失败,先别急着改配置,先看它是"根本没起来"还是"起来了又退出",这两种情况的排查方向完全不同。
# 看所有容器(含已退出的),STATUS 列会显示 Exited (退出码)
docker ps -a
# 看某个容器的日志(启动失败的容器往往在这里暴露原因)
docker logs <容器名或ID>
# 看容器详细状态,重点看 State 里的 Status / ExitCode / OOMKilled / Error
docker inspect <容器名或ID> --format '{{json .State}}' | python3 -m json.tool
# 若没有 python,可用:
docker inspect <容器名或ID> | grep -A 20 '"State"'
退出码是最有价值的线索,记住这几个常见值:
| 退出码 | 含义 | 常见原因 |
|---|---|---|
| 0 | 正常退出 | 主进程执行完就结束了,容器使命完成即退出 |
| 1 | 应用报错 | 程序自身异常、配置错误、缺少依赖 |
| 126 | 命令不可执行 | 权限不足,或入口脚本没有执行权限 |
| 127 | 命令未找到 | 入口命令/脚本不存在或路径写错 |
| 137 | 被 SIGKILL 杀死 | 最常见是 OOM 被杀,也可能是外部强杀 |
| 139 | 段错误 | 程序崩溃,常见于架构不匹配或二进制损坏 |
| 143 | 被 SIGTERM 正常终止 | 收到停止信号,通常是被 docker stop 或宿主重启 |
这里有个高频误区:容器退出码是 0,很多人以为没问题,其实"启动后立即退出码 0"恰恰是新手最常见的坑之一。原因是容器的主进程(PID 1)执行完就退出了——容器生命周期跟着主进程走,主进程一结束容器就停。比如你用 docker run ubuntu echo hi,echo 执行完容器自然退出,这不是故障,而是容器的正常工作方式。
二、常见原因逐类排查
原因一:端口被占用
报错通常是 bind: address already in use。宿主机的某个端口已经被别的进程或容器占了。
# 查端口占用
sudo ss -lntp | grep ':8080'
sudo lsof -i :8080 # 需装 lsof:Debian/Ubuntu apt install lsof;RHEL 系 yum install lsof
# 查是否有其他容器占用了该端口
docker ps --format 'table {{.Names}}\t{{.Ports}}' | grep 8080
解决方式:换成别的宿主端口(-p 8081:80),或者停掉占用端口的进程。要注意 -p 8080:80 的格式是"宿主端口:容器端口",两个写反了也会出问题。
原因二:挂载路径或权限错误
bind mount 时如果宿主机路径不存在,或者文件权限不对,容器可能启动失败或无法读写。Docker 在挂载不存在的宿主机目录时通常会自动创建空目录,但如果是把文件挂到目录位置、或 SELinux 环境缺标签,就会报错。
# 确认宿主机路径存在且权限正确
ls -ld /data/app /data/app/config.yml
# SELinux 系统(RHEL/CentOS/Rocky)挂载常需加 :z 或 :Z 标签
docker run -v /data/app:/app:z myapp
# 只读挂载用 :ro,注意应用若需要写就会失败
docker run -v /data/conf:/etc/app:ro myapp
权限问题的典型症状是日志里出现 Permission denied,而 root 手工进容器却能读——这通常是容器内进程以非 root 用户运行,而宿主文件属主/权限不允许。
原因三:CMD 或 ENTRYPOINT 不存在
退出码 127 基本就是这个原因:镜像里写的入口命令找不到。可能是 Dockerfile 里 CMD ["appp"] 拼错,或脚本路径不对,或脚本没有执行权限(退出码 126)。
# 看镜像定义的入口
docker inspect <镜像名> --format 'ENTRYPOINT={{.Config.Entrypoint}} CMD={{.Config.Cmd}}'
# 确认脚本在镜像里的位置和权限
docker run --rm --entrypoint sh <镜像名> -c 'ls -l /app; which app'
原因四:依赖服务未就绪
应用启动时连不上数据库或缓存,直接崩溃退出。这种情况下应用日志里会有 Connection refused、could not connect to server 之类。要注意容器启动顺序不能保证依赖"可用",解决方案是给依赖加 healthcheck,或在应用里做连接重试(第十四篇 Compose 部分讲过)。
原因五:内存超限被 OOM 杀死
退出码 137 且 docker inspect 里 OOMKilled: true,说明容器内存触顶被内核杀掉。常见于 JVM 应用没设堆上限、或内存限制设得过小。
docker inspect <容器名> --format 'OOMKilled={{.State.OOMKilled}} ExitCode={{.State.ExitCode}}'
docker stats --no-stream <容器名> # 看实时内存占用
解决办法:要么调大 --memory,要么让应用自身限制内存(如 JVM 设 -Xmx),两者要匹配,不能只调一边。
原因六:镜像架构不符
在 x86 服务器上拉了 arm64 镜像,或在 Apple 芯片开发机上构建的镜像拿到 amd64 服务器跑,会报 exec format error(退出码常见 139/126 或直接提示格式错误)。
uname -m # 宿主机架构
docker inspect <镜像名> --format '{{.Architecture}}' # 镜像架构
# 交叉构建时明确指定平台
docker build --platform linux/amd64 -t myapp .
docker run --platform linux/amd64 myapp
原因七:磁盘满
磁盘写满时,Docker 创建容器、写日志、解压镜像都会失败。报错可能是 no space left on device。
df -h /var/lib/docker
docker system df # 看镜像/容器/卷各占多少
排查思路是先看 docker system df,再考虑清理(清理操作有风险,详见第二十篇,务必确认再动手)。
三、进不去容器怎么调试
当容器一启动就退出,常规的 docker exec 用不了,这时有两个利器。
第一个是覆盖入口,用交互式 shell 启动,绕过原本会崩溃的命令。
# 用 sh 覆盖 ENTRYPOINT,进容器里手工查(有的镜像没有 bash,用 sh)
docker run --rm -it --entrypoint sh <镜像名>
# 如果是 entrypoint 脚本问题,可以先看看脚本内容
docker run --rm --entrypoint cat <镜像名> /docker-entrypoint.sh
第二个是把退出码 0 的容器"钉住"用来观察。对启动即退出的容器,可以临时改成前台跑一个不退出
# 临时让容器保持运行,进去手工执行原命令看报错
docker run -d --name debug --entrypoint sleep <镜像名> 3600
docker exec -it debug sh
注意 sleep 必须存在于镜像里,distroless 这类极简镜像没有 shell 也没有 sleep,这时只能在构建阶段排查。
四、用 docker events 和 inspect 还原现场
如果容器是被外部事件干掉的(比如被误删、被 OOM、被调度驱逐),docker events 能给出时间线。
# 实时观察容器事件
docker events --filter type=container
# 查看容器从创建到现在的关键时间点
docker inspect <容器名> --format 'Created={{.Created}} Started={{.State.StartedAt}} Finished={{.State.FinishedAt}}'
结合 docker inspect 的完整输出(网络、挂载、环境变量、重启策略、健康检查),基本能还原出"它经历了什么"。特别留意 RestartCount,如果这个数字一直在涨,说明容器在反复重启失败,要看 Error 字段和日志里的重复报错。
五、healthcheck 失败的排查
有一类问题更隐蔽:容器明明在运行,状态却是 unhealthy,或者 Compose 里依赖它的服务一直起不来。这是健康检查没通过。
# 看健康检查的最近几次结果
docker inspect <容器名> --format '{{json .State.Health}}' | python3 -m json.tool 2>/dev/null \
|| docker inspect <容器名> --format '{{.State.Health.Status}}'
# 手工在容器里执行健康检查命令,看返回码
docker exec <容器名> sh -c 'curl -fsS http://127.0.0.1:8080/health; echo $?'
健康检查失败常见的三个原因:检查命令在容器里不存在(比如镜像没装 curl,应改用 wget 或 nc);检查地址写错(用了 localhost 但服务监听在别的地址,或端口不对);start_period 太短,服务还没就绪检查就开始计时。逐条验证即可。
六、一套可复用的排查清单
把上面的内容压缩成一张清单,遇到容器起不来按顺序走:
| 步骤 | 命令 | 看什么 |
|---|---|---|
| 1. 看状态 | docker ps -a |
是没起来还是 Exited,退出码多少 |
| 2. 看日志 | docker logs <名> |
应用层报错信息 |
| 3. 看详情 | docker inspect <名> |
ExitCode、OOMKilled、Error、RestartCount |
| 4. 查端口 | ss -lntp |
端口是否被占 |
| 5. 查挂载 | ls -ld <路径> |
路径是否存在、权限是否够 |
| 6. 查入口 | docker inspect <镜像> |
ENTRYPOINT/CMD 是否正确 |
| 7. 查架构 | uname -m / inspect |
架构是否匹配 |
| 8. 查磁盘 | df -h /var/lib/docker |
空间是否充足 |
| 9. 进容器 | --entrypoint sh |
手工复现报错 |
| 10. 看事件 | docker events |
外部动作与时间线 |
绝大多数问题在前五步就能定位。养成"先看退出码再动手改"的习惯,能省下大量瞎试的时间。
常见问题
Q:容器退出码是 0 但服务用不了,算失败吗?
算。退出码 0 表示主进程正常结束,但容器生命周期绑在主进程上,主进程一退容器就停。要让常驻服务持续运行,主进程必须是前台阻塞的进程(如 nginx 前台模式、exec java -jar ...),不能是后台启动后立即返回。
Q:docker logs 什么都没有?
可能日志被应用写到了文件而不是标准输出,也可能容器没运行起来。容器里的日志应该输出到 stdout/stderr,Docker 才能收集;写到文件的要改成输出到终端,或者挂载出来单独看。
Q:怎么确认是不是 OOM 杀的?
docker inspect <名> --format '{{.State.OOMKilled}}' 返回 true 就是。退出码 137 配合 OOMKilled=true 基本可以定论。
Q:容器一直重启怎么办?
先看 docker inspect 的 RestartCount 和 Error,再把 restart 策略临时改成 no 防止无限重启掩盖日志,然后手工启动一次看报错。
Q:改了配置还是起不来?
确认改的是生效的那份配置(bind mount 的路径对不对、--env-file 有没有生效、Compose 有没有重新 up -d)。很多"改了没用"其实是容器仍在用旧配置运行。
总结
排查容器启动失败,先看 docker ps -a 的退出码和 docker inspect 的 State,明确是"没起来"还是"起来又退出",再顺着日志往下定位。
常见原因有七类:端口占用、挂载路径/权限、CMD/ENTRYPOINT 不存在、依赖未就绪、OOM 被杀(退出码 137 + OOMKilled)、镜像架构不符、磁盘满。按清单顺序逐条验证,多数问题五步内解决。
容器启动即退出、来不及 exec 时,用 --entrypoint sh 覆盖入口进容器,或用 sleep 临时钉住容器手工复现,这是最实用的两个调试技巧。
docker events 和 docker inspect 的 RestartCount、Error 字段能还原被外部事件影响的现场;健康检查失败要单独查检查命令、地址和 start_period。
最重要的习惯是:退出码 0 不等于成功(主进程退完容器就停),先看退出码再动手改配置,避免盲目试错。
企业QQ咨询




