Docker容器启动就退出?完整排查指南(含退出码137/1解决方案)

周五晚上八点半,我正准备关电脑下班。手机突然震了一下——生产环境告警。点开一看,四个核心服务的容器全部变成了Exited状态。
打开终端,敲下 docker ps。空白。一片空白。
那种感觉就像你打开冰箱想拿饮料,发现整个冰箱都空了。慌。
说实话,我当时脑子里闪过的第一个念头是”完了,这周末泡汤了”。但冷静下来后,我意识到这不是第一次遇到容器启动失败的问题了。只是这次,问题来得更突然,影响更大而已。
后来经过2个多小时的排查,我发现问题其实挺简单——有个服务配置文件路径写错了,导致依赖的数据库连接失败,容器启动后立即退出。如果当时我有一套系统的排查流程,这个问题可能10分钟就能解决。
这篇文章就是我踩了无数个坑之后,总结出来的容器启动失败排查指南。不管你看到的是Exit Code 1、137还是其他退出码,这套方法都能帮你快速定位问题根源。
理解容器生命周期和退出码
在开始排查之前,咱们先搞清楚一个基本问题:容器为什么会退出?
容器的本质:进程的生命周期
Docker容器说白了就是一个被隔离的进程。这个进程活着,容器就在运行;进程挂了,容器也就退出了。
想象一下,你启动一个Web服务容器。容器里的主进程可能是nginx或者node,只要这个进程还在运行,docker ps就能看到这个容器。但如果主进程因为某种原因退出了——可能是正常结束,可能是崩溃,也可能是被系统强制杀掉——容器就会立即变成Exited状态。
这就是为什么你有时候执行 docker ps 什么都看不到,得加上 -a 参数才能看到那些已经退出的容器。
退出码速查表:数字背后的故事
每次容器退出,Docker都会记录一个退出码(Exit Code)。这个数字看起来很神秘,其实它在告诉你发生了什么。
Exit Code 0:一切正常,任务完成了。
比如你运行一个数据导入脚本,跑完了自然退出,这时候就是0。这不是问题,只是容器完成了它该做的事。
Exit Code 1:程序自己出问题了。
这是最常见的错误码。可能是配置文件写错了,可能是缺少某个依赖,也可能是代码里有bug。总之,是容器里的应用程序自己挂了。
我记得有次部署MySQL容器,一直Exit Code 1。查了半天日志才发现,是我手滑把配置文件里的一个等号写成了冒号。MySQL一启动就发现配置有语法错误,直接罢工。
Exit Code 137:内存不够用了,或者被强制杀掉了。
这个码我最怕看到。137通常意味着两种情况:
- 容器内存用超了,Linux内核的OOM Killer(内存杀手)把进程干掉了
- 有人(或者系统)执行了
docker kill或kill -9
怎么区分?用 docker inspect 看一眼 OOMKilled 字段就知道了。如果是 true,那就是内存问题;如果是 false,那可能是被人为终止的。
Exit Code 127:找不到命令。
通常是Dockerfile里的CMD或ENTRYPOINT写错了路径,或者容器镜像里压根没有那个可执行文件。
Exit Code 139:段错误(Segmentation Fault)。
这个一般出现在C/C++程序里,说明程序访问了不该访问的内存地址。如果你不是在跑底层程序,基本遇不到这个。
退出码的规律
状态码其实是有规律的:
- 0:正常退出,没毛病
- 1-128:程序自己的问题(应用错误、配置错误等)
- 129-255:外部干预(被信号中断、被系统杀掉等)
知道这些规律后,你看到退出码就大概能判断是什么类型的问题了,后面的排查就有方向了。
四步排查法快速定位问题
好,现在你知道退出码的含义了。但光知道含义还不够,得知道怎么一步步把问题挖出来。
我摸索出了一套四步排查法,基本上能覆盖90%的容器启动失败场景。跟着这个流程走,你会发现问题其实没那么神秘。
第一步:确认容器状态
先别急着看日志,第一步是确认容器确实存在,而且确实挂了。
docker ps -a这个命令会列出所有容器,包括已经退出的。你要关注几个关键信息:
CONTAINER ID:容器的唯一标识,后面的命令都要用到它。可以只复制前几位,Docker会自动匹配。
STATUS列:重点看这里。正常运行的容器会显示 Up X minutes,退出的容器会显示 Exited (退出码) X minutes ago。
比如你看到:
CONTAINER ID IMAGE STATUS
a1b2c3d4e5f6 mysql:8.0 Exited (1) 2 minutes ago退出码是1,说明是应用层的问题。如果是137,那可能是内存问题。
注意容器的创建时间和退出时间。如果容器创建后不到1秒就退出了,那多半是启动命令或者配置有问题。如果运行了一段时间才退出,可能是资源不足或者依赖服务挂了。
第二步:查看容器日志
这是最关键的一步。容器退出前通常会留下线索,而这些线索就藏在日志里。
基础查看:
docker logs <container_id>这会显示容器的所有标准输出和标准错误。很多时候,你会直接看到报错信息,比如 Permission denied、No such file or directory、Connection refused 等等。
实时追踪(适合排查启动过程):
docker logs -f <container_id>如果你想看容器启动过程中发生了什么,用 -f 参数。它会像 tail -f 一样实时显示新日志。不过已经退出的容器用这个意义不大,主要用于尝试重新启动时观察。
只看最近的日志:
docker logs --tail 100 <container_id>如果容器日志太长,可以只看最后100行。往往最后几行就能找到问题。
带上时间戳:
docker logs -t <container_id>-t 参数会在每行日志前加上时间戳,方便你判断问题发生的准确时间。
过滤错误日志:
docker logs <container_id> 2>&1 | grep -i error如果日志太多,可以只看包含”error”的行。这个技巧能快速定位关键错误。
第三步:检查容器配置
有时候日志也看不出个所以然,这时候就要深入看看容器的配置和状态了。
查看完整配置:
docker inspect <container_id>这个命令会输出一大堆JSON格式的信息,包括容器的所有配置、环境变量、挂载点、网络设置等等。信息量很大,但也很有用。
快速查看特定信息:
查看退出码:
docker inspect --format '{{.State.ExitCode}}' <container_id>查看是否被OOM Killed:
docker inspect --format '{{.State.OOMKilled}}' <container_id>如果输出是 true,那就确定了是内存问题。
查看环境变量:
docker inspect --format '{{.Config.Env}}' <container_id>有时候是环境变量配置错了,数据库连接字符串、API密钥之类的。
查看挂载路径:
docker inspect --format '{{.Mounts}}' <container_id>检查配置文件、数据目录的挂载是否正确。
查看日志文件路径:
docker inspect --format='{{.LogPath}}' <container_id>万一 docker logs 也不好使,你可以直接去主机上找日志文件。
第四步:交互式启动验证
前面三步都走完了,问题还没定位到?那就得亲自进容器里看看了。
交互式启动容器:
如果你原来的启动命令是:
docker run -d my-app那就把 -d 改成 -it,让容器在前台运行:
docker run -it my-app这样你能实时看到容器启动过程中的所有输出,很多错误会直接显示在屏幕上。
手动进入容器:
如果容器启动了又立即退出,你可以用shell进入容器手动执行命令:
docker run -it my-app /bin/bash或者:
docker run -it my-app /bin/sh进去后,你可以:
- 检查配置文件是否存在:
ls /etc/app/config.yaml - 测试配置文件语法:比如MySQL的
mysqld --verbose --help会检查配置 - 手动运行启动命令,看具体报什么错
- 检查依赖服务连通性:
ping database、telnet redis 6379
这种方法特别适合排查路径问题、权限问题、依赖问题。
说了这么多,你可能觉得步骤挺多的。但相信我,实际操作的时候,很多问题在第二步看日志时就能找到答案。只有少数疑难杂症才需要走完全部四步。
五大常见失败场景及解决方案
排查方法讲完了,现在咱们看看实战中最常遇到的几种情况。我把它们归纳成五大类,基本涵盖了日常遇到的大部分问题。
场景1:配置文件错误或路径不存在
典型表现:
- Exit Code 1
- 日志里出现
No such file or directory、config file not found、syntax error之类的报错
真实案例:
有次我部署一个Node.js应用,容器一直启动不起来。日志显示:
Error: ENOENT: no such file or directory, open '/app/config/prod.json'检查后发现,我在 docker run 命令里把挂载路径写成了:
-v /home/user/config:/app/conf # 注意这里是conf但应用里读取的路径是 /app/config。路径差一个字母,应用找不到配置文件,启动失败。
排查方法:
- 用
docker inspect --format '{{.Mounts}}'检查挂载路径 - 进容器里
ls看看文件是不是真的在那个位置 - 如果是配置文件语法错误,大多数应用会在日志里明确指出哪一行有问题
解决方案:
挂载路径错误:
# 错误示例
docker run -v /host/path:/wrong/path my-app
# 正确做法
docker run -v /host/path:/app/config my-app配置文件语法错误:
- 对于YAML文件,可以用在线工具或者
yamllint检查语法 - 对于JSON文件,用
jq命令验证:jq . config.json - 对于MySQL配置,可以在容器里执行
mysqld --verbose --help看有没有语法错误
场景2:内存不足(OOM Killed)
典型表现:
- Exit Code 137
docker inspect --format '{{.State.OOMKilled}}'返回true- 日志里可能出现
Cannot allocate memory、Out of memory等字样
真实案例:
我有个Java应用,在本地开发环境跑得好好的,一部署到测试服务器就不停重启。查看日志发现:
OpenJDK 64-Bit Server VM warning: INFO: os::commit_memory failed; error='Cannot allocate memory' (errno=12)原来测试服务器上Docker Desktop的内存限制只给了512MB,而这个Java应用启动就要吃掉600MB。
排查方法:
# 确认是否OOM
docker inspect --format '{{.State.OOMKilled}}' <container_id>
# 查看主机内存情况
free -h
# 查看容器运行时的内存使用
docker stats <container_id>解决方案:
增加容器内存限制:
docker run -m 1g my-app # 限制最大内存1GB
docker run -m 512m --memory-swap 1g my-app # 同时设置swap如果是Docker Desktop,去设置里调整:
- macOS:Docker Desktop → Preferences → Resources → Memory
- Windows:Docker Desktop → Settings → Resources → Memory
优化应用本身:
- Java应用可以限制JVM堆大小:
java -Xmx512m -jar app.jar - Node.js可以设置:
node --max-old-space-size=512 app.js - 检查代码是否有内存泄漏
生产环境建议:
- 根据应用实际需求设置合理的内存上限
- 配置
--memory-reservation设置软限制 - 监控内存使用趋势,提前扩容
场景3:端口冲突
典型表现:
- Exit Code 1
- 日志里出现
port is already allocated、address already in use、bind: address already in use
真实案例:
周一早上来公司,运行 docker-compose up,结果Nginx容器起不来。错误信息:
Error starting userland proxy: listen tcp4 0.0.0.0:80: bind: address already in use原来是周五下班前我测试了一个本地的Nginx,忘记关了。端口80被占用了,新容器当然起不来。
排查方法:
检查端口占用(Linux/macOS):
lsof -i :8080
netstat -tuln | grep 8080检查端口占用(Windows):
netstat -ano | findstr 8080查看其他容器的端口映射:
docker ps --format "table {{.Names}}\t{{.Ports}}"解决方案:
方案1:更换映射端口
# 原来的命令
docker run -p 8080:8080 my-app
# 改成其他端口
docker run -p 8081:8080 my-app方案2:停止占用端口的服务
# 找到进程ID
lsof -i :8080
# 杀掉进程
kill -9 <PID>方案3:如果是其他容器占用,先停掉
docker stop <conflicting_container>特别注意:如果你使用 --network=host 模式,容器会直接使用主机网络,端口冲突的概率更高。这种模式下,容器内的端口必须和主机上的端口不冲突。
场景4:权限不足
典型表现:
- Exit Code 1
- 日志里出现
Permission denied、Operation not permitted、chown: changing ownership failed
真实案例:
部署一个MongoDB容器,挂载数据目录到主机。结果容器一直起不来:
chown: changing ownership of '/data/db': Permission denied查了才知道,我挂载的主机目录属于root用户,而容器里MongoDB进程用的是mongodb用户(UID 999),没有权限写入这个目录。
排查方法:
检查主机目录权限:
ls -la /host/data/path查看容器内用户:
docker run -it my-app /bin/bash
whoami
id检查SELinux(CentOS/RHEL):
getenforce # 查看SELinux状态解决方案:
方案1:调整主机目录权限
# 给所有用户读写权限(不太安全,仅开发环境)
chmod 777 /host/data/path
# 更安全的做法:改变所有者
chown -R 999:999 /host/data/path # 999是容器内用户的UID方案2:使用特权模式(慎用)
docker run --privileged=true my-app注意:特权模式会给容器几乎所有主机权限,有安全风险。生产环境不推荐。
方案3:指定运行用户
docker run --user 1000:1000 my-app # 使用主机上的UID/GID方案4:处理SELinux问题
# 方法1:添加Z标签(修改主机文件标签)
docker run -v /host/path:/container/path:Z my-app
# 方法2:添加z标签(共享标签)
docker run -v /host/path:/container/path:z my-app
# 方法3:临时关闭SELinux(不推荐生产环境)
setenforce 0场景5:依赖服务未就绪
典型表现:
- Exit Code 1
- 日志里出现数据库连接失败、Redis连接超时等信息
Connection refused、ECONNREFUSED、could not connect to server
真实案例:
用docker-compose部署一套微服务,里面有个应用容器依赖MySQL数据库。两个容器几乎同时启动,但应用容器总是失败:
Error: connect ECONNREFUSED 172.18.0.2:3306问题在于:MySQL容器虽然启动了,但MySQL服务还在初始化,没有真正准备好接受连接。应用容器启动太快,连接失败后就退出了。
排查方法:
检查依赖服务是否启动:
docker ps # 看依赖的容器是否在运行测试网络连通性:
docker exec my-app ping database
docker exec my-app telnet database 3306
docker exec my-app nc -zv database 3306查看Docker网络配置:
docker network ls
docker network inspect <network_name>解决方案:
方案1:使用docker-compose的健康检查和depends_on
version: '3.8'
services:
database:
image: mysql:8.0
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
app:
image: my-app
depends_on:
database:
condition: service_healthy # 等待数据库健康检查通过方案2:应用层增加重试机制
在应用代码里增加连接重试逻辑:
// Node.js示例
async function connectWithRetry(maxRetries = 5) {
for (let i = 0; i < maxRetries; i++) {
try {
await db.connect();
console.log('Database connected');
return;
} catch (err) {
console.log(`Connection failed, retrying... (${i+1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, 5000));
}
}
throw new Error('Failed to connect to database');
}方案3:使用启动等待脚本
可以在容器启动前加一个等待脚本,比如 wait-for-it.sh:
# 在Dockerfile里
COPY wait-for-it.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/wait-for-it.sh
# 启动时使用
CMD ["wait-for-it.sh", "database:3306", "--", "node", "app.js"]方案4:配置重启策略
让容器失败后自动重试:
docker run --restart=on-failure:3 my-app # 失败后最多重启3次docker-compose里:
services:
app:
restart: on-failure这几种方案可以组合使用,比如健康检查+应用层重试+重启策略,三重保险。
预防性措施和最佳实践
前面讲的都是出问题后怎么修。但其实,如果一开始就配置好一些机制,很多问题压根不会发生,或者发生了也能自动恢复。
配置健康检查(HEALTHCHECK)
健康检查是Docker容器自检的机制。通过定期执行检查命令,Docker能判断容器是不是真的在正常工作,而不仅仅是进程还活着。
在Dockerfile里配置:
FROM nginx:alpine
# 每30秒检查一次,超时3秒,连续失败3次则标记为unhealthy
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD curl -f http://localhost/ || exit 1对于Web服务,可以检查HTTP端点:
HEALTHCHECK --interval=30s --timeout=5s --start-period=40s \
CMD curl -f http://localhost:8080/health || exit 1对于数据库,可以用专用命令:
# MySQL
HEALTHCHECK CMD mysqladmin ping -h localhost || exit 1
# PostgreSQL
HEALTHCHECK CMD pg_isready -U postgres || exit 1
# Redis
HEALTHCHECK CMD redis-cli ping || exit 1健康检查的好处:
- Kubernetes/Swarm等编排工具会根据健康状态自动重启或重新调度容器
- docker-compose的depends_on可以等待服务真正健康后再启动依赖容器
- 监控系统可以基于健康状态发出告警
查看健康状态:
docker ps # STATUS列会显示health状态
docker inspect --format='{{.State.Health.Status}}' <container_id>设置重启策略
重启策略能让容器在失败后自动恢复,不用你半夜爬起来手动重启。
Docker提供四种重启策略:
no(默认):不自动重启
docker run --restart=no my-app适合一次性任务,跑完就结束的容器。
on-failure[:max-retries]:只在非正常退出时重启
docker run --restart=on-failure:5 my-app # 最多重试5次适合可能出错但不想无限重试的服务。注意:只有Exit Code非0时才会重启。
always:总是重启
docker run --restart=always my-app适合长期运行的服务,比如Web服务器、API服务。即使手动stop了,下次Docker Daemon重启时也会自动启动这个容器。
unless-stopped:除非手动停止,否则总是重启
docker run --restart=unless-stopped my-app和always类似,但如果你手动执行了 docker stop,下次Docker Daemon重启时不会自动启动这个容器。这是我最常用的策略,给了我一定的控制权。
重要提示:
- 10秒规则:容器首次启动后至少运行10秒,重启策略才会生效。这是为了防止配置错误导致无限重启吃满系统资源。
- 无限重启陷阱:如果容器因为配置错误(比如端口冲突)不停重启,日志会爆炸式增长。记得配合日志轮转使用。
对于已运行的容器,可以动态修改策略:
docker update --restart=unless-stopped <container_id>在docker-compose里配置:
services:
web:
image: nginx
restart: unless-stopped # 推荐生产环境使用
worker:
image: my-worker
restart: on-failure # 可能失败,但不希望无限重试日志管理:避免磁盘爆满
Docker默认会把所有容器日志保存在JSON文件里,时间长了这些日志文件能吃掉几十GB磁盘空间。我就遇到过生产服务器因为Docker日志把磁盘撑爆的事故。
配置日志轮转(推荐):
创建或编辑 /etc/docker/daemon.json:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m", // 单个日志文件最大10MB
"max-file": "3" // 最多保留3个日志文件
}
}修改后重启Docker:
sudo systemctl restart docker这样每个容器最多占用30MB日志空间(10MB × 3),旧日志会自动删除。
针对单个容器配置:
docker run --log-opt max-size=10m --log-opt max-file=3 my-appdocker-compose里:
services:
app:
image: my-app
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"其他日志驱动选择:
- syslog:发送到系统日志
- journald:使用systemd的journal
- fluentd:发送到Fluentd进行集中管理
- none:不记录日志(不推荐)
查看容器日志文件位置和大小:
docker inspect --format='{{.LogPath}}' <container_id>
du -h $(docker inspect --format='{{.LogPath}}' <container_id>)监控和告警:提前发现问题
不要等容器挂了才知道,提前监控能避免很多生产事故。
基础监控:docker stats
docker stats # 实时显示所有容器的资源使用情况
docker stats <container_id> # 监控特定容器这个命令会显示CPU、内存、网络IO、磁盘IO的实时数据。如果看到内存使用持续增长,可能有内存泄漏,要提前处理。
生产环境:Prometheus + Grafana
更专业的做法是用Prometheus采集指标,Grafana可视化:
# docker-compose.yml
services:
prometheus:
image: prom/prometheus
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"
grafana:
image: grafana/grafana
ports:
- "3000:3000"
cadvisor: # 采集容器指标
image: google/cadvisor
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
ports:
- "8080:8080"配置告警规则,内存使用超过80%、容器重启次数过多等情况自动发送通知。
简单粗暴:定时脚本
如果你觉得Prometheus太复杂,可以写个简单脚本:
#!/bin/bash
# check-containers.sh
# 检查有没有Exited状态的容器
EXITED=$(docker ps -a -f "status=exited" --format "{{.Names}}")
if [ -n "$EXITED" ]; then
echo "Warning: The following containers are exited:"
echo "$EXITED"
# 这里可以发送邮件或者推送通知
fi加到crontab里每5分钟执行一次:
*/5 * * * * /path/to/check-containers.sh生产环境配置清单
最后,给你一个生产环境的配置清单,照着这个来基本不会出大问题:
version: '3.8'
services:
web:
image: my-web-app:latest
# 重启策略
restart: unless-stopped
# 资源限制
deploy:
resources:
limits:
cpus: '2'
memory: 1G
reservations:
memory: 512M
# 健康检查
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 40s
# 日志管理
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# 环境变量(敏感信息用secrets)
environment:
- NODE_ENV=production
# 端口映射
ports:
- "8080:8080"
# 依赖关系
depends_on:
database:
condition: service_healthy
database:
image: postgres:14
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
volumes:
- db-data:/var/lib/postgresql/data
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
volumes:
db-data:有了这些配置,容器挂了会自动重启,资源用超了会被限制,日志不会撑爆磁盘,监控能提前预警。基本上可以睡个安稳觉了。
结论
说了这么多,我希望你记住的是:Docker容器启动失败不可怕,可怕的是没有系统的排查思路。
回顾一下这篇文章的核心内容:
理解退出码:看到137就想到内存问题,看到1就想到配置或依赖问题。退出码是Docker留给你的线索,别忽略它。
四步排查法:
- 看容器状态(
docker ps -a) - 查日志(
docker logs) - 检查配置(
docker inspect) - 交互式验证(
docker run -it)
九成以上的问题在第二步就能解决。
五大常见场景:配置错误、内存不足、端口冲突、权限问题、依赖未就绪。把这几种情况的排查方法记住,基本够用了。
提前预防:配置健康检查、设置重启策略、管理好日志、做好监控。这些机制能让你的容器更稳定,问题出现时自动恢复。
最后给你一个快速排查清单,可以截图保存:
Docker容器启动失败排查清单
□ 步骤1: docker ps -a 查看容器状态和退出码
□ 步骤2: docker logs <container_id> 查看详细日志
□ 步骤3: docker inspect <container_id> 检查配置
□ 步骤4: docker run -it <image> 交互式验证
常见问题快速定位:
- Exit Code 1 + "No such file" → 检查挂载路径和配置文件
- Exit Code 1 + "port already allocated" → 检查端口冲突
- Exit Code 1 + "Permission denied" → 检查文件权限和SELinux
- Exit Code 1 + "Connection refused" → 检查依赖服务是否就绪
- Exit Code 137 + OOMKilled=true → 增加内存限制
- Exit Code 127 → 检查CMD/ENTRYPOINT路径是否正确
预防措施:
□ 配置HEALTHCHECK
□ 设置restart策略(推荐unless-stopped)
□ 配置日志轮转(max-size + max-file)
□ 资源限制(-m 内存限制)
□ 监控告警(docker stats或Prometheus)如果这篇文章帮到了你,建议收藏起来,下次遇到容器启动问题时拿出来对照着查。如果你遇到过什么特殊的容器启动问题,也欢迎在评论区分享,说不定能帮到其他人。
祝你的容器永远Up and Running,再也不要在周五晚上接到容器挂掉的告警!
16 分钟阅读 · 发布于: 2025年12月18日 · 修改于: 2025年12月26日



评论
使用 GitHub 账号登录后即可评论