Docker 构建是每个开发人员都需要理解的基本概念。无论您是要对第一个应用程序进行容器化,还是优化现有的 Docker 工作流程,了解 Docker 构建上下文和 Docker 构建架构对于创建高效、可扩展的容器化应用程序都至关重要。
本综合指南涵盖了从基本概念到高级优化技术的所有内容,帮助您避免常见的陷阱并构建更好的 Docker 镜像。
目录
- [什么是 Docker Build?](#什么是 Docker Build?)
- [Docker 构建架构:工作原理](#Docker 构建架构:工作原理)
- [Docker 构建功能](#Docker 构建功能)
- [Docker 构建上下文](#Docker 构建上下文)
- [Docker 构建上下文的类型](#Docker 构建上下文的类型)
- [常见的 Docker 构建错误(以及如何修复它们)](#常见的 Docker 构建错误(以及如何修复它们))
- 如何优化和监控构建性能
- [Docker 构建性能的最佳实践](#Docker 构建性能的最佳实践)
- [解决 Docker 构建问题](#解决 Docker 构建问题)
- 结论
什么是 docker build
Docker 构建是从 Dockerfile 和一组称为构建上下文 的文件创建 Docker 镜像的过程。运行 docker build
时,实际上是在指示 Docker 执行以下操作:
-
阅读 Dockerfile 说明
-
收集必要的文件(构建上下文)
-
逐步执行每条指令
-
创建最终的 Docker 镜像
可以将其想象成遵循食谱:Dockerfile 是您的食谱,而构建上下文包含您可能需要的所有成分。
Docker 构建架构:工作原理
cker 构建架构:工作原理
Docker Build 采用客户端-服务器架构,其中两个独立的组件( Buildx 和 BuildKit )协同工作来构建 Docker 镜像。这与许多人对 Docker 工作原理的理解不同,它并非一个包罗万象的单体程序。
什么是 Buildx(客户端)?
无论何时使用 Docker 构建,Buildx 都是您直接与之交互的用户界面。当您在终端中输入 docker build .
时,实际上是在与 Buildx 进行通信,它充当您和实际构建引擎之间的中介。
Buildx 的主要工作:
-
解释你的构建命令和选项
-
向 BuildKit 发送结构化的构建请求
-
管理多个 BuildKit 实例(构建器)
-
处理身份验证和机密
-
向您显示构建进度
什么是 BuildKit(服务器/构建器)
BuildKit 充当实际的构建引擎,在 Docker 构建过程中执行所有繁重的工作。这个强大的后端组件接收来自 Buildx 的结构化构建请求,并立即开始逐行读取和解释您的 Dockerfile。
BuildKit 的主要工作:
-
接收来自 Buildx 的构建请求
-
读取并解释 Dockerfile
-
逐步执行构建指令
-
管理构建缓存和层
-
仅向客户端请求所需的文件
-
创建最终的 Docker 镜像
他们如何沟通
以下是运行 docker build .
时发生的情况:

当您运行 docker build
时,该命令将使用 BuildKit 启动一个多步骤过程(如上图所示)。
首先,它会发送一个包含 Dockerfile、构建参数、导出选项和缓存选项的构建请求。然后,BuildKit 会智能地在需要时仅请求所需的文件,首先从 package.json
开始,然后运行 npm install
来安装依赖项。
完成后,它会请求包含应用程序代码的 src/
目录,并使用 COPY
命令将这些文件复制到图像中。
所有构建步骤完成后,BuildKit 会返回已完成的镜像。您也可以选择将此镜像推送到容器注册表进行分发或部署。
这种按需文件传输方法是 BuildKit 的关键优化之一:它不是预先发送整个构建上下文,而是在每个构建步骤需要时才请求特定文件,从而使构建过程更加高效。
关键沟通细节
构建请求包含:
{ "dockerfile": "FROM node:18\nWORKDIR /app\n...", "buildArgs": {"NODE_ENV": "production"}, "exportOptions": {"type": "image", "name": "my-app:latest"}, "cacheOptions": {"type": "registry", "ref": "my-app:cache"} }
资源请求:
-
BuildKit 询问:"我需要
./package.json
中的文件" -
Buildx 响应:发送实际文件内容
-
BuildKit 询问:"我需要目录
./src/
" -
Buildx 响应:发送该目录中的所有文件
为什么存在这种架构
1. 效率
旧版 Docker 构建器有一个重大缺陷:它总是预先复制整个构建上下文,而不管实际需要什么。即使你的 Dockerfile 只使用了几个文件,Docker 在开始构建之前也会传输数百兆字节的数据。
BuildKit 通过按需文件传输解决了这个问题。它在每个步骤中只请求特定的文件。
# Old Docker Builder (legacy)
# Always copied ENTIRE context upfront
$ docker build .
Sending build context to Docker daemon 245.7MB # Everything!
# New BuildKit Architecture
# Only requests files when needed
$ docker build .
#1 [internal] load build definition from Dockerfile 0.1s
#2 [internal] load .dockerignore 0.1s
#3 [1/4] FROM node:18 0.5s
#4 [internal] load build context 0.1s
#4 transferring context: 234B # Only package.json initially!
#5 [2/4] WORKDIR /app 0.2s
#6 [3/4] COPY package*.json ./ 0.1s
#7 [4/4] RUN npm install 5.2s
#8 [internal] load build context 0.3s
#8 transferring context: 2.1MB # Now requests src/ files
#9 [5/4] COPY src/ ./src/ 0.2s
2.可扩展性
客户端-服务器架构支持可扩展性。多个 Docker CLI 客户端可以连接到同一个 BuildKit 实例,并且 BuildKit 可以在远程服务器(而非本地机器)上运行。这意味着您可以在云服务器上执行构建,同时通过笔记本电脑进行控制。团队还可以为不同的团队或用途部署多个 BuildKit 实例,从而从个人开发者扩展到大型企业。
3. 安全
通过仅在明确需要时请求敏感文件,安全性得到了提升。BuildKit 永远不会访问 Dockerfile 未引用的文件,从而减少了攻击面。它还通过单独的安全通道处理凭证,而不是将其与构建上下文混合,从而防止机密信息被嵌入到镜像层中或在构建日志中暴露。
真实世界的例子
让我们一步步回顾一下典型的构建过程。完整代码可以在这里找到: 02-python-cache 。
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY src/ ./src/
COPY main.py .
CMD ["python", "main.py"]
让我们看看这里到底发生了什么:
-
运行
docker build .
-
Buildx 对 BuildKit 说:
"Here's a build request with this Dockerfile"
-
BuildKit 流程 :
FROM python:3.9-slim
- 无需客户端文件,拉取基础镜像
-
BuildKit 流程 :
COPY requirements.txt .
-
BuildKit 向 Buildx 发送消息:"我需要
requirements.txt
" -
Buildx 到 BuildKit:发送文件内容
-
-
BuildKit 进程 :
RUN pip install -r requirements.txt
- 无需客户端文件,在容器内运行
-
BuildKit 流程 :
COPY src/ ./src/
-
BuildKit 致 Buildx:"我需要
src/
目录中的所有文件" -
Buildx 到 BuildKit:发送 src/ 中的所有文件
-
-
BuildKit 流程 :
COPY main.py .
-
BuildKit 对 Buildx 说:"我需要
main.py
" -
Buildx 到 BuildKit:发送文件
-
-
BuildKit 向 Buildx 发送消息:"构建完成,这是你的图像"
从图中可以看出,BuildKit 仅在需要时请求所需的内容。而不是整个上下文:
` my-app/ ├── src/ # ← Only loaded when COPY src/ runs ├── tests/ # ← Never requested (not in Dockerfile) ├── docs/ # ← Never requested ├── node_modules/ # ← Never requested (in .dockerignore) ├── requirements.txt # ← Loaded early (first COPY) └── main.py # ← Loaded later (second COPY)`
Docker 构建功能
命名上下文
👉 演示项目: 07-named-contexts
命名上下文允许您在构建过程中包含来自多个来源的文件,同时保持它们在逻辑上相互独立。当您在构建过程中需要来自不同目录或存储库的文档、配置文件或共享库时,此功能非常有用。
`# Build with additional named context docker build --build-context docs=./documentation . `
`# Use named context in Dockerfile FROM alpine COPY . /app # Mount files from named context RUN --mount=from=docs,target=/docs \ cp /docs/manual.pdf /app/ `
建立秘密
👉 演示项目: 06-build-secrets
构建密钥允许您将敏感信息(例如 API 密钥或密码)传递到构建过程中,而无需将其包含在最终镜像或构建历史记录中。这些密钥会在特定的 RUN
命令期间临时挂载,并且绝不会存储在镜像层中。
`# Pass secret to build echo "api_key=secret123" | docker build --secret id=apikey,src=- . `
`# Use secret in Dockerfile FROM alpine RUN --mount=type=secret,id=apikey \ export API_KEY=$(cat /run/secrets/apikey) && \ curl -H "Authorization: $API_KEY" https://api.example.com/data`
Docker 构建上下文
什么是构建上下文?
构建上下文是 Docker 在构建过程中可以访问的文件和目录的集合。这就像在开始烹饪之前,将所有食材都摆放在台面上一样。
`docker build [OPTIONS] CONTEXT ^^^^^^^ This is your build context `
为什么构建上下文很重要
-
安全性:在构建期间只能访问上下文中的文件
-
性能:大型上下文会减慢构建速度
-
功能:您的 Dockerfile 只能从上下文中复制/添加文件
-
效率:了解上下文有助于您构建更快、更精简的图像
Docker 构建上下文的类型
1. 本地目录上下文(最常见)
👉 请参阅此处的代码: 01-node-local-context
在 90% 的情况下,您都会使用这个命令 -- 指向您机器上的一个文件夹:
`# Use current directory docker build . # Use specific directory docker build /path/to/my/project # Use parent directory docker build .. `
项目结构示例:
`my-webapp/ ├── src/ │ ├── index.js │ └── utils.js ├── public/ │ ├── index.html │ └── styles.css ├── package.json ├── package-lock.json ├── Dockerfile ├── .dockerignore └── README.md `
对应的 Dockerfile:
`FROM node:18-alpine WORKDIR /app # Copy package files first for better layer caching COPY package*.json ./ RUN npm ci --only=production # Copy application source COPY src/ ./src/ COPY public/ ./public/ EXPOSE 3000 CMD ["node", "src/index.js"] `
2. 远程 Git 仓库上下文
您可以直接从 Git 存储库构建,而无需在本地克隆:
`# Build from GitHub main branch docker build https://github.com/<username>/project.git # Build from specific branch docker build https://github.com/<username>/project.git#develop # Build from specific directory in repo docker build https://github.com/<username>/project.git#main:docker # Build with authentication docker build --ssh default git@github.com:<username>/private-repo.git `
这有各种情况,如 CI/CD 管道、构建开源项目、确保从源代码控制进行干净的构建、自动化部署等等。
3. 远程 Tarball 上下文
您还可以从托管在 Web 服务器上的压缩档案构建。远程 tarball 是一个 .tar.gz
或类似的压缩档案文件,可通过 HTTP/HTTPS 访问。当您的源代码打包并托管在 Web 服务器、构件库或 CDN 上时,这非常有用。Docker 会自动下载并提取档案,并将其内容作为构建上下文。
这种方法非常适合集中存储构建工件的 CI/CD 管道,或者当您想要从已发布的代码版本构建图像而无需克隆整个存储库时。
`# Build from remote tarball docker build http://server.com/context.tar.gz # BuildKit downloads and extracts automatically docker build https://example.com/project-v1.2.3.tar.gz `
4. 空上下文(高级)
当你不需要任何文件时,你可以直接通过管道传输 Dockerfile:
`# Create image without file context docker build -t hello-world - <<EOF FROM alpine:latest RUN echo "Hello, World!" > /hello.txt CMD cat /hello.txt EOF`
常见的 Docker 构建错误(以及如何修复它们)
错误 1:错误的上下文目录
👉 转载于: 04-wrong-context
当您从错误的目录运行 docker build
时,就会发生此错误,导致构建上下文与 Dockerfile 所期望的不同。
在示例中,从 /projects/
目录运行 docker build frontend/
意味着上下文是 /projects/frontend/
,但 Dockerfile 尝试访问 ../shared/utils.js
,而该文件不在此上下文中。Docker 只能访问构建上下文内的文件,因此任何引用构建上下文外文件的尝试都将失败。
`# Project structure /projects/ ├── frontend/ │ ├── Dockerfile │ ├── src/ │ └── package.json └── shared/ └── utils.js # WRONG - Running from projects directory docker build frontend/ # This won't work if Dockerfile tries to COPY ../shared/utils.js `
如何修复错误的上下文目录:
关键是将构建上下文与 Dockerfile 的需求相一致。
-
选项 1 会更改你的工作目录,使其上下文与 Dockerfile 的预期相符。你从
frontend/
目录内部运行构建,并将该目录设置为上下文根目录。 -
选项 2 会将您保留在父目录中,但会将其显式设置为上下文(
.
参数),并使用-f
标志告诉 Docker 在哪里找到 Dockerfile。现在,frontend/
和shared/
都可以访问,因为它们都在/projects/
上下文中。
`# Option 1: Run from correct directory cd frontend docker build . # Option 2: Use parent directory as context docker build -f frontend/Dockerfile . `
错误2:包含大量文件
👉 使用 .dockerignore
优化版本: 05-dockerignore-optimization
当您的构建上下文包含较大的、不必要的文件,从而减慢构建过程时,就会发生此错误。
Docker 必须在启动前将整个上下文传输到构建守护进程,因此包含诸如 node_modules
(可能高达数百 MB)、git 历史记录、构建工件、日志和数据库转储等文件会使构建速度极其缓慢。这些文件在最终镜像中很少用到,因此应该排除。
`# This context includes everything! my-app/ ├── node_modules/ # 200MB+ ├── .git/ # Version history ├── dist/ # Built files ├── logs/ # Log files ├── temp/ # Temporary files ├── database.dump # 1GB database backup └── Dockerfile `
如何修复 Docker 构建大量文件:
使用 .dockerignore
排除不必要的文件,显著减少上下文大小和构建时间。我们将在下面详细讨论这一点。
错误3:低效的层缓存
👉 请参阅此处的良好实践代码: 02-python-cache
这个错误会在运行开销较大的操作(例如 npm install
)之前复制频繁更改的文件(例如源代码),从而浪费 Docker 的层缓存系统。当你修改源代码时,Docker 会使该层及所有后续层的缓存失效,从而强制再次运行 npm install
,即使依赖项并未更改。这可能会导致 5 秒的构建耗时变成 5 分钟。
`# BAD - Changes to source code rebuild npm install FROM node:18 COPY . /app WORKDIR /app RUN npm install CMD ["npm", "start"] `
如何修复 docker build 低效的层缓存:
先复制依赖文件,安装依赖项,再复制源代码。这样, npm install
只会在 package.json
实际更改时运行:
`# GOOD - npm install only rebuilds when package.json changes
FROM node:18
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["npm", "start"]`
如何优化和监控构建性能
了解构建性能指标有助于您识别瓶颈并衡量改进。
如何使用 .dockerignore 优化 Docker 构建
.dockerignore
文件是实现更快、更安全构建的秘密武器。它告诉 Docker 哪些文件需要从构建上下文中排除。
创建 .dockerignore 模式
在项目根目录中创建一个 .dockerignore
文件。其语法与 .gitignore
类似,您可以使用通配符 ( *
)、匹配特定文件扩展名 ( *.log
)、排除整个目录 ( node_modules/
),或使用否定模式 ( !important.txt
) 来包含原本会被排除的文件。每一行代表一个模式,注释以 #
开头。
.dockerignore 文件的示例:
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Build outputs
dist/
build/
*.tgz
# Version control
.git/
.gitignore
.svn/
# IDE and editor files
.vscode/
.idea/
*.swp
*.swo
*~
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Logs and databases
*.log
*.sqlite
*.db
# Environment and secrets
.env
.env.local
.env.*.local
secrets/
*.key
*.pem
# Documentation
README.md
docs/
*.md
# Test files
test/
tests/
*.test.js
coverage/
# Temporary files
tmp/
temp/
*.tmp
测量构建性能
分析构建时间
了解构建耗时之处有助于识别瓶颈并发现优化机会。详细的进度输出会显示每个构建步骤的耗时、缓存命中/未命中情况以及资源使用情况。
`# Enable BuildKit progress output DOCKER_BUILDKIT=1 docker build --progress=plain . # Use buildx for detailed timing docker buildx build --progress=plain . `
分析上下文传输
监控上下文传输时间,了解构建上下文大小如何影响整体性能。分析哪些目录贡献最大,有助于实现 .dockerignore
优化。
`# Measure context transfer time time docker build --no-cache . # Profile context size by directory du -sh */ | sort -hr `
测量 .dockerignore 的影响
在 .dockerignore
之前,您会注意到 transfering context
大小在 15.2 秒内为 245.7MB:
`$ docker build . #1 [internal] load build context #1 transferring context: 245.7MB in 15.2s `
添加 .dockerignore 文件后,上下文在 0.3 秒内减少到 2.1MB:
`$ docker build . #1 [internal] load build context #1 transferring context: 2.1MB in 0.3s `
结果:上下文大小减少 99%,上下文传输速度提高 50 倍!
Docker 构建性能的最佳实践
本指南介绍了多种优化技巧。以下简要回顾一下关键做法,并附上一些其他策略:
-
层缓存(在错误 3 中介绍):在源代码之前复制依赖文件以最大限度地提高缓存重用率。
-
使用 .dockerignore (在错误 2 中介绍):排除不必要的文件以减少上下文大小并提高构建速度。
-
选择正确的上下文(前面介绍过):根据您的用例选择适当的上下文类型(本地、Git、tarball)。
现在让我们讨论一些可以提高性能的方法:
使用多阶段构建
👉 演示项目: 03-multistage-node
多阶段构建功能允许您使用一个镜像来构建/编译应用程序,并使用另一个较小的镜像来运行它。通过从生产镜像中排除构建工具、源代码和其他不必要的文件,这可以显著减少最终镜像的大小。
`# Build stage FROM node:18 AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build # Production stage FROM nginx:alpine COPY --from=builder /app/dist /usr/share/nginx/html EXPOSE 80 CMD ["nginx", "-g", "daemon off;"] `
使用特定的基础镜像
像 ubuntu:latest
的通用基础镜像包含许多您不需要的软件包,这会导致镜像体积更大、下载速度更慢。而像 node:18-alpine
或 distroless 这样的特定镜像则只包含应用程序运行所需的软件包。
`# Large base image FROM ubuntu:latest # Smaller, more specific base image FROM node:18-alpine # Even smaller distroless image FROM gcr.io/distroless/nodejs18-debian11 `
组合 RUN 命令
每个 RUN
命令都会在镜像中创建一个新层。多个 RUN
命令会创建多个层,从而增加镜像大小。将多个命令组合成一个 RUN
指令只会创建一个层,并且您可以在同一步骤中清理临时文件。
`# Creates multiple layers RUN apt-get update RUN apt-get install -y curl RUN apt-get clean # Single layer RUN apt-get update && \ apt-get install -y curl && \ apt-get clean && \ rm -rf /var/lib/apt/lists/*`
解决 Docker 构建问题
问题:"复制失败:没有此文件或目录"
问题 :文件不在构建上下文中
问题出在哪里 :Docker 只能访问构建上下文(即您在 docker build
中指定的目录)内的文件。如果您的 Dockerfile 尝试 COPY
上下文目录中不存在的文件,构建将失败。这种情况通常发生在从错误的目录运行构建命令,或者文件路径相对于上下文根目录不正确时。
解决方案:
`# Check what's in your context ls -la # Verify file path relative to context docker build -t debug . --progress=plain `
问题:"Docker Build 极其缓慢"
问题 :大型构建上下文
问题所在 :Docker 必须在构建开始之前将整个构建上下文传输到 BuildKit 守护进程。如果上下文包含大文件、类似 node_modules
的目录或不必要的文件,则此传输可能需要几分钟而不是几秒钟。上下文越大,构建速度越慢。
解决方案:
`# Check context size du -sh . # Add more patterns to .dockerignore echo "large-directory/" >> .dockerignore echo "*.zip" >> .dockerignore `
问题:"无法找到指定的 Dockerfile"
问题 :Dockerfile 不在上下文根中
问题出在哪里 :默认情况下,Docker 会在构建上下文的根目录中查找名为 Dockerfile
的文件。如果您的 Dockerfile 位于子目录中或使用其他名称,Docker 将无法找到它。这在 Monorepo 设置中很常见,因为 Dockerfile 会组织在单独的文件夹中。
解决方案:
`# Specify Dockerfile location docker build -f path/to/Dockerfile . # Or move Dockerfile to context root mv path/to/Dockerfile . `
问题:"未更改文件的缓存未命中"
问题 :文件时间戳或权限已更改
问题所在:Docker 的层缓存依赖于文件校验和及元数据。即使文件内容未发生更改,不同的时间戳或权限也可能导致缓存未命中,从而强制进行不必要的重建。这种情况通常发生在 git 操作、文件系统操作或在系统之间复制文件之后。
解决方案:
`# Check file modifications git status # Reset timestamps git ls-files -z | xargs -0 touch -r .git/HEAD `
结论
了解 Docker 构建上下文和架构对于实现更快的构建至关重要。本文介绍了各种技术,例如优化的上下文和缓存策略、通过高效的分层和多阶段构建创建更小的镜像、通过适当的密钥处理和最小化攻击面来维护更高的安全性,以及如何通过更快的迭代周期提供更好的开发者体验。
结论
了解 Docker 构建上下文和架构对于实现更快的构建至关重要。本文介绍了各种技术,例如优化的上下文和缓存策略、通过高效的分层和多阶段构建创建更小的镜像、通过适当的密钥处理和最小化攻击面来维护更高的安全性,以及如何通过更快的迭代周期提供更好的开发者体验。
👉完整的代码示例可在 GitHub 上找到: Docker 构建架构示例