容器"起不来"通常有三种表现:docker run 敲下去就返回一个容器 ID,但 docker ps 里找不到它;容器出现了,状态是 Exited (1) 或者反复 Restarting;命令干脆直接报错,容器根本没被创建。下面这套排查顺序,覆盖了绝大多数场景。
报错现象
常见的报错原文有这么几类:
```
docker: Error response from daemon: driver failed programming external connectivity on endpoint web:
Bind for 0.0.0.0:8080 failed: port is already allocated.
```
```
standard_init_linux.go:xxx: exec user process caused "exec format error"
```
```
docker: Error response from daemon: no matching manifest for linux/amd64 in the manifest list entries.
```
```
/app/start.sh: permission denied
```
以及没有明显报错、只是静默退出的:
```
$ docker ps -a
CONTAINER ID IMAGE STATUS PORTS NAMES
a1b2c3d4e5f6 myapp:dev Exited (1) 8 seconds ago myapp
```
影响范围从"单个容器起不来"到"整台机器上所有容器都起不来"都有可能。先确认是个例还是全局:如果同一台机器上新起的容器全部失败,优先怀疑磁盘、Docker 守护进程、内核或资源问题,而不是应用代码。
可能原因
按出现概率从高到低:
1. 应用自身启动就失败。配置缺失、连不上数据库、启动脚本路径写错、CMD 里命令拼错,容器进程一启动就退出。对应退出码 1、126、127、255。
2. 端口被占用或端口映射写错。宿主机端口已被别的进程或别的容器占用,或者 -p 两边写反。
3. 挂载路径有问题。宿主机目录不存在、写成了相对路径被当成 volume 名、把镜像里的目录整个盖掉了、挂载后权限不对。
4. 权限与资源限制。容器内进程是非 root 用户但要去写 root 拥有的目录;被 cgroup 内存限制触发 OOM Killer 杀掉(退出码 137);SELinux / AppArmor 拦截。
5. CPU 架构不匹配。镜像构建在 linux/arm64 上,跑到 linux/amd64 机器上(或反过来),直接 exec format error。
逐条排查与解决
第一处:退出码——判断是"自己退出"还是"被杀掉"
```bash
docker ps -a
docker inspect --format '{{.State.ExitCode}} OOM={{.State.OOMKilled}} {{.State.Error}}' 容器名
```
退出码的含义是通用约定:
| 退出码 | 含义 | 常见场景 |
|---|---|---|
| 0 | 主进程正常结束 | 一次性任务跑完了;常驻服务却返回 0,说明它没有前台进程 |
| 1 | 应用层通用错误 | 配置错、依赖连不上,需要看日志 |
| 125 | docker 客户端/守护进程层面的失败 | docker run 参数写错 |
| 126 | 命令存在但没有执行权限 | 脚本没加可执行位 |
| 127 | 命令找不到 | 镜像里没装这个命令,或 PATH 不对 |
| 128+N | 被信号 N 终止 | 137 = 128+9(SIGKILL,多为 OOM 或 stop 超时);143 = 128+15(SIGTERM);139 = 段错误 |
| 255 | 应用自己返回的错误 | 需要看应用日志 |
判断方法:看到 137 且 OOMKilled=true,就去查内存限制;看到 126 / 127,基本可以锁定启动命令或执行权限问题,不用再猜业务逻辑。
第二处:日志——看容器自己说了什么
```bash
docker logs --tail 200 容器名
docker logs -f 容器名
docker logs --since 10m 容器名
```
日志为空说明两件事之一:进程还没跑到打印日志就退出了,或者应用把日志写进了容器内的文件而不是 stdout/stderr。后者很常见,表现就是"容器一直重启,日志一条没有"。
想进一步确认,可以覆盖入口命令进容器看:
```bash
docker run --rm -it --entrypoint sh 镜像名
```
进去之后手动执行 Dockerfile 里 CMD / ENTRYPOINT 的那条命令,看它报什么。
第三处:端口与挂载
端口先看有没有被占:
```bash
docker ps -a --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
ss -ltnp | grep :8080
ss -lunp | grep :8080
```
ss 输出里能直接看到占用该端口的进程 PID 和程序名。如果占用方是 docker-proxy,说明是另一个容器在用这个端口,用第一条命令就能找出是哪个容器。想快速验证是不是端口冲突,换一个宿主机端口再起一次即可。
挂载先看实际挂了什么:
```bash
docker inspect --format '{{json .Mounts}}' 容器名
```
几个高频坑:
- 宿主机目录不存在时,bind mount 会自动创建一个空目录,容器里看到的是空目录,应用以为配置文件丢了,于是报错退出。
-v data:/app/data这种写法里data是卷名,不是路径。想用路径必须写绝对路径/srv/data:/app/data。- 把宿主机目录挂到
/app这种代码目录上,会把镜像里原有的文件全部盖掉,容器里就只剩你挂进去的东西。 - 排查阶段建议改用
--mount,它在源路径不存在时会直接报错,而不是悄悄造一个空目录:
```bash
docker run --rm --mount type=bind,src=/srv/conf,dst=/app/conf,readonly 镜像名
```
报 Read-only file system 时,检查是不是加了 --read-only 或者挂载带了 :ro,需要写入的目录要用 --tmpfs 或单独的卷放出来。
第四处:权限
```bash
docker inspect --format '{{.Config.User}}' 镜像名
ls -ln /srv/data
```
ls -ln 看的是数字 UID/GID,这一点很关键:容器里跑的是 UID 1000,宿主机目录属主也是 1000,看起来"名字不一样但数字一样",实际是能写的。反过来,如果容器内进程是 UID 1000、宿主机目录属主是 root 且权限是 755,写操作就会 Permission denied。
解决方式按场景选:
```bash
sudo chown -R 1000:1000 /srv/data
```
或者在 Dockerfile 里显式指定一个和宿主机匹配的 UID:
```dockerfile
RUN useradd -u 1000 -m appuser
USER 1000
```
CentOS / RHEL / Fedora 这类启用 SELinux 的系统,还要看挂载标签:
```bash
ls -Z /srv/data
```
需要时在挂载参数后加 :z(多个容器共享)或 :Z(独占)。
注意区分一个容易混淆的报错:
```
permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock
```
这是当前用户没有权限访问 Docker 套接字,和容器内部权限没有关系,处理方式是加入 docker 组或用 sudo,具体做法以官方文档当前版本为准。
第五处:架构
```bash
uname -m
docker version --format '{{.Server.Arch}}'
docker image inspect 镜像名 --format '{{.Architecture}} {{.Os}}'
```
两边的架构对不上,就会出现 exec format error,或者拉镜像阶段就报 no matching manifest。
典型场景:在 Apple 芯片的 Mac 上构建出来的镜像,直接推到 x86 服务器上跑;反过来在 x86 上构建的镜像拿到 arm 服务器上跑。
处理方式:
```bash
docker run --platform linux/amd64 镜像名
```
前提是宿主机已经注册了对应的模拟执行支持,具体配置方式以官方文档当前版本为准。更稳妥的做法是用 buildx 一次性构建多架构镜像:
```bash
docker buildx build --platform linux/amd64,linux/arm64 -t 镜像名:tag --push .
```
需要说明的是,跨架构模拟执行的性能会明显下降,部分依赖特定指令的应用也可能跑不稳,能构建原生架构镜像就不要长期依赖模拟。
都不管用时的兜底方案
1. 绕开所有包装层,最小化复现:不用 compose、不用自定义 entrypoint,直接 docker run --rm -it --entrypoint sh 镜像名,手动执行启动命令。
2. 看守护进程日志:journalctl -u docker --since "30 min ago",桌面版 Docker 则看它的日志界面。
3. 看资源与存储:docker info 关注存储驱动和告警,df -h /var/lib/docker 确认磁盘没满,磁盘写满会让新容器直接创建失败。
4. 观察事件流:另开一个终端跑 docker events,再启动容器,能看到状态变化的完整过程。
5. 检查编排文件:用 docker compose config 校验语法,去掉 -d 前台启动,报错会直接打到终端。
6. 重启守护进程:sudo systemctl restart docker。注意这会停掉机器上所有运行中的容器,生产环境要先确认影响面。
如何预防再次发生
- 镜像里显式声明非 root 用户和固定 UID,别让容器以 root 跑。
- 挂载统一用
--mount写绝对路径,配置文件优先用环境变量或配置卷,少挂整个代码目录。 - 应用日志打到 stdout/stderr,别只写文件,否则排查时等于没有日志。
- 端口在编排文件里集中管理,起新服务前先
ss -ltnp确认端口空闲。 - 给容器设
--memory、--cpus上限,并配上restart: unless-stopped或on-failure策略,让偶发崩溃能自愈,同时避免单个容器吃光整机内存。 - 构建阶段就用 buildx 产出多架构镜像,部署前在目标架构上跑一次冒烟测试。
- 给关键服务加 healthcheck,让"起来了但没就绪"这类问题暴露在编排层,而不是等到用户报障。
