Docker容器无法启动怎么办?常见错误原因与排查方法

容器起不来,是运维日常里最频繁的求助之一。表现可能有很多种: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 不等于成功(主进程退完容器就停),先看退出码再动手改配置,避免盲目试错。