本节摘要:错误处理讲究对号入座。本节以"症状—原因—解法"对照表为主线,覆盖语法错误、端口冲突、卷挂载失败、镜像拉取失败、环境变量未生效、启动顺序错乱、健康检查失败、退出码非零、升级兼容九类高频故障,每类都给出真实报错文本、成因分析和可执行的修复步骤。读完本节,遇到报错先查表,多数问题十分钟内能收工,剩下的再进容器深挖。
报错文本决定排查方向。这句话是本节的总纲——别急着改配置,先把报错复制出来,对照下表归类:
| 症状 | 常见原因 | 首选解法 |
|---|---|---|
| 服务起不来,报 YAML 解析失败 | 缩进、引号、字段拼写错误 | docker compose config 定位行号 |
| bind: address already in use | 宿主机端口被其他进程或容器占用 | netstat -tulnp 查占用,改端口 |
| 挂载目录为空或写入报权限拒绝 | 宿主机路径不存在、属主不匹配 | 预先建目录,chown 授权 |
| manifest unknown 或 pull access denied | 镜像名或标签错误、私有仓库未登录 | 核对名称,docker login |
| 环境变量读到空值 | .env 未加载、变量名拼写不一致 | 检查 .env 与 environment 段 |
| 依赖服务未就绪 | 只写了 depends_on 没等健康 | 加 healthcheck 与 condition |
| 容器显示 unhealthy 反复重启 | 健康检查命令错误或间隔不当 | 调整 test、interval、retries |
| 退出码 137 | 内存超限被内核杀死 | docker stats 观察,调大限制 |
| 升级后行为异常 | 新版本特性或行为变化 | 读升级文档,逐步升级 |
下面九个小节逐一拆解,每类都带真实示例和修复步骤。
YAML 对缩进零容忍,差一个空格就是两种语义。最典型的是列表项缩进不齐:
# 错误示例:ports 列表项缩进不一致 version: "3.9" services: web: image: nginx:latest ports: - "80:80" # 正确示例:列表项与键名对齐 version: "3.9" services: web: image: nginx:latest ports: - "80:80"
修复步骤:第一步,docker compose config 直接报出错误行号,顺着行号找;第二步,统一缩进风格,同一层级用同样数量的空格,全文件保持一致;第三步,检查类型错误——典型的是 deploy 段的资源限制,cpus 必须写成字符串 "0.5" 而不能写数字 0.5,memory 要写 "512M" 这样的字符串,类型不匹配同样解析失败。
报错文本通常是 Error starting userland proxy: listen tcp4 0.0.0.0:8080: bind: address already in use。含义很直白:宿主机 8080 端口已经被占,要么是别的服务,要么是另一个容器。
修复步骤:第一步,netstat -tulnp 找出占用者,端口对应的进程号和时间戳都能看到,Windows 上可以用 netstat -ano 加任务管理器对照;第二步,判断占用者能不能停,不能停就改 compose 映射,把 "8080:80" 改成 "8081:80";第三步,如果不在乎宿主机端口号,可以只写容器端口,比如 ports 下写 "80",让 Docker 自动分配宿主机端口,再用 docker ps 查实际映射。
⚠️ 一个常见误区:两个容器映射到不同宿主机端口就不会冲突,但容器内端口互相冲突仍然会报错。同一个 compose 项目里两个服务都写 "3306:3306" 而宿主机只有一个 3306,照样起不来第二个。
症状有两类:容器起来了但目录是空的,或者写入时报 permission denied。根因通常是三个:宿主机路径不存在、路径属主与容器内用户不匹配、卷名称冲突。
先说路径。bind mount 指向的宿主机路径如果不存在,Docker 会尝试自动创建,但创建出的目录属主是 root,容器里以非 root 用户运行的应用(比如 UID 1000 的 node 用户)根本写不进去。正确做法是预先建目录并授权:
sudo mkdir -p /data/myapp sudo chown -R 1000:1000 /data/myapp
更通用的写法是先把属主改成容器内用户的 UID,再给目录加权限:sudo chown -R 用户:用户组 宿主机路径,sudo chmod -R 755 宿主机路径。排查时用 docker inspect 容器名 看 Mounts 段,源路径、目标路径、读写权限一目了然。
再说卷名称冲突。不同 compose 项目如果都声明了同名具名卷,可能互相污染数据。compose 默认会给具名卷加项目名前缀,但显式指定卷名时仍可能撞车。解决方式是使用具名卷并在顶层声明:
services: web: image: nginx:latest volumes: - mydata:/usr/share/nginx/html volumes: mydata:
具名卷由 Docker 管理,数据落在卷里而不是散落在宿主机路径,容器删除后数据仍在,比 bind mount 更适合存数据库文件。
报错常见三种:manifest unknown 表示镜像或标签不存在;pull access denied 表示没权限;connection refused 表示连不上仓库。成因分别是名称错误、未登录私有仓库、网络或仓库不可用。
修复步骤:第一步,核对镜像名、标签和仓库地址,latest 标签不存在是高频坑;第二步,用 docker pull 手动拉一次,隔离 compose 层的问题;第三步,私有仓库先登录:docker login 私有镜像仓库地址;第四步,确认镜像加速器配置正确,国内环境尤其常见;第五步,用 image_pull_policy 控制拉取策略,设置为 always 强制每次拉最新,设置为 if_not_present 则仅在本地没有时才拉。
症状是应用起来后读到空值或默认值,但 compose 文件里明明写了。最常见的原因不是没写,而是写错了地方。.env 文件只做插值,不会自动变成容器环境变量——只有 environment 段里引用了 ${变量名},值才会注入容器。
# .env 文件 DATABASE_URL=postgres://user:password@host:port/database
services: web: image: myapp:latest environment: DATABASE_URL: ${DATABASE_URL}
修复步骤:第一步,确认 .env 和 docker-compose.yml 在同一目录;第二步,检查变量名拼写,.env 里叫 DATABASE_URL,environment 里写成 DATABASE_URL 之外的名字,插值结果就是空;第三步,用 docker compose config 看插值后的最终值,这一步能直接暴露空值和类型问题;第四步,进容器用 env 命令核对实际注入的变量。
💡 环境变量值里含有冒号或特殊字符时,务必给 .env 里的值加引号或转义,否则解析可能截断。密码里带特殊符号的用户,建议改用 secrets 或文件挂载的方式传递敏感信息。
症状是 web 启动时报数据库连接失败,但 db 容器明明在运行。原因:depends_on 默认只等"容器启动完成",不等"服务就绪"。MySQL 进程起来了,但还没准备好接受连接,web 就抢跑了。
修复方法有两种,我更倾向第二种。第一种是粗暴等待,在启动命令里加延时:
services: db: image: postgres:latest web: image: myapp:latest depends_on: db: condition: service_started command: ["/bin/sh", "-c", "sleep 10 && npm start"]
sleep 10 能解决大部分情况,但依赖服务启动慢时照样翻车。第二种是健康检查加 condition,等依赖服务真正健康再启动:
services: db: image: postgres:latest healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 10s timeout: 5s retries: 5 web: image: myapp:latest depends_on: db: condition: service_healthy
pg_isready -U postgres 是 PostgreSQL 官方的就绪探测命令,比 sleep 精确得多。如果服务之间存在循环依赖——A 依赖 B、B 又依赖 A——compose 会直接拒绝启动,必须重新设计服务拆分,没有其他解法。
容器显示 unhealthy 并且被反复重启,说明健康检查命令在持续失败。三个常见原因:命令写错、镜像里没有探测工具、间隔时间不合理。
先看标准示例:
services: web: image: nginx:latest healthcheck: test: ["CMD-SHELL", "curl -f http://localhost || exit 1"] interval: 30s timeout: 10s retries: 3
每 30 秒检查一次,单次超时 10 秒,连续 3 次失败判为不健康。参数调优原则:timeout 必须小于 interval,否则检查会重叠;应用冷启动慢就把 interval 调大、retries 调多,别一上来就判死刑。
⚠️ 高频翻车点:alpine 基础镜像默认没有 curl,test 命令换成 wget -q -O - 「相关地址请参见官方文档」 || exit 1,或者在 Dockerfile 里安装 curl。镜像里没有探测命令时,健康检查永远失败,容器无限重启,日志里却看不到任何应用报错。
容器 Exited 后面跟的数字不是随机数,每种退出码都有含义。先看表:
| 退出码 | 典型含义 | 排查方向 |
|---|---|---|
| 0 | 正常退出 | 检查命令是否本就该退出,如初始化容器 |
| 1 | 应用内部错误 | docker logs 看异常堆栈 |
| 127 | 命令不存在 | 检查 command 或 entrypoint 的路径 |
| 137 | 被 SIGKILL 杀死 | 多半是内存超限,docker stats 观察 |
| 143 | 收到 SIGTERM 优雅停止 | 一般是编排操作,不是故障 |
修复步骤:第一步,docker logs 容器名 看最后几十行输出,错误原因通常就在最后;第二步,docker inspect 看 State.ExitCode 和 OOMKilled 字段,OOMKilled 为 true 时直接确认是内存问题;第三步,内存超限就在 compose 里调大限制,用 deploy 段的 resources.limits,例如 cpus: "0.5"、memory: 512m,同时用 docker stats 观察实际占用,别盲目翻倍。
升级 Docker Compose 或改动 version 字段后行为异常,属于另一类"错误"——文件语法没错,是版本语义变了。两类典型:新特性旧版本不认,报 unsupported config option;旧配置在新版本里行为变化,不报错但结果不对。
处理步骤:第一步,读官方升级文档,重点看行为变化清单;第二步,逐步升级,先升到中间版本验证,别一步跨到最新;第三步,需要迁移文件时用转换命令:docker-compose convert -f docker-compose.yml -o docker-compose-new.yml,转换后逐行检查,别直接信任输出。
另外两个背景知识值得记住:新版 Compose 已经建议去掉 version 字段,旧文件里的 version: "3.9" 在新版本里不再起实际作用;docker compose build 遇到莫名构建失败时,加 --no-cache 参数跳过缓存重试,能排除缓存污染。
网络配置错误和命令执行权限问题出现频率不算高,但遇到时容易卡住,单独拎出来说。
网络配置错误。症状是容器之间互相访问失败,三个常见成因:容器没连到预期的网络、多个网络用了相同的子网或网关、DNS 解析失败。修复顺序:docker inspect 查看容器的网络连接,确认 NetworkSettings 段的 Networks 子段里列出了目标网络;避免多个自定义网络使用相同子网和网关,创建网络时显式指定 ipam 配置;DNS 解析失败时检查 Docker 的 DNS 配置。自定义网络的正确写法是顶层声明加服务引用,两边都要写:
services: web: image: nginx:latest networks: - mynetwork db: image: postgres:latest networks: - mynetwork networks: mynetwork:
命令执行权限问题。症状是执行 docker 或 docker compose 命令直接报 permission denied,成因通常是当前用户不在 docker 用户组,或者 compose 文件本身不可读。修复:把用户加入 docker 组:
sudo usermod -aG docker $USER newgrp docker
加入用户组后需要重新登录才完全生效,临时生效可以执行 newgrp docker 切换当前会话。compose 文件权限不足时,检查文件属主和权限位,确保当前用户可读。
排查流程始终是同一个循环:复制报错、归类阶段、对应工具、修复复测。九类错误覆盖了日常部署的绝大多数场景,剩下的疑难杂症,交给第 5.3 节的日志时间线去深挖。
报错对上了号,下一步就是把日志读细。5.3 节教你把分散在各容器里的日志拼成一条时间线,用证据锁定根因。