ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Docker容器启动即退出的完整排查指南

Docker容器启动即退出的完整排查指南 1. 这不是“没启动”而是“启动即退出”——先破除一个普遍误解很多人看到标题第一反应是“docker ps 不显示容器那肯定没启动成功啊。”但实际在绝大多数真实场景里容器确实启动了而且执行了只是瞬间就退出了。docker ps默认只显示正在运行的容器即状态为Up的而docker ps -a才会列出所有容器——包括已退出、已停止、创建失败的。所以当你执行docker run hello-world或docker run nginx后立刻敲docker ps却一片空白根本不是 Docker 没工作而是你没看清它的“生死时速”逻辑。这背后是 Docker 容器生命周期的本质容器不是“服务进程”而是“主进程的生命周期镜像”。只要容器内 PID 1 进程结束整个容器就立即终止。比如你运行docker run ubuntu:22.04 echo hello容器会打印 hello 然后退出——它完成了任务干净收工。这不是 bug是设计哲学。可很多新手误以为“启动后台常驻”于是反复docker run却始终看不到docker ps里的条目陷入“我到底有没有跑起来”的自我怀疑。更隐蔽的问题在于某些镜像默认不带前台守护进程。比如官方python:3.11-slim镜像docker run python:3.11-slim会直接退出因为它没有指定任何命令shell 启动后无事可做立即 exit而nginx镜像之所以能常驻是因为它的 ENTRYPOINT 是/docker-entrypoint.sh最终调用nginx -g daemon off;——这个-g daemon off;强制 Nginx 以前台模式运行PID 1 就是 nginx 主进程不退出容器就一直 Up。提示docker ps是“活着的快照”不是“历史记录仪”。想查容器是否真被创建过、为什么挂了、退出码是多少必须用docker ps -a配合docker logs和docker inspect。这是所有 Docker 故障排查的第一步铁律绕不开也省不得。我刚带团队时新人平均每天要问三次“我的容器怎么不显示”——结果八成是忘了加-a两成是镜像本身没设计前台运行逻辑。后来我们把alias dpsdocker ps -a写进所有开发机的.bashrc并配上一句注释“ps 不带 -a等于没查”。这句话现在还贴在我们 DevOps 墙上。你手头那个“启动了但不显示”的容器大概率正安静地躺在docker ps -a的列表末尾状态写着Exited (1) 2 minutes ago。别急着重装 Docker先去把它捞出来——这才是真正解决问题的起点。2. 从docker ps -a开始三步定位容器“猝死”真相一旦确认docker ps为空立刻执行docker ps -a。你会看到类似这样的输出$ docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES a1b2c3d4e5f6 nginx:alpine /docker-entrypoint.… 3 minutes ago Exited (1) 3 minutes ago mystifying_bell 7890abcd1234 redis:7 docker-entrypoint.s… 5 minutes ago Up 5 minutes 6379/tcp festive_mahavira注意看第三列STATUSUp 5 minutes是健康状态Exited (1) 3 minutes ago就是“猝死现场”。括号里的数字(1)是退出码exit code它是诊断容器为何挂掉的最直接线索。Docker 规定退出码0表示成功退出非零值如1,127,137代表某种失败类型。下面这张表是我整理了三年线上故障后总结出的高频退出码对照清单比官方文档更贴近实战退出码常见原因典型场景快速验证方法1应用程序内部错误、配置文件缺失、依赖未安装Python 脚本 import 失败Nginx 配置语法错误Java 应用找不到application.ymldocker logs 容器ID查看最后一行报错127命令未找到command not founddocker run ubuntu:22.04 node app.js但镜像没装 NodeCMD [mysqld]但镜像里只有mysql-clientdocker exec -it 容器ID /bin/sh进入后手动执行 CMD 中的命令137被 OOM Killer 杀死内存超限启动 Spring Boot 应用但未限制内存运行大数据处理脚本时内存爆满docker stats 容器ID观察内存峰值dmesg -T | grep -i killed process查系统日志143收到 SIGTERM 信号后优雅退出docker stop执行后容器正常关闭K8s Pod 被驱逐此属正常行为非故障重点看是否被外部强制终止255Docker daemon 无法启动容器权限/路径问题挂载宿主机目录时权限不足如/var/run/docker.sockSELinux 强制策略拦截docker info检查 daemon 状态journalctl -u docker -n 50查 daemon 日志举个真实案例上周一位同事部署一个 Flask APIdocker run -p 5000:5000 my-flask-app后docker ps -a显示Exited (1)。他第一反应是代码错了花两小时 debug。我让他docker logs a1b2c3d4e5f6输出第一行就是Traceback (most recent call last): File app.py, line 3, in module from flask import Flask ModuleNotFoundError: No module named flask原来他构建镜像时requirements.txt没正确 COPY或者pip install -r requirements.txt步骤被误删。退出码1直接指向 Python 解释器层面的异常而不是网络或端口问题。如果他跳过docker ps -a和docker logs就会在错误的方向上狂奔。注意docker logs默认只显示容器 stdout/stderr 的缓存日志。如果容器启动极快就退出比如几毫秒日志可能来不及刷入缓冲区。此时必须加-t参数docker logs -t 容器ID查看时间戳或用--details查看更底层信息。另外某些镜像如 Alpine 版本默认日志驱动不同需确认docker info \| grep Logging Driver是否为json-file。3.docker inspect解剖容器的“尸体”获取被忽略的127个关键字段当docker ps -a和docker logs还不能定位根因时docker inspect就是终极解剖刀。它返回一个 JSON 对象包含容器从创建到死亡全过程的全部元数据——共 127 个字段以 Docker 24.0.7 为准。其中 90% 的字段日常用不到但有 7 个字段几乎每次排查都必查。我把它们按优先级排序并附上每个字段在什么情况下会“说谎”或“沉默”。3.1State.Status与State.ExitCode状态的双重校验State.Status是容器当前状态created,running,exited,pausedState.ExitCode是退出码。这两个字段必须交叉验证。曾遇到一个诡异 caseState.Status显示running但State.ExitCode是0且docker ps看不到它。一查发现是容器被docker pause过pause状态下docker ps默认不显示需加-a而inspect里State.Status仍为running但State.Paused字段为true。所以永远不要只信Status要看Paused、Dead、Restarting等辅助状态字段。3.2HostConfig.Memory与HostConfig.MemoryReservation内存限制的隐形杀手如果你设置了-m 512m或--memory512mHostConfig.Memory会显示536870912字节。但很多人忽略HostConfig.MemoryReservation——它定义软限制当系统内存紧张时Docker 会优先 kill 掉超过此值的容器。某次生产事故中一个 Redis 容器ExitCode是137Memory设为1g但MemoryReservation是0即未设。运维同学以为够用结果宿主机跑满其他进程Redis 因超软限被 OOM Kill。硬限制Memory防爆软限制MemoryReservation防挤兑——这是血泪教训。3.3NetworkSettings.Networks.network-name.IPAddress网络层的“幽灵地址”当容器启动后无法访问如curl http://localhost:8080失败很多人只查端口映射。但NetworkSettings里藏着真相。比如docker run -p 8080:80 nginxNetworkSettings.Ports显示80/tcp: [{HostIp: 0.0.0.0, HostPort: 8080}]看似正常。但如果NetworkSettings.Networks.bridge.IPAddress是空字符串或null说明容器根本没获得 IP 地址——常见于 Docker daemon 网络插件故障、/var/lib/docker/network目录损坏、或宿主机 iptables 规则被清空。此时docker network inspect bridge会显示Containers数组为空或IPAM.Config缺失。3.4Mounts挂载点的“权限陷阱”Mounts数组列出所有 volume/bind mount。重点看RW读写、Propagation传播模式和Driver驱动类型。曾有个 Java 应用挂载/app/logs到宿主机docker ps -a显示Exited (1)logs里只有一句Permission denied。inspect发现Mounts中该路径RW为false但启动命令明明写了-v /host/logs:/app/logs:rw。一查是宿主机/host/logs目录权限为755而容器内应用用户 UID 是1001对755目录只有读执行权无写权。解决方案不是改容器用户而是chmod 775 /host/logs或chown 1001:1001 /host/logs。Docker 不帮你解决 Linux 权限它只忠实地执行你的挂载指令。3.5Config.Cmd与Config.Entrypoint命令链的“断点定位”Config.Cmd是用户传入的命令如docker run ubuntu echo hello中的echo helloConfig.Entrypoint是镜像定义的入口如nginx镜像的/docker-entrypoint.sh。两者组合逻辑是Entrypoint Cmd。如果Cmd为空就只执行Entrypoint如果Entrypoint为空就只执行Cmd。某次排查一个自定义镜像docker run my-image退出码127inspect显示Config.Entrypoint是[/start.sh]但Config.Cmd是null。进入容器ls /start.sh发现文件不存在——原来构建时COPY start.sh /漏了chmod x导致start.sh不可执行exec失败。inspect的Config段让你一眼看清“谁该干啥”避免在Dockerfile里反复猜。实操技巧docker inspect输出太长别用| less浪费时间。直接用docker inspect --format{{.State.Status}} {{.State.ExitCode}} 容器ID提取关键字段查网络用docker inspect --format{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}} 容器ID查挂载用docker inspect --format{{range .Mounts}}{{.Source}} - {{.Destination}} ({{.RW}}){{\n}}{{end}} 容器ID。这些--format表达式我存在~/.docker/inspect-templates里随用随调。4. 构建与运行阶段的“静默陷阱”那些不会报错却让容器秒退的坑很多容器“启动即退”并非运行时错误而是构建或启动参数埋下的雷。它们不抛异常不打日志甚至docker build都显示Successfully built但一运行就凉。这类问题最难排查因为表面一切正常。以下是我在 CI/CD 流水线中踩过的五个典型静默陷阱每个都附带复现步骤和防御方案。4.1HEALTHCHECK指令的“反向自杀”HEALTHCHECK本意是健康检查但若配置不当会成为容器的“定时炸弹”。例如FROM nginx:alpine HEALTHCHECK --interval5s --timeout3s --start-period30s --retries3 \ CMD curl -f http://localhost/health || exit 1这段代码看似合理但问题在于curl命令依赖curl工具而nginx:alpine镜像默认不包含 curldocker build不会报错因为HEALTHCHECK是元数据不参与构建过程。容器启动后Docker daemon 每 5 秒执行一次curl失败三次后标记容器为unhealthy然后——某些 orchestrator如 Docker Swarm会自动重启它但更常见的是容器本身仍在运行只是状态变灰。然而如果用户误以为HEALTHCHECK失败会导致容器退出就会困惑“为什么docker ps有它但服务不可用”防御方案在HEALTHCHECK前确保基础镜像包含所需工具或RUN apk add --no-cache curl用CMD wget --spider -q http://localhost/health || exit 1wget在 Alpine 中默认存在最佳实践HEALTHCHECK只用于探测绝不影响容器主进程生命周期。4.2USER指令引发的“权限雪崩”USER指令切换容器内运行用户但若后续COPY或RUN没适配就会埋雷。例如FROM python:3.11-slim RUN useradd -u 1001 -m appuser USER appuser COPY requirements.txt . RUN pip install -r requirements.txt # ❌ 这里会失败因为 appuser 对 /tmp 无写权 CMD [python, app.py]pip install默认使用/tmp作为临时目录而appuser对/tmp只有读权限。docker build时RUN指令以 root 执行所以pip install成功但USER appuser后CMD以appuser启动app.py导入包时可能因缓存路径权限问题崩溃退出码1。docker logs只显示ImportError根本看不出是/tmp权限导致。防御方案USER后所有RUN指令必须显式指定用户权限或改用--user参数RUN --user root pip install -r requirements.txt更安全做法USER放在Dockerfile最后且COPY和RUN全部在USER前完成永远用docker run --user 1001:1001测试模拟最终运行环境。4.3WORKDIR的“路径幻觉”WORKDIR /app设置工作目录但若CMD中的路径是相对的就会出错。例如FROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --onlyproduction COPY . . CMD [npm, start] # ✅ 正确npm 在 /app 下找 package.json # CMD [node, server.js] # ❌ 错误server.js 在 /app 下但 node 会去 /usr/local/bin 找看起来没问题但npm start依赖package.json中的scripts.start字段。如果该字段是node server.js而server.js文件在/app下一切正常。但如果package.json被误写成node ./src/server.js而src目录没被COPYnpm start就会报Cannot find module ./src/server.js退出码1。docker logs只显示这一行你得翻package.json才知道问题在哪。防御方案CMD用绝对路径CMD [node, /app/server.js]构建时加验证步骤RUN ls -l /app npm ls确保文件存在且依赖完整CI 流水线中docker run --rm 镜像 sh -c ls -l /app npm start作为冒烟测试。4.4ARG与ENV的“变量幽灵”ARG是构建参数ENV是环境变量两者作用域不同。常见错误FROM openjdk:17-jdk-slim ARG SPRING_PROFILES_ACTIVEprod ENV SPRING_PROFILES_ACTIVE$SPRING_PROFILES_ACTIVE COPY target/app.jar /app.jar CMD [java, -jar, /app.jar]ARG只在docker build时有效ENV将其值固化到镜像中。但若docker run时覆盖SPRING_PROFILES_ACTIVE比如docker run -e SPRING_PROFILES_ACTIVEdev新值会生效。问题在于某些 Spring Boot 应用要求SPRING_PROFILES_ACTIVE在 JVM 启动参数中指定而非环境变量。java -jar不读取环境变量所以ENV设置无效应用默认加载defaultprofile连接错数据库启动失败。防御方案查文档确认框架如何读取 profileSpring Boot 2.4 推荐用--spring.profiles.activedevCMD改为CMD [sh, -c, java -Dspring.profiles.active$SPRING_PROFILES_ACTIVE -jar /app.jar]或者ARG直接注入 JVM 参数ARG JAVA_OPTS-Dspring.profiles.activeprodCMD [sh, -c, java $JAVA_OPTS -jar /app.jar]。4.5EXPOSE的“端口幻觉”EXPOSE 8080只是声明不开启端口。但很多人以为写了EXPOSEdocker run -p 8080:8080就一定能通。实际上EXPOSE对容器网络无实质影响它只是给docker run --publish-all即-P提供参考。真正的端口绑定靠-p。陷阱在于如果应用监听的是127.0.0.1:8080而非0.0.0.0:8080即使-p映射了外部也连不上。docker ps显示端口映射正常但curl localhost:8080返回Connection refused。防御方案应用代码中监听0.0.0.0:$PORT而非127.0.0.1:$PORTdocker run后用docker exec -it 容器ID netstat -tuln \| grep :8080确认监听地址在Dockerfile中加健康检查HEALTHCHECK CMD curl -f http://localhost:8080/actuator/health确保端口真通。5. 环境与权限的“底层暗礁”Docker daemon、SELinux 与 cgroups 的协同故障当以上所有排查都指向“容器没错命令没错配置没错”问题往往下沉到操作系统层。这些故障不常发生但一旦出现会让docker ps彻底失灵——不是容器不显示而是 Docker daemon 根本无法管理容器。我经历过三次此类事故每次都花了 4-8 小时才定位这里把核心线索和验证路径摊开讲。5.1 Docker daemon 的“半死状态”systemctl与journalctl的黄金组合docker ps报错Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?是经典提示但有时 daemon 进程在只是“半死”。验证步骤sudo systemctl is-active docker返回active表示服务在运行若inactive或failedsudo systemctl start dockersudo systemctl status docker看Active:行后的状态以及Main PID是否有值sudo journalctl -u docker -n 50 --no-pager这是最关键的日志。重点关注levelerror或levelfatal的行failed to start daemon、graphdriver相关错误如overlay2不支持failed to start containerdcontainerd 是 Docker 的底层运行时permission deniedon/var/run/docker.sock。曾有一次journalctl显示time2024-05-20T10:23:45.123456789Z levelerror msgfailed to start daemon: error initializing graphdriver: driver not supported原因是宿主机内核版本5.4.0太旧不支持overlay2的某些特性。解决方案升级内核或在/etc/docker/daemon.json中强制指定storage-driver:vfs性能差但兼容性好。5.2 SELinux 的“无声拦截”setenforce 0不是万能解药在 CentOS/RHEL 系统上SELinux 是 Docker 的隐形守门人。它可能允许容器创建但阻止其访问挂载卷、网络或套接字。现象是docker run返回Error response from daemon: ... permission denied但docker ps -a里容器状态是Created而非Exited。docker logs为空docker inspect的State.Status是created。验证方法sestatus确认 SELinux 是否启用enabledsudo ausearch -m avc -ts recent | audit2why查看最近的 SELinux 拒绝日志sudo setenforce 0临时关闭 SELinux再试docker run。如果成功证明是 SELinux 问题。但setenforce 0是治标不治本。正确做法为 Docker 添加 SELinux 策略sudo semanage fcontext -a -t container_file_t /path/to/volume(/.*)?然后sudo restorecon -R /path/to/volume或者在docker run时加--security-opt labeldisable不推荐生产环境最佳实践使用:z或:Z标签挂载卷如docker run -v /host/data:/container/data:z让 SELinux 自动打标签。5.3 cgroups v1/v2 的“世代冲突”Docker 20.10 默认要求 cgroups v2但某些旧系统如 Ubuntu 18.04默认是 v1。docker info会显示Cgroup Version: 1或2。如果 daemon 配置与内核不匹配容器可能创建失败。验证cat /proc/sys/kernel/cgroup_version返回1或2docker info \| grep Cgroup看输出是否一致若不一致修改 GRUBsudo nano /etc/default/grub添加GRUB_CMDLINE_LINUXsystemd.unified_cgroup_hierarchy1然后sudo update-grub sudo reboot。5.4/var/run/docker.sock的“权限迷宫”docker.sock是 Docker daemon 的 Unix socket客户端通过它通信。权限错误会导致docker ps失败。标准权限是srw-rw---- 1 root docker且用户必须在docker组。验证ls -l /var/run/docker.sock确认 owner 是root:docker权限是srw-rw----id -nG确认当前用户在docker组sudo usermod -aG docker $USER然后newgrp docker或重新登录。曾有一次docker.sock权限是srw-rw---- 1 root rootdocker组存在但用户不在其中。docker ps报错Permission denied。sudo chmod 660 /var/run/docker.sock无效因为 daemon 重启后会重置权限。根本解法sudo chown root:docker /var/run/docker.sock并确保用户加入docker组。5.5 存储驱动的“空间幻影”docker system df显示磁盘空间充足但docker run失败journalctl报no space left on device。这是因为 Docker 存储驱动如overlay2的元数据空间耗尽而非用户可见的磁盘空间。验证sudo du -sh /var/lib/docker/overlay2/* \| sort -hr \| head -10找最大的 layersudo docker system prune -a清理无用镜像、容器、网络、build cachesudo find /var/lib/docker/overlay2 -name commit -type f \| xargs -r ls -lt \| head -5看最近 commit 时间判断是否堆积。终极方案修改/etc/docker/daemon.json增加storage-driver: overlay2和storage-opts: [overlay2.override_kernel_checktrue]然后sudo systemctl restart docker。我的经验操作系统层故障占比不到 5%但排查耗时占 70%。建议把docker daemon的健康检查写成脚本放在cron里每 5 分钟跑一次提前预警。脚本核心就三行docker info /dev/null 21 echo OK || echo FAILdocker ps -a \| wc -ldf -h /var/lib/docker \| awk NR2 {print $5}。简单但救命。6. 从“启动即退”到“稳定常驻”一份可落地的容器健康 checklist排查完所有可能性最终要回归到预防。我给团队制定了一份《容器健康 Checklist》不是理论文档而是每条都对应一个docker命令或一行 shell 脚本开发提交镜像前必须执行。它不追求 100% 覆盖但能拦截 95% 的“启动即退”问题。6.1 构建阶段 checklistdocker build后立即执行检查项命令期望输出不通过后果基础镜像是否存在CMD或ENTRYPOINTdocker inspect 镜像名 | jq .[0].Config.Cmd,.[0].Config.Entrypoint至少一个非null镜像无法直接运行docker run退出码127WORKDIR下文件是否齐全docker run --rm 镜像名 ls -l /app替换/app为你的WORKDIR关键文件如app.jar,server.js存在CMD执行失败退出码1或2USER权限是否覆盖所有操作docker run --rm --user 1001:1001 镜像名 sh -c touch /tmp/test rm /tmp/test无报错容器启动后因权限失败退出HEALTHCHECK是否可执行docker run --rm 镜像名 sh -c which curl | wc -l或wget输出1健康检查失败容器被标记unhealthy6.2 运行阶段 checklistdocker run后 30 秒内执行检查项命令期望输出不通过后果容器是否在docker ps -a中且状态为Updocker ps -a | grep 容器名或ID | awk {print $7}Up或Up (healthy)容器已退出需查logs和inspect端口是否真监听docker exec 容器ID netstat -tuln | grep :8080替换端口tcp 0 0 :::8080 :::* LISTEN应用未监听0.0.0.0外部无法访问日志是否有ERROR或Exceptiondocker logs 容器ID 21 | grep -i error|exception|fail|cannot无输出应用启动失败需深入分析日志内存是否超限docker stats --no-stream 容器ID | awk NR2 {print $3} 80%根据设置调整可能被 OOM Kill退出码1376.3 生产环境 checklist部署后每日执行检查项命令说明Docker daemon 状态sudo systemctl is-active docker sudo journalctl -u docker -n 10 --no-pager | grep -i error|fail确保 daemon 稳定无近期错误磁盘空间预警df -h /var/lib/docker | awk NR2 {if ($50 85) print ALERT: $5 full}/var/lib/docker使用率 85% 时触发告警僵尸容器清理docker ps -a | awk $NF ~ /^(ExitedCreated)$/ {print $1} | xargs -r docker rm镜像更新检查docker images | awk $2 ~ /^latest$/ {print $1:$2} | xargs -r docker pull确保基础镜像为最新版修复已知漏洞这份 checklist 的价值不在于多高深而在于把经验转化为可自动化的动作。我们把它集成到 GitLab CI 的teststage任何docker build失败都会在 MR 页面标红开发者必须修复才能合并。上线三个月团队“启动即退”类故障下降了 92%。最后分享一个小技巧在Dockerfile末尾加一行LABEL maintaineryour-teamcompany.com并在CMD前加HEALTHCHECK --start-period60s CMD curl -f http://localhost:$PORT/health || exit 1。这两行代码成本为零但能让后续所有排查工作提速 50%。因为LABEL让你能快速定位责任人HEALTHCHECK让docker ps直接显示healthy或unhealthy省去一半logs和inspect时间。容器的世界没有魔法只有清晰的因果链。docker ps不显示从来不是 Docker 的错而是它在用最简洁的方式告诉你“嘿你的进程已经完成了它的使命。”听懂这句话你就离真正掌控容器不远了。
返回列表