欢迎光临
我们一直在努力

【Linux排障实战】Docker容器启动失败怎么查:端口、日志、权限与网络

封面

🔥个人主页:爱和冰阔乐 📚专栏传送门:《数据结构与算法》 、C++ 🐶学习方向:C++方向学习爱好者 ⭐人生格言:得知坦然 ,失之淡然

在这里插入图片描述


🏠博主简介 在这里插入图片描述

文章目录

  • 前言
  • 一、先别急着看日志,先看容器状态
    • 1.1 “启动失败”其实至少有四种状态
    • 1.2 退出码能告诉我们多少?
  • 二、建立一条固定的排障证据链
  • 三、日志:先抓住第一次失败
    • 3.1 最常用的 `docker logs`
    • 3.2 为什么有时 `docker logs` 是空的?
    • 3.3 入口脚本为什么经常让错误变得模糊?
  • 四、端口问题:区分容器端口和宿主机端口
    • 4.1 `-p 18080:8080` 到底是什么意思?
    • 4.2 宿主机端口已经被占用
    • 4.3 端口映射正确,为什么还是访问不到?
    • 4.4 不要随意把数据库端口发布到所有网卡
  • 五、挂载与权限:容器里是哪个用户?
    • 5.1 先看进程身份
    • 5.2 不要用 `chmod 777` 当最终方案
    • 5.3 文件存在,容器为什么说找不到?
    • 5.4 SELinux 场景
  • 六、环境变量与启动命令
    • 6.1 容器中到底拿到了哪些变量?
    • 6.2 Compose 变量替换和容器环境不是一回事
  • 七、网络问题:先确认“谁访问谁”
    • 7.1 宿主机访问容器
    • 7.2 一个容器访问另一个容器
    • 7.3 `depends_on` 不等于依赖服务已经就绪
  • 八、健康检查失败,但应用明明能访问
    • 8.1 HEALTHCHECK 在哪个环境执行?
    • 8.2 健康检查不要检查太多东西
    • 8.3 启动宽限时间
  • 九、最小化复现比反复改配置有效
    • 9.1 从最小命令开始
    • 9.2 覆盖入口命令进入镜像
    • 9.3 比较镜像默认配置和容器实际配置
  • 总结
  • 参考资料

前言

Docker 容器启动失败时,很多人的第一反应是:

docker restart 容器名

不行就删容器、重新构建,再不行就清缓存、重启 Docker。

偶尔确实能恢复,但问题也被一起藏起来了。下一次换一台机器、换一个端口、换一个挂载目录,同样的错误还会重新出现。

容器排障最麻烦的地方,不是命令太多,而是故障可能来自不同层:

  • 镜像本身没有问题,但启动命令写错了;
  • 主进程能运行,但没有权限读取挂载目录;
  • 容器显示 Running,但应用只监听 127.0.0.1;
  • 端口映射写对了,但宿主机端口已经被占用;
  • 服务已经启动,但健康检查依赖了镜像里不存在的 curl;
  • 应用可以访问 IP,却不能解析 Compose 服务名;
  • 日志只显示最后一次重启,真正的第一条错误已经被刷掉了。

Docker 排障文章封面

这篇文章不背命令表,而是搭一条排障证据链。遇到容器起不来时,先判断它停在哪一层,再使用对应命令验证。

文中示例是我为排障流程重新设计的实验场景,不引用其他文章内容。命令以 Linux/Docker 通用写法为主,涉及宿主机端口检查时同时给出 Windows PowerShell 用法。


一、先别急着看日志,先看容器状态

1.1 “启动失败”其实至少有四种状态

用户说“容器起不来”,可能指下面任何一种情况:

状态现象优先检查
Created 容器创建了,但主进程没真正启动 启动参数、运行时错误
Exited 主进程已经退出 退出码、日志、入口脚本
Restarting 主进程反复退出,被重启策略拉起 第一段日志、健康检查、依赖
Running 容器活着,但业务访问不到 监听地址、端口、网络、防火墙

所以第一条命令不是 docker logs,而是:

docker ps -a

只看目标容器:

docker ps -a –filter name=api-demo

进一步查看状态:

docker inspect api-demo \\
–format 'status={{.State.Status}} exit={{.State.ExitCode}} error={{.State.Error}}'

示例:

status=exited exit=1 error=

这个结果至少说明两件事:

  • Docker 已经成功创建并启动过容器;
  • 容器里的主进程自己以状态码 1 退出。
  • 这时继续研究宿主机防火墙没有意义,应先看进程为什么退出。

    1.2 退出码能告诉我们多少?

    常见退出码可以作为方向,但不能当作最终结论:

    退出码常见含义
    0 进程正常结束,但服务型容器不应该立刻结束
    1 应用通用错误
    126 命令存在,但不可执行
    127 找不到命令
    137 收到 SIGKILL,可能是 OOM 或人工强制停止
    139 段错误
    143 收到 SIGTERM,常见于正常停止流程

    查看完整状态:

    docker inspect api-demo –format '{{json .State}}'

    如果有 jq:

    docker inspect api-demo –format '{{json .State}}' | jq

    重点字段包括:

    Status
    Running
    ExitCode
    Error
    StartedAt
    FinishedAt
    OOMKilled
    Health

    ExitCode=137 时不要直接认定为 OOM,应继续看:

    docker inspect api-demo –format 'oom={{.State.OOMKilled}}'

    如果 OOMKilled=false,也可能是管理员执行了 docker kill,或者外部编排系统强制终止。

    状态码负责缩小范围,不负责替我们下结论。


    二、建立一条固定的排障证据链

    Docker 故障证据链

    我习惯按照下面顺序排查:

    状态
    → 退出码和错误
    → 应用日志
    → 启动配置
    → 文件与权限
    → 监听地址和端口
    → 容器网络与依赖
    → 健康检查
    → 最小化复现

    这条顺序不是绝对的,但能避免常见的跳跃:

    网页打不开
    → 怀疑 Docker 网络
    → 重建 network
    → 清理镜像
    → 最后发现应用根本没有启动

    排障时每一步都要留下证据。

    例如,不要只说“端口应该没问题”,而要给出:

    docker port api-demo
    ss -lntp
    curl -v http://127.0.0.1:18080/health

    命令的结果能互相验证,才算把这一层排除。


    三、日志:先抓住第一次失败

    3.1 最常用的 docker logs

    docker logs api-demo

    容器重启很多次时,日志可能很长。先看末尾:

    docker logs –tail 100 api-demo

    持续跟踪:

    docker logs -f –tail 50 api-demo

    带时间戳:

    docker logs -t –tail 100 api-demo

    只看最近十分钟:

    docker logs –since 10m api-demo

    如果容器处于 Restarting,我更建议先关闭自动重启干扰:

    docker update –restart=no api-demo
    docker stop api-demo
    docker start -a api-demo

    docker start -a 会把当前启动过程直接附加到终端,第一条错误更容易看到。

    Docker 排障终端输出

    这一组输出应该连着看:ps -a 确认容器状态,inspect 给出退出码和 OOM 标记,logs 保留应用报错。单独看到一个 Exited (1),信息还不够。

    调完后再恢复策略:

    docker update –restart=unless-stopped api-demo

    3.2 为什么有时 docker logs 是空的?

    docker logs 只能读取容器主进程写到标准输出和标准错误的内容。

    如果应用把日志写进:

    /var/log/app/app.log

    而没有输出到 stdout/stderr,Docker 日志自然为空。

    检查日志驱动:

    docker inspect api-demo –format '{{.HostConfig.LogConfig.Type}}'

    常见结果:

    json-file
    local
    journald
    none

    如果是 none,docker logs 不会返回应用日志。

    对容器应用更推荐把普通运行日志写到 stdout/stderr,让日志收集系统统一处理。把日志只写在容器文件系统中,容器删除后很容易一起丢失。

    3.3 入口脚本为什么经常让错误变得模糊?

    例如 Dockerfile:

    COPY docker-entrypoint.sh /usr/local/bin/
    ENTRYPOINT ["docker-entrypoint.sh"]
    CMD ["node", "server.js"]

    脚本:

    #!/bin/sh
    set -e

    echo "preparing configuration"
    ./prepare-config.sh

    $@

    这里的 $@ 没有使用 exec。主应用变成 shell 的子进程,信号转发和退出状态处理可能出现问题。

    更好的写法:

    #!/bin/sh
    set -eu

    echo "preparing configuration" >&2
    ./prepare-config.sh

    exec "$@"

    exec 会让目标程序替换 shell,成为容器中的主进程。

    另外还要检查:

    file docker-entrypoint.sh

    如果脚本在 Windows 下编辑,可能带 CRLF,容器中出现:

    /bin/sh^M: bad interpreter

    转换方式:

    sed -i 's/\\r$//' docker-entrypoint.sh
    chmod +x docker-entrypoint.sh


    四、端口问题:区分容器端口和宿主机端口

    4.1 -p 18080:8080 到底是什么意思?

    docker run -p 18080:8080 demo-api:1.0

    含义是:

    宿主机 18080 → 容器 8080

    左边是宿主机端口,右边是容器内部应用监听的端口。

    最常见的错误是应用实际监听 3000,却映射到 8080:

    docker run -p 18080:8080 demo-api:1.0

    容器可以正常 Running,但访问 18080 没有响应。

    检查镜像配置:

    docker image inspect demo-api:1.0 \\
    –format '{{json .Config.ExposedPorts}}'

    检查容器端口绑定:

    docker port api-demo

    检查实际配置:

    docker inspect api-demo \\
    –format '{{json .NetworkSettings.Ports}}'

    注意:Dockerfile 中的 EXPOSE 8080 只是元数据和说明,不会自动把端口发布到宿主机。

    4.2 宿主机端口已经被占用

    Linux:

    ss -lntp | grep ':8080'

    或者:

    lsof -iTCP:8080 -sTCP:LISTEN

    Windows PowerShell:

    Get-NetTCPConnection LocalPort 8080 State Listen

    继续查进程:

    Get-Process Id 18724

    如果端口被占用,不要第一反应杀进程。先判断那个进程是不是正常服务。

    最安全的验证方式是换一个宿主机端口:

    docker run –rm -p 18080:8080 demo-api:1.0

    Docker 端口冲突示意

    端口冲突时不要先改一串配置。先证明 8080 被谁监听,再换 18080 验证镜像内部服务。两步能把“宿主机端口问题”和“容器内服务问题”拆开。

    4.3 端口映射正确,为什么还是访问不到?

    检查应用在容器中监听的地址。

    错误示例:

    server.listen(8080, "127.0.0.1");

    这只监听容器自己的回环地址。端口发布流量进入容器后,不一定能到达这个监听。

    容器服务一般应监听:

    server.listen(8080, "0.0.0.0");

    Docker 容器监听地址

    127.0.0.1 和 0.0.0.0 的区别发生在容器内部。前者只接收容器回环流量,后者才会接收进入容器网卡的连接。宿主机端口映射写对了,并不能替应用修改监听地址。

    进入容器检查:

    docker exec -it api-demo sh

    如果镜像中有 ss:

    ss -lntp

    或者直接从容器内部请求:

    wget -qO- http://127.0.0.1:8080/health

    判断方式:

    结果方向
    容器内也访问失败 应用未监听或应用异常
    容器内成功,宿主机失败 端口发布、防火墙或监听地址
    宿主机成功,外部机器失败 宿主机防火墙、云安全组、绑定地址

    4.4 不要随意把数据库端口发布到所有网卡

    docker run -p 5432:5432 postgres

    默认会把端口发布到宿主机地址上,具体暴露范围还受 Docker 和防火墙配置影响。

    如果只供本机访问,可以明确绑定:

    docker run -p 127.0.0.1:5432:5432 postgres

    如果只供同一 Docker 网络中的容器访问,甚至不需要 -p。服务之间直接通过容器网络通信。

    能访问不等于应该暴露。排障时临时打开的端口,解决后也要恢复最小暴露。


    五、挂载与权限:容器里是哪个用户?

    5.1 先看进程身份

    docker exec api-demo id

    输出可能是:

    uid=10001(app) gid=10001(app) groups=10001(app)

    再看挂载:

    docker inspect api-demo \\
    –format '{{range .Mounts}}{{println .Source "->" .Destination .Mode}}{{end}}'

    假设宿主机目录:

    /srv/api-data

    权限:

    ls -ld /srv/api-data

    输出:

    drwx—— 2 root root 4096 Jun 11 10:20 /srv/api-data

    容器内的 UID 10001 无法写入这个目录,应用就可能报:

    EACCES: permission denied

    Docker UID 与挂载权限

    Linux 判断权限时看的是 UID/GID,不是用户名长得是否一样。容器里的 app 和宿主机上的 app 即使同名,只要数字 ID 不同,对挂载目录的权限就可能完全不同。

    5.2 不要用 chmod 777 当最终方案

    chmod -R 777 能快速判断“是不是权限问题”,但不适合作为长期配置。

    更合理的处理方式包括:

    sudo chown -R 10001:10001 /srv/api-data
    sudo chmod -R u+rwX,go-rwx /srv/api-data

    或者让容器使用与宿主机文件一致的 UID/GID。

    Compose:

    services:
    api:
    image: demoapi:1.0
    user: "1000:1000"
    volumes:
    ./data:/app/data

    但这里也不能机械照抄。镜像内部如果依赖特定用户、HOME 目录或文件所有权,强制切换 UID 可能引入新问题。

    正确顺序是:

  • 确认容器进程 UID/GID;
  • 确认挂载源目录所有者和权限;
  • 确认应用需要读、写还是执行;
  • 只授予实际需要的权限。
  • 5.3 文件存在,容器为什么说找不到?

    检查挂载的真实结果:

    docker inspect api-demo –format '{{json .Mounts}}'

    常见问题:

    • 相对路径相对于当前执行目录,而不是 Compose 文件所在目录;
    • 宿主机文件不存在,Docker 创建了同名目录;
    • 把文件挂载到应用期望的目录上,覆盖了镜像原内容;
    • Windows 路径没有共享给 Docker Desktop;
    • SELinux 阻止容器访问。

    例如:

    docker run -v ./config.json:/app/config.json demo-api:1.0

    如果 ./config.json 不存在,某些情况下宿主机会创建目录,容器中 /app/config.json 也变成目录。

    启动前先检查:

    test -f ./config.json && echo ok

    Compose 可以使用长语法,让意图更清晰:

    volumes:
    type: bind
    source: ./config.json
    target: /app/config.json
    read_only: true

    5.4 SELinux 场景

    在启用 SELinux 的 Linux 发行版上,即使 Unix 权限看起来正确,也可能被安全上下文阻止。

    可以检查:

    getenforce
    ls -Z /srv/api-data

    Docker 绑定挂载可使用合适的标签选项,例如 :Z 或 :z,但两者共享语义不同,不能盲目添加。

    不要为了验证直接永久关闭 SELinux。可以先查审计日志,确认是否真的由策略拒绝。


    六、环境变量与启动命令

    6.1 容器中到底拿到了哪些变量?

    docker inspect api-demo \\
    –format '{{range .Config.Env}}{{println .}}{{end}}'

    或者进入容器:

    docker exec api-demo env | sort

    注意不要在公开日志中直接打印密码、Token 和数据库连接串。

    更安全的做法是只检查变量是否存在:

    test -n "${DATABASE_URL:-}" \\
    && echo "DATABASE_URL is set" \\
    || echo "DATABASE_URL is missing"

    6.2 Compose 变量替换和容器环境不是一回事

    Compose 文件:

    services:
    api:
    image: demoapi:${APP_VERSION}
    environment:
    APP_ENV: ${APP_ENV:production}

    这里至少有两次处理:

  • Compose 在宿主机读取 .env 或当前环境,替换 ${…};
  • 替换后的 APP_ENV 才进入容器。
  • 查看最终配置:

    docker compose config

    这是排查 Compose 变量最有用的命令之一。

    它能让我们看到:

    • 变量替换后的镜像名;
    • 最终端口;
    • 最终挂载路径;
    • 合并后的多个 Compose 文件;
    • 网络和环境配置。

    如果 docker compose up 的行为和想象不同,先保存:

    docker compose config > resolved-compose.yml

    不要只盯着原始 YAML 猜。

    七、网络问题:先确认“谁访问谁”

    7.1 宿主机访问容器

    路径:

    浏览器/宿主机进程
    → 宿主机发布端口
    → Docker 转发
    → 容器监听端口

    检查命令:

    docker port api-demo
    curl -v http://127.0.0.1:18080/health
    docker exec api-demo wget -qO- http://127.0.0.1:8080/health

    这三条分别验证端口配置、宿主机访问和容器内应用。

    7.2 一个容器访问另一个容器

    Compose:

    services:
    api:
    image: demoapi:1.0
    environment:
    DATABASE_HOST: db
    db:
    image: postgres:17

    api 应该访问:

    db:5432

    而不是:

    localhost:5432

    容器中的 localhost 指容器自己,不是宿主机,也不是另一个容器。

    Docker 容器间服务访问

    这类错误常出现在把本地开发配置原样搬进 Compose 时。API 容器访问数据库,要使用 Compose 服务名 db;写成 localhost,请求只会回到 API 容器自己。

    检查网络:

    docker network ls
    docker inspect api-demo –format '{{json .NetworkSettings.Networks}}'
    docker inspect db –format '{{json .NetworkSettings.Networks}}'

    两个容器要通过服务名通信,必须处在可互通的用户定义网络中。

    测试 DNS:

    docker exec api-demo getent hosts db

    测试端口:

    docker exec api-demo sh -c 'nc -vz db 5432'

    但精简镜像中可能没有 getent、nc、curl。不要为了排障把一堆工具永久装进生产镜像,可以临时启动调试容器进入同一个网络:

    docker run –rm -it \\
    –network demo_default \\
    nicolaka/netshoot

    如果环境不允许使用额外镜像,也可以使用已有基础镜像做最小测试。

    7.3 depends_on 不等于依赖服务已经就绪

    Compose 中:

    depends_on:
    db

    主要表达启动顺序,不代表数据库已经完成初始化并可以接受连接。

    更稳妥的处理:

    • 为数据库配置健康检查;
    • 应用连接失败时做有限退避重试;
    • 把“容器启动”与“服务就绪”分开;
    • 不要用固定 sleep 30 当长期方案。

    固定等待时间在自己电脑上可能够,换到慢磁盘或 CI 环境就不够;设置太长又会浪费每次启动时间。


    八、健康检查失败,但应用明明能访问

    8.1 HEALTHCHECK 在哪个环境执行?

    健康检查命令在容器内部执行。

    Dockerfile:

    HEALTHCHECK –interval=10s –timeout=3s –retries=3 \\
    CMD curl -f http://localhost:8080/health || exit 1

    如果镜像里没有 curl,每次检查都会返回 127。

    Docker 健康检查执行环境

    健康检查不是从宿主机替你访问服务,而是在容器里执行命令。因此命令是否存在、工作目录、环境变量和容器内端口都要成立。浏览器能打开页面,不代表这条检查命令一定能运行。

    查看结果:

    docker inspect api-demo \\
    –format '{{json .State.Health}}'

    手动执行同一条命令:

    docker exec api-demo sh -c \\
    'curl -f http://localhost:8080/health'

    如果提示 curl: not found,问题不在应用。

    可以改成镜像已有的工具,或者在应用中提供轻量自检命令:

    HEALTHCHECK CMD ["node", "healthcheck.js"]

    8.2 健康检查不要检查太多东西

    一个 /health 接口同时检查:

    • 数据库;
    • Redis;
    • 第三方支付;
    • 邮件服务;
    • 对象存储;

    只要任何一个外部服务短暂抖动,容器就变成 unhealthy,甚至被编排系统重启。

    建议区分:

    检查目的
    liveness 进程是否还能工作,失败时可考虑重启
    readiness 当前是否适合接收流量
    startup 启动是否完成,给慢启动程序宽限时间

    Docker 原生 HEALTHCHECK 只有一套状态,复杂系统要在应用和编排层继续区分。

    8.3 启动宽限时间

    应用初始化需要 40 秒,但健康检查从第 5 秒开始,每 3 次失败就 unhealthy。

    可以设置:

    HEALTHCHECK \\
    –interval=10s \\
    –timeout=3s \\
    –start-period=45s \\
    –retries=3 \\
    CMD ["node", "healthcheck.js"]

    start-period 不是越长越好,而是要覆盖正常启动的合理上限。


    九、最小化复现比反复改配置有效

    9.1 从最小命令开始

    复杂 Compose 启动失败时,可以先绕开编排文件:

    docker run –rm demo-api:1.0

    再逐项加入:

    docker run –rm \\
    -e APP_ENV=production \\
    demo-api:1.0

    然后加入端口:

    docker run –rm \\
    -e APP_ENV=production \\
    -p 18080:8080 \\
    demo-api:1.0

    最后加入挂载和网络。

    当某一步开始失败,新增的那一组配置就是优先检查对象。

    9.2 覆盖入口命令进入镜像

    容器一启动就退出,无法 docker exec,可以覆盖入口:

    docker run –rm -it \\
    –entrypoint sh \\
    demo-api:1.0

    进入后检查:

    pwd
    ls -la
    id
    env | sort
    node –version
    node server.js

    这能区分:

    • 文件没复制进去;
    • 工作目录不对;
    • 运行用户无权限;
    • 依赖缺失;
    • 应用本身启动失败。

    9.3 比较镜像默认配置和容器实际配置

    镜像:

    docker image inspect demo-api:1.0 \\
    –format 'user={{.Config.User}} workdir={{.Config.WorkingDir}} entry={{json .Config.Entrypoint}} cmd={{json .Config.Cmd}}'

    容器:

    docker inspect api-demo \\
    –format 'user={{.Config.User}} workdir={{.Config.WorkingDir}} entry={{json .Config.Entrypoint}} cmd={{json .Config.Cmd}}'

    如果两者不同,说明运行参数或 Compose 覆盖了镜像设置。


    把前面的检查串起来,最终应该得到一组能互相印证的输出:容器状态说明它停在哪,inspect 给出退出码,日志保留第一条可执行的错误。

    总结

    Docker 排障不是记住越多命令越好,而是每次都知道当前在验证哪一层。

    排查的顺序其实很简单:先看状态(Exited / Restarting / Running / unhealthy),再看退出码和第一段日志,然后检查启动命令、环境变量和 Compose 解析结果。权限问题用 UID/GID 和真实挂载信息来判断。端口要分清三个层:宿主机端口、容器端口、应用监听地址。容器间访问和宿主机访问容器也要分开看。复杂配置失败就做最小化复现,修好以后别只记“重启好了”,把验证命令也留下来。

  • 先看状态,确认是 Exited、Restarting、Running 还是 unhealthy;
  • 再看退出码和第一段日志;
  • 检查最终启动命令、环境变量和 Compose 解析结果;
  • 用 UID/GID 和真实挂载信息判断权限;
  • 分清宿主机端口、容器端口与应用监听地址;
  • 分清宿主机访问容器和容器访问容器;
  • 手动执行健康检查命令;
  • 复杂配置失败时,做最小化复现;
  • 修好以后保留验证命令,不要只留下“重启好了”。
  • 容器显示 Running 只证明主进程没挂,业务能不能用还得看端口、依赖和健康检查。排查的目标不是“这次起来了”,是找到能复现、能解释、能验证的根因。


    参考资料

    • Docker Docs:查看容器日志 https://docs.docker.com/reference/cli/docker/container/logs/
    • Docker Docs:发布和暴露端口 https://docs.docker.com/get-started/docker-concepts/running-containers/publishing-ports/
    • Docker Docs:Port publishing and mapping https://docs.docker.com/engine/network/port-publishing/
    • Docker Docs:Dockerfile HEALTHCHECK https://docs.docker.com/reference/dockerfile/#healthcheck
    • Docker Docs:Bind mounts https://docs.docker.com/engine/storage/bind-mounts/
    • Docker Docs:Compose 配置解析 https://docs.docker.com/reference/cli/docker/compose/config/
    赞(0)
    未经允许不得转载:171主机测评 » 【Linux排障实战】Docker容器启动失败怎么查:端口、日志、权限与网络
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址