在上一篇我们认识了 Dockerfile 这个"镜像蓝图",现在我们来学习如何使用这个蓝图来构建(或称"编译")我们自己的 Docker 镜像。对于初学者和日常使用来说,最核心、最常用的方式就是使用 Docker 自带的docker build命令。
使用 docker build 构建镜像
docker build命令会读取你的 Dockerfile 文件,并根据其中的指令一步步执行,最终生成一个 Docker 镜像。
基本语法:
docker build [选项] 构建上下文路径
让我们来拆解一下这个命令的关键部分:
构建上下文 (Build Context)
命令最后的构建上下文路径非常重要。它告诉 Docker 命令去哪里寻找 Dockerfile 文件以及构建镜像所需要的文件(比如你想用COPY指令复制到镜像里的文件)。
- 最常见的情况是使用.(一个点),表示当前目录作为构建上下文。
- 当你运行
docker build .
时,Docker 客户端会把当前目录下的所有文件(除了被.dockerignore排除的)打包发送给 Docker 守护进程(Docker Engine)用于构建。
示例:
# 假设你的 Dockerfile 和需要的文件都在当前目录下
docker build -t myapp:1.0 .
# 这里 `.` 就是构建上下文路径,表示使用当前目录
-t 标签 (Tag)
-t 选项用来给你的镜像起一个有意义的名字和标签,方便你管理和使用。标签的格式通常是:
<仓库名>/<镜像名>:<标签>
- <仓库名>(Repository Name):通常是你的 Docker Hub 用户名或者私有仓库地址。如果是本地使用,可以省略或使用自定义名称。
- <镜像名>(Image Name):你为这个镜像起的名字,比如my-web-app。
- <标签>(Tag):通常表示版本号,比如1.0,latest。如果省略,默认为latest。
示例:
# 给镜像打上标签 myapp:1.0
docker build -t myapp:1.0 .
# 给镜像打上包含仓库名的标签 yourusername/myapp:latest
docker build -t yourusername/myapp:latest .
# 同时打多个标签
docker build -t myapp:1.0 -t myapp:latest .
Dockerfile 文件位置 (-f选项)
默认情况下,docker build会在构建上下文的根目录下查找名为Dockerfile的文件。如果你的 Dockerfile 文件名不是Dockerfile,或者不在根目录下,你需要使用-f选项来明确指定路径。
示例:
# 使用位于 configs/ 目录下的 Dockerfile.dev 文件
docker build -t myapp-dev -f configs/Dockerfile.dev .
dockerignore文件
就像 Git 有.gitignore一样,Docker 也有.dockerignore文件。这个文件放在构建上下文的根目录下,用来指定哪些文件或目录在构建时应该被忽略,不要发送给 Docker 守护进程。
为什么需要它?
- 加快构建速度:减少发送给 Docker 引擎的数据量。
- 减小镜像体积:避免将不必要的文件(如.git目录、日志文件、本地依赖node_modules等)复制到镜像中。
- 提高安全性:防止将敏感文件(如密钥、配置文件)意外打包进镜像。
.dockerignore 文件示例 (.dockerignore):
# 忽略 Git 目录
.git
# 忽略 Node.js 依赖目录
node_modules
# 忽略 Python 缓存文件
__pycache__/
*.pyc
*.pyo
# 忽略日志文件
*.log
# 忽略 IDE 配置文件
.vscode/
.idea/
# 忽略特定的配置文件
secrets.yml
强烈建议在你的项目中使用.dockerignore文件。
理解构建缓存 (Build Cache)
Docker 构建镜像是分层的,Dockerfile 中的每一条指令都会(通常)创建一个新的镜像层。为了提高构建效率,Docker 会缓存这些层。
- 缓存命中: 当 Docker 构建镜像时,它会检查当前指令及其基础层是否与之前构建时完全相同。如果相同,并且该指令引用的文件(如COPY的源文件)没有变化,Docker 就会直接使用缓存中的层(称为缓存命中),而不是重新执行该指令。
- 缓存失效: 一旦某条指令导致缓存失效(例如,COPY的源文件发生更改,或者执行RUN命令),后续的所有指令都不会再使用缓存,而是会重新执行。
优化缓存利用的关键:
- 将不经常变化的指令放在前面:例如,先安装基础依赖包 (RUN apt-get update && apt-get install -y …),再COPY应用程序代码。
- COPY和ADD指令对缓存最敏感:只要这些指令引用的文件内容或元数据发生变化,缓存就会失效。因此,应尽量晚地执行COPY应用代码的操作。
- 合并RUN指令:尽量将多个相关的RUN命令合并成一条,以减少层数(虽然 BuildKit 在这方面有所优化)。
理解并善用构建缓存是优化 Dockerfile 构建速度的关键。
使用多阶段构建 (Multi-Stage Builds)
多阶段构建是编写 Dockerfile 的一项强大技术,特别适用于需要编译步骤(如 Go, Java, C++, 或前端项目)的应用程序。它允许你在一个 Dockerfile 中定义多个构建阶段,最终只将必要的文件从中间阶段复制到最终的生产镜像中。
主要优势:
- 减小最终镜像体积: 构建工具、临时文件、开发依赖等只存在于中间阶段,不会进入最终镜像。
- 提高安全性: 最终镜像只包含运行应用所必需的文件,减少了攻击面。
- 保持 Dockerfile 简洁: 将构建逻辑和运行环境清晰分离在同一文件中。
示例结构:
# ---- 构建阶段 (Build Stage) ----
# 使用包含完整构建工具链的镜像
FROM golang:1.19 AS builder
WORKDIR /app
COPY . .
# 执行编译
RUN CGO_ENABLED=0 GOOS=linux go build -o myapp .
# ---- 生产阶段 (Production Stage) ----
# 使用轻量级的基础镜像
FROM alpine:latest
WORKDIR /root/
# 只从构建阶段复制编译好的二进制文件
COPY --from=builder /app/myapp .
# 暴露端口 (如果需要)
# EXPOSE 8080
# 设置运行命令
CMD ["./myapp"]
在这个例子中,golang:1.19镜像(包含 Go 编译器等)只用于builder阶段。最终的镜像基于轻量的alpine,只包含了编译后的myapp二进制文件。
使用构建参数 (ARG与–build-arg)
有时你需要在构建镜像时传递一些变量,例如指定应用版本、配置环境特定的值等。这可以通过ARG指令和docker build的–build-arg选项实现。
- ARG指令:在 Dockerfile 中定义一个参数及其默认值(可选)。
ARG APP_VERSION=1.0
# 你可以在后续指令中使用 ${APP_VERSION}
RUN echo "Building version ${APP_VERSION}"
- –build-arg选项:在执行docker build命令时传递参数值。
docker build --build-arg APP_VERSION=2.1 -t myapp:2.1 .
注意:ARG定义的变量只在构建过程中可用,不会持久化到最终镜像的元数据中(除非与ENV结合使用)。不要使用ARG来传递敏感信息,因为它可能会暴露在镜像历史中。对于敏感信息,应考虑使用 BuildKit 的RUN --mount=type=secret功能。
BuildKit构建镜像
你可能听说过 BuildKit。它是 Docker 新一代的构建引擎,带来了许多改进,例如:
- 更好的性能:支持并行构建阶段执行,优化了指令间的依赖关系分析。
- 更优化的缓存:不仅仅依赖本地构建缓存,还可以配置使用远程缓存后端(如 Registry)。对缓存失效的处理更智能。
- 多阶段构建优化:更高效地处理多阶段构建中的依赖和并行性。
- 丰富的构建输出格式:除了默认的 OCI/Docker 镜像格式,还可以输出为 tar 包、本地目录等。
- 新的特性:带来了许多强大的新功能,尤其是在挂载 (–mount) 方面。
启用 BuildKit 特性 (# syntax=指令):
为了确保 Docker 使用 BuildKit 的解析器并启用其高级功能或实验性语法,你可以在 Dockerfile 的第一行添加一个特殊的解析器指令:
# syntax=docker/dockerfile:1
# 或者指定更具体的版本,如 1.4,以使用特定版本的功能
# syntax=docker/dockerfile:1.4
FROM ubuntu:latest
# ... rest of your Dockerfile ...
这个# syntax=指令告诉 Docker 构建器使用哪个镜像来解析 Dockerfile。docker/dockerfile:1是稳定版本,推荐使用。添加这个指令后,你就可以在 Dockerfile 中使用 BuildKit 提供的增强功能了。
BuildKit 与docker buildx的高级功能:
BuildKit 的许多高级功能通常通过docker buildx这个 CLI 插件来使用。buildx是 Docker 官方推荐的用于利用 BuildKit 全部能力的工具。一些关键特性包括:
- 跨平台构建 (–platform):无需复杂的设置,即可轻松构建支持多种 CPU 架构(如linux/amd64,linux/arm64)的镜像。例如:
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest --push .
- 更安全的秘密管理 (RUN --mount=type=secret):允许你在构建过程中安全地挂载敏感文件(如私有仓库的 token、API 密钥),这些文件仅在RUN指令执行期间可用,不会被缓存或包含在任何镜像层中。
# syntax=docker/dockerfile:1
FROM ...
RUN --mount=type=secret,id=mysecret cat /run/secrets/mysecret
# 构建时: docker buildx build --secret id=mysecret,src=mysecret.txt .
- 更高效的缓存挂载 (RUN --mount=type=cache):允许你在构建的不同RUN指令之间或跨构建共享缓存目录,非常适合缓存包管理器(如apt,npm,pip)的下载内容。
- SSH 代理挂载 (RUN --mount=type=ssh):允许构建过程通过主机的 SSH Agent 安全地访问私有 Git 仓库等 SSH 资源。
在较新版本的 Docker Desktop 和 Docker Engine 中,BuildKit 通常是默认启用的后端构建引擎,并且docker build命令很多时候会直接利用 BuildKit 的能力。但要充分利用上述高级特性,特别是跨平台构建和多输出类型时,通常需要使用docker buildx build命令。
其他常见的镜像构建方式
虽然docker build是最基础和最常用的方式,但在某些特定场景下,开发者和运维团队也会采用其他的工具来构建容器镜像。了解这些工具可以帮助你在不同环境中选择最合适的方案:
Kaniko (Google)
Kaniko 是由 Google 开发的一个开源工具,它可以在没有 Docker 守护进程 (daemon-less)的环境中,根据 Dockerfile 构建容器镜像。它的主要特点和优势在于:
- Daemon-less:最大的优势是不需要特权 Docker 守护进程,这使得它在安全性要求较高或无法访问 Docker daemon 的环境中(如标准的 Kubernetes Pod 内)非常受欢迎。
- 在容器内构建:Kaniko 自身作为一个容器运行,并在该容器的用户空间内执行 Dockerfile 的每一条指令,包括运行命令、复制文件等,然后将文件系统更改快照为镜像层。
- 兼容 Dockerfile:设计目标是尽可能兼容标准的 Dockerfile 语法。
- CI/CD 集成:非常适合集成到 Kubernetes 原生的 CI/CD 流水线中(如 Tekton, Argo CD, Jenkins X, GitLab CI on Kubernetes)。
使用场景: 主要用于 Kubernetes 集群内的 CI/CD 流程,或者任何需要以非特权方式安全构建镜像的环境。
Buildah (Red Hat / Podman 生态)
Buildah 是另一个流行的 daemon-less 镜像构建工具,与 Podman 紧密集成,是 Red Hat 生态系统中的重要组成部分。它的特点包括:
-
Daemon-less:与 Kaniko 类似,它也不需要 Docker 守护进程。
-
多种构建方式:
- 可以通过类似docker build的方式,使用buildah bud(Build Using Dockerfile) 命令基于 Dockerfile 构建镜像。
- 提供了一系列命令行工具 (buildah from,buildah run,buildah copy,buildah config,buildah commit),允许通过脚本化的方式,更细粒度地控制镜像的创建过程,甚至可以不使用 Dockerfile。
-
OCI 标准兼容:构建的镜像是符合 OCI (Open Container Initiative) 标准的,与 Docker 兼容。
-
集成 Podman:可以无缝地与 Podman 容器运行时一起工作。
使用场景:常用于 RHEL/CentOS/Fedora 等 Linux 环境,以及偏好 Podman 生态或需要通过脚本精细控制构建过程的场景。
Cloud Native Buildpacks (CNCF)
Cloud Native Buildpacks (CNB) 提供了一种与前两者不同的思路:它旨在无需 Dockerfile 即可将应用程序源代码转化为符合 OCI 标准的容器镜像。它是一个 CNCF (Cloud Native Computing Foundation) 的项目。
- 自动检测与构建: Buildpacks 会自动检测你的应用程序类型(如 Python, Node.js, Java, Go 等),并应用一系列预定义的、可重用的构建逻辑(称为 “buildpacks”)来编译代码、安装依赖、设置运行环境等。
- 无需 Dockerfile: 对于标准的应用结构,开发者通常不需要编写和维护 Dockerfile。
- 标准化与最佳实践:Buildpacks 封装了构建特定语言应用的语言生态系统知识和最佳实践。
- 可重现性和安全性: 通过分层和元数据管理,支持镜像的快速重新构建(rebase),以便在基础镜像更新(如安全补丁)时,无需重新编译应用程序代码即可更新应用镜像。
- 实现: packCLI 是 Buildpacks 的官方命令行工具。许多 PaaS 平台(如 Heroku, Google Cloud Run/Build, VMware Tanzu)都使用了 Buildpacks 技术。
使用场景:适用于希望简化和标准化构建流程、减少维护 Dockerfile 工作量、或利用 PaaS 平台自动化构建能力的团队和组织。
选择哪种方式?
- docker build是最通用、最基础的方式,适合大多数本地开发和简单场景。
- Kaniko是在 Kubernetes CI/CD 中构建镜像的首选方案之一。
- Buildah适合需要精细控制构建过程或在 Podman 环境中工作的用户。
- Cloud Native Buildpacks则为那些希望摆脱 Dockerfile、实现高度自动化和标准化的团队提供了一个强大的选择。
掌握docker build命令的基本用法,理解构建上下文、.dockerignore、构建缓存和多阶段构建的重要性,是构建高效、安全 Docker 镜像的基础。同时,了解 BuildKit 带来的高级功能以及 Kaniko、Buildah、Cloud Native Buildpacks 等替代方案,可以让你在面对不同环境和需求时,做出更合适的选择。
引用链接
[1]Docker build 文档:https://docs.docker.com/engine/reference/commandline/build/
[2]Dockerfile 最佳实践:https://docs.docker.com/develop/develop-images/dockerfile_best-practices/
[3]Docker BuildKit 文档: https://docs.docker.com/build/buildkit/
[4]Docker buildx 文档: https://docs.docker.com/buildx/working-with-buildx/
[5]Kaniko 文档: https://github.com/GoogleContainerTools/kaniko
[6]Buildah 文档: https://buildah.io/
[7]Cloud Native Buildpacks 文档: https://buildpacks.io/