解决 Docker 构建失败:全面故障排除指南

调试由错误路径、缺失包、缓存意外、网络问题、权限或磁盘空间导致的 Docker 构建失败。

解决 Docker 构建失败:全面故障排除指南

当您将构建输出视为一份记录时,Docker 构建失败更容易修复。Docker 会告诉您哪个步骤失败、运行了什么命令以及命令打印了什么。有用的工作是找到第一个真正的错误,而不是最后一行显示构建失败的信息。

当默认输出隐藏太多信息时,使用纯文本进度运行构建:

docker build --progress=plain -t my-app:debug .

查找失败的步骤编号,例如 #8,以及旁边的指令。如果失败的指令是 COPY,您可能遇到了构建上下文或路径问题。如果是 RUN apt-get install,则存在包、网络、仓库或架构问题。如果是 RUN npm cipip install,请在更改 Docker 设置之前阅读包管理器的错误信息。

COPY failed:文件不在构建上下文中

最常见的构建错误之一也是最简单的之一:

COPY failed: file not found in build context or excluded by .dockerignore

Docker 只能复制构建上下文中的文件,构建上下文通常是 docker build 的最后一个参数:

docker build -t my-app .

这里的 . 就是上下文。子目录中的 Dockerfile 无法从该上下文外部复制 ../secret.txt。Docker 有意阻止这一点,因为构建应该可以从其上下文中重现。

检查三件事:

pwd
ls -la
docker build --progress=plain -f path/to/Dockerfile .

如果您的 Dockerfile 位于 docker/Dockerfile,但应用程序在仓库根目录,请从根目录构建并使用 -f 指向 Dockerfile:

docker build -f docker/Dockerfile -t my-app .

同时检查 .dockerignore。它可能排除了您试图复制的文件。这通常发生在 disttarget.env 或生成的文件上。如果 Dockerfile 期望某个文件,请不要忽略它,除非该文件是在构建内部创建的。

包安装失败

包管理器失败通常属于以下几类:过时的包索引、错误的包名、缺失的仓库、网络问题,或使用了错误 Linux 发行版的命令。

以下命令在 Alpine 上会失败,因为 Alpine 不使用 apt-get

FROM alpine:3.20
RUN apt-get update && apt-get install -y curl

使用基础镜像的包管理器:

FROM alpine:3.20
RUN apk add --no-cache curl

对于 Debian 或 Ubuntu 镜像,将更新和安装放在同一层:

RUN apt-get update &&     apt-get install -y --no-install-recommends curl ca-certificates &&     rm -rf /var/lib/apt/lists/*

如果 apt-get install 提示找不到包,请确认该发行版版本的包名。Debian、Ubuntu、Alpine、Fedora 和特定语言的镜像之间的包名可能不同。最小化镜像也可能缺少您认为存在的工具,例如 bashcurlgittarca-certificates

如果 HTTPS 下载因证书错误而失败,请在使用 curlwget、基于 HTTPS 的 git、npm、pip 或语言包管理器之前安装 CA 证书:

RUN apt-get update &&     apt-get install -y --no-install-recommends ca-certificates &&     rm -rf /var/lib/apt/lists/*

缓存意外

Docker 的缓存通常很有用,但它可能使损坏的构建看起来不一致。如果您怀疑缓存过时,请运行:

docker build --no-cache --progress=plain -t my-app:debug .

如果构建仅在没有缓存时失败,您可能一直依赖旧的层。如果仅在有缓存时失败,请检查生成的文件或依赖锁文件是否以 Docker 无法在您期望的位置看到的方式发生了变化。

对于依赖安装,在复制完整源代码树之前先复制锁文件:

WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .

对于 Python:

WORKDIR /app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY . .

这种模式不仅更快。它使构建失败更容易理解,因为依赖安装依赖于依赖文件,而不是每次源代码更改。

网络和注册表失败

如果 Docker 无法拉取基础镜像,构建可能在 Dockerfile 运行之前失败:

docker pull python:3.12-slim

如果失败,请先修复注册表访问。检查私有注册表的身份验证、公司代理设置、DNS 和防火墙规则。在代理后面,Docker 守护进程需要代理配置;仅在交互式 shell 中设置 HTTP_PROXY 可能不够。

对于 RUN 步骤中的下载,先从主机测试 URL,然后从相同网络路径的临时容器中测试:

curl -I https://example.com/file.tar.gz
docker run --rm curlimages/curl -I https://example.com/file.tar.gz

如果可能,避免依赖未固定的远程脚本。从移动分支 curl install.sh 的 Dockerfile 可能因为远程脚本更改而损坏。对于二进制文件,优先使用版本化下载和校验和验证:

RUN curl -fsSLo tool.tar.gz https://example.com/tool-1.2.3-linux-amd64.tar.gz &&     echo '<sha256>  tool.tar.gz' | sha256sum -c - &&     tar -xzf tool.tar.gz -C /usr/local/bin &&     rm tool.tar.gz

<sha256> 替换为项目发布页面上的实际校验和。

架构不匹配

在 Apple Silicon 或混合 CI 集群上,构建失败可能源于架构。镜像或下载的二进制文件可能是 amd64,而构建器是 arm64,反之亦然。症状包括 exec format error、缺少某个架构的包,或构建期间失败的二进制文件。

检查您的主机和目标:

docker version
docker buildx ls

在需要时为特定平台构建:

docker buildx build --platform linux/amd64 -t my-app:amd64 .

注意:涉及模拟的跨平台构建可能更慢。对于 CI,每个平台的原生构建器通常更快且更少意外。

构建期间的权限错误

当文件以意外的所有权复制、脚本不可执行,或 Dockerfile 在设置完成前切换到非 root 用户时,构建中会出现权限问题。

如果脚本因 permission denied 失败,在将假设复制到 Dockerfile 之前检查它:

ls -l scripts/start.sh

然后在 git 或镜像中修复它:

COPY scripts/start.sh /usr/local/bin/start.sh
RUN chmod +x /usr/local/bin/start.sh

如果您使用非 root 运行时用户,请在切换用户之前创建目录并设置所有权:

RUN useradd -r -u 10001 appuser && mkdir -p /app/data && chown -R appuser:appuser /app
USER appuser

COPY --chown=appuser:appuser . . 通常比以 root 身份复制然后运行广泛的递归 chown 更简洁。

磁盘空间和构建缓存清理

大型构建可能因为 Docker 主机磁盘空间不足而失败。检查 Docker 的使用情况:

docker system df

在适当时移除未使用的构建缓存:

docker builder prune

对广泛的清理命令要更加小心。docker system prune -a 会移除未使用的镜像,这可能导致大量重新拉取或破坏依赖本地镜像的工作流。在了解影响时使用它。

如果构建经常填满磁盘,更好的修复方法通常是更小的构建上下文、多阶段构建,以及避免在层中创建巨大的临时文件。在创建临时工件的同一 RUN 指令中清理它们。

交互式调试失败的 RUN 步骤

当长 RUN 行失败时,临时拆分它:

RUN apt-get update
RUN apt-get install -y --no-install-recommends packageA packageB
RUN some-command-that-fails

一旦找到失败的命令,您可以再次组合相关命令以获得更干净的镜像。

另一个有用的技巧是在已知良好的阶段停止。如果您的 Dockerfile 有多个阶段,构建一个目标:

docker build --target builder -t my-app-builder .
docker run --rm -it my-app-builder sh

从那里,您可以检查文件、手动运行失败的命令、检查环境变量,并查看文件系统实际包含的内容。

对于没有 shell 的镜像,添加一个临时调试阶段,而不是污染生产镜像:

FROM builder AS debug
RUN apt-get update && apt-get install -y --no-install-recommends bash curl

构建 --target debug,进行调查,完成后移除或忽略调试目标。

保持构建可预测

一个可靠的 Docker 构建在最好的意义上是平淡无奇的。使用版本化的基础镜像。将依赖锁文件保留在源代码控制中。避免在生产 Dockerfile 中使用 latest,除非您的流程有意针对移动标签进行重建和测试。保持 .dockerignore 紧凑。使网络下载版本化并经过验证。将频繁更改的源代码放在依赖安装之后。

当构建失败时,不要一次性重写整个 Dockerfile。识别失败的指令,使用纯文本日志重现,隔离命令,并修复最小的真正原因。这种方法更快,并且留给下一个人的 Dockerfile 仍然可以理解。