> [!info] 章节导航
> [[docker学习路径|Docker 学习索引]] · [[04-运行与管理容器|上一章]] · [[06-Docker Compose 多容器应用|下一章]]

## 5. 使用 Dockerfile 构建镜像

### 5.1 Dockerfile 基础

`Dockerfile` 是一个基于文本的脚本，它提供了有关如何构建镜像的指令集。在本快速入门指南中，存储库已包含 Dockerfile。

Dockerfile 是一个用于创建容器镜像的文本文档。它为镜像构建器提供了有关要运行的命令、要复制的文件、启动命令等的说明。

例如，以下 Dockerfile 将生成一个可运行的 Python 应用程序：



```dockerfile
FROM python:3.13
WORKDIR /usr/local/app

# Install the application dependencies
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt

# Copy in the source code
COPY src ./src
EXPOSE 8080

# Setup an app user so the container doesn't run as the root user
RUN useradd app
USER app

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"]
```



### 5.2 常用指令

`Dockerfile` 中一些最常见的指令包括：



- `FROM <image>` - 这指定了构建将扩展的基础图像。
- 
  `WORKDIR <path>` - 此指令指定“工作目录”或图像中将复制文件和执行命令的路径。
- `COPY <host-path> <image-path>` - 此指令告诉构建器从主机复制文件并将其放入容器映像中。
- 
  `RUN <command>` - 此指令告诉构建器运行指定的命令。
- 
  `ENV <name> <value>` - 此指令设置正在运行的容器将使用的环境变量。
- `EXPOSE <port-number>` - 此指令在图像上设置配置，指示图像想要公开的端口。
- 
  `USER <user-or-uid>` - 此指令为所有后续指令设置默认用户。
- `CMD ["<command>", "<arg1>"]` - 此指令设置使用此映像的容器将运行的默认命令。

正如您在前面的示例中看到的那样，Dockerfile 通常遵循以下步骤：

1. 确定您的基础图像
2. 安装应用程序依赖项
3. 复制任何相关的源代码和/或二进制文件
4. 配置最终图像



需要注意的是， `Dockerfile` *没有*文件扩展名。有些编辑器会自动为文件添加扩展名（或者提示没有扩展名）。

**使用 `docker init` 快速容器化新项目**

`docker init` 命令将分析您的项目并快速创建 Dockerfile、 `compose.yaml` 和 `.dockerignore` ，帮助您获得 开始吧。既然你在这里专门学习 Dockerfiles， 你现在不会用它。



通常，镜像是使用 Dockerfile 构建的。最基本的 `docker build` 命令可能如下所示：



```bash
docker build .
```

命令中的最后一个 `.` 提供了[构建上下文](https://docs.docker.com/build/concepts/context/#what-is-a-build-context)的路径或 URL。在此位置，构建器将找到 `Dockerfile` 和其他引用的文件。


当您运行构建时，构建器会根据需要提取基础映像，然后运行 ​​Dockerfile 中指定的指令。

使用上述命令时，镜像没有名称，但输出会提供镜像的 ID。例如，上述命令可能会产生以下输出：



```console
 exporting to image                                                                          0.1s
 => => exporting layers                                                                         0.1s
 => => writing image sha256:9924dfd9350407b3df01d1a0e1033b1e543523ce7d5d5e2c83a724480ebe8f00    0.0s
```



使用前面的输出，您可以使用引用的图像启动容器：



```console
docker run sha256:9924dfd9350407b3df01d1a0e1033b1e543523ce7d5d5e2c83a724480ebe8f00
```

.这个名字肯定不容易记住，这时标记就变得有用了。

### 5.3 标记镜像（Tagging images）

给图片添加标签是为图片赋予一个容易记住的名称的方法。然而，图片名称是有结构的。完整的图片名称结构如下：



```text
[HOST[:PORT_NUMBER]/]PATH[:TAG]
```

- `HOST` ：可选，镜像所在的镜像仓库主机名。如果未指定主机名，则默认使用 Docker 的公共镜像仓库 `docker.io` 。
- `PORT_NUMBER` ：如果提供了主机名，则为注册表端口号
- `PATH` ：镜像的路径，由斜杠分隔的组件组成。对于 Docker Hub，格式遵循 `[NAMESPACE/]REPOSITORY` ，其中 namespace 是用户或组织的名称。如果未指定 namespace，则使用 `library` ，即 Docker 官方镜像的命名空间。
- `TAG` ：自定义的、人类可读的标识符，通常用于标识镜像的不同版本或变体。如果未指定标签，则默认使用 `latest` 。



要在构建期间标记图像，请添加 `-t` 或 `--tag` 标志：



```console
docker build -t my-username/my-image .
```

如果您已经构建了镜像，则可以使用 [`docker image tag`](https://docs.docker.com/engine/reference/commandline/image_tag/) 命令向镜像添加另一个标签：



```console
docker image tag my-username/my-image another-username/another-image:v1
```

`docker image history mobywhale/concepts-build-image-demo`

`docker tag <IMAGE_ID> <仓库名>:<标签>`

### 5.4 发布镜像（Publishing images）

构建并标记好镜像后，就可以将其推送到镜像仓库了。使用 [`docker push`](https://docs.docker.com/engine/reference/commandline/image_push/) 命令即可：



```console
docker push my-username/my-image
```



docker build -t my-node-app .
你会得到一个镜像 my-node-app。
👉 这是一个单个服务的“成品”镜像。

docker compose up
👉 一条命令同时启动 web 和 redis 两个容器。
不用你手动去一个个 docker run。

| 对比项         | Dockerfile                | docker-compose.yml        |
| -------------- | ------------------------- | ------------------------- |
| 主要用途       | 构建镜像                  | 启动和管理多个容器        |
| 内容类型       | 一步步构建指令            | 各容器服务的配置          |
| 操作命令       | `docker build`            | `docker compose up`       |
| 作用范围       | 单个镜像                  | 多个容器（应用栈）        |
| 是否依赖另一方 | Compose 可引用 Dockerfile | Dockerfile 不需要 Compose |

### 5.5 构建、标记并推送镜像实战

现在您已拥有帐户，可以创建镜像仓库了。就像 Git 仓库保存源代码一样，镜像仓库也保存容器镜像。

Go to [Docker Hub](https://hub.docker.com/).
前往 [Docker Hub](https://hub.docker.com/) 。

就这样。你已经成功创建了你的第一个仓库。

此存储库目前为空。现在您可以通过向其推送镜像来修复此问题。



要创建镜像，首先需要一个项目。为了快速入门，我们将使用 上的[示例](https://github.com/dockersamples/helloworld-demo-node)Node.js 项目。此仓库包含构建 Docker 镜像所需的预构建 Dockerfile。



1. 
   使用以下命令克隆 GitHub 存储库：

   ```console
   git clone https://github.com/dockersamples/helloworld-demo-node
   ```

2. 导航到新创建的目录。

   ```console
   cd helloworld-demo-node
   ```

3. 运行以下命令来构建 Docker 镜像，将 `YOUR_DOCKER_USERNAME` 替换为您的用户名。

   ```console
   docker build -t YOUR_DOCKER_USERNAME/docker-quickstart .
   确保在 docker build 命令末尾添加点 (.)。这会告诉 Docker 在哪里找到 Dockerfile。
   ```

4. 运行以下命令列出新创建的 Docker 镜像：

```console
docker images
```

您将看到如下输出：

```console
REPOSITORY                                 TAG       IMAGE ID       CREATED         SIZE
YOUR_DOCKER_USERNAME/docker-quickstart   latest    476de364f70e   2 minutes ago   170MB
```

5. 通过运行以下命令启动容器来测试图像（将用户名替换为您自己的用户名）：

```console
docker run -d -p 8080:8080 YOUR_DOCKER_USERNAME/docker-quickstart 
```

您可以使用浏览器访问 [http://localhost:8080](http://localhost:8080/) 来验证容器是否正常工作。

6. 使用 [`docker tag`](https://docs.docker.com/reference/cli/docker/image/tag/) 命令用于标记 Docker 镜像。Docker 标签允许您标记镜像并对其进行版本控制。

```console
docker tag YOUR_DOCKER_USERNAME/docker-quickstart YOUR_DOCKER_USERNAME/docker-quickstart:1.0 
```

7. 最后，是时候使用以下命令将新构建的镜像推送到 Docker Hub 存储库了 [`docker push`](https://docs.docker.com/reference/cli/docker/image/push/) 命令：

```console
docker push YOUR_DOCKER_USERNAME/docker-quickstart:1.0
```


**VScode**

1. 右键单击 `Dockerfile` 并选择 **Build Image...** 菜单项。

2. 在出现的对话框中，输入名称 `DOCKER_USERNAME/getting-started-todo-app` ，将 `DOCKER_USERNAME` 替换为您的 Docker 用户名。

3. 按下 **Enter 键**后，您将看到一个终端出现，构建将在此进行。构建完成后，您可以随时关闭终端。

4. 通过选择左侧导航菜单中的 Docker 徽标来打开 VS Code 的 Docker 扩展。

5. 找到您创建的图像。它的名称为 `docker.io/DOCKER_USERNAME/getting-started-todo-app` 。

6. 展开镜像以查看其标签（或不同版本）。您应该会看到一个名为 `latest` 的标签，这是分配给镜像的默认标签。

7. 右键单击**最新**项目并选择**推送...** 选项。

8. 按 **Enter** 确认，然后观察镜像是否被推送到 Docker Hub。根据上传速度，推送镜像可能需要一些时间。

   上传完成后，请随时关闭终端。

### 5.6 构建缓存（Cache）

```dockerfile
FROM node:22-alpine
WORKDIR /app
COPY . .
RUN yarn install --production
CMD ["node", "./src/index.js"]
```

当您运行 `docker build` 命令创建新镜像时，Docker 会执行 Dockerfile 中的每条指令，并按照指定的顺序为每个命令创建一个层。对于每条指令，Docker 都会检查是否可以重用之前构建的指令。如果发现之前已经执行过类似的指令，Docker 无需重做，而是会使用缓存的结果。这样，您的构建过程将变得更快、更高效，从而节省您宝贵的时间和资源。

有效使用构建缓存可以让您通过重复使用先前构建的结果并跳过不必要的工作来加快构建速度。为了最大限度地利用缓存并避免耗费资源和时间的重建，了解缓存失效的工作原理至关重要。以下是一些可能导致缓存失效的情况示例：

- 对 `RUN` 指令的任何更改都会使该层失效。如果 Dockerfile 中的 `RUN` 命令有任何修改，Docker 会检测到该更改并使构建缓存失效。
- 使用 `COPY` 或 `ADD` 指令复制到镜像中的文件的任何更改。Docker 会密切关注项目目录中文件的任何更改。无论是内容更改还是权限等属性的更改，Docker 都会将这些修改视为使缓存失效的触发因素。
- 一旦某个层失效，所有后续层也会失效。如果任何先前的层（包括基础镜像或中间层）由于变更而失效，Docker 会确保依赖该层的后续层也失效。这可以保持构建过程同步，并防止出现不一致的情况。

当您编写或编辑 Dockerfile 时，请留意不必要的缓存未命中，以确保构建尽可能快速高效地运行。

### 5.7 多阶段构建



在传统的构建过程中，所有构建指令都按顺序在单个构建容器中执行：下载依赖项、编译代码以及打包应用程序。所有这些步骤最终都会出现在最终镜像中。这种方法虽然有效，但会导致镜像过于臃肿，带来不必要的负担，并增加安全风险。这时，多阶段构建就应运而生了。

多阶段构建会在 Dockerfile 中引入多个阶段，每个阶段都有特定的用途。可以将其想象成在多个不同环境中同时运行构建的不同部分。通过将构建环境与最终的运行时环境分离，可以显著减少镜像大小和攻击面。这对于构建依赖项较多的应用程序尤其有益。

建议对所有类型的应用程序进行多阶段构建。

- 对于 JavaScript、Ruby 或 Python 等解释型语言，您可以在一个阶段构建并压缩代码，然后将可用于生产环境的文件复制到更小的运行时镜像中。这可以优化镜像的部署。
- 对于 C、Go 或 Rust 等编译型语言，多阶段构建允许您在一个阶段完成编译，并将编译后的二进制文件复制到最终的运行时映像中。无需将整个编译器捆绑到最终映像中。



以下是使用伪代码简化的多阶段构建结构示例。请注意，其中包含多个 `FROM` 语句和一个新的 `AS <stage-name>` 。此外，第二阶段的 `COPY` 语句是 `--from` 上一阶段复制的。



```dockerfile
# Stage 1: Build Environment
FROM builder-image AS build-stage 
# Install build tools (e.g., Maven, Gradle)
# Copy source code
# Build commands (e.g., compile, package)

# Stage 2: Runtime environment
FROM runtime-image AS final-stage  
#  Copy application artifacts from the build stage (e.g., JAR file)
COPY --from=build-stage /path/in/build/stage /path/to/place/in/final/stage
# Define runtime configuration (e.g., CMD, ENTRYPOINT) 
```

此 Dockerfile 使用两个阶段：

- 构建阶段使用包含编译应用程序所需构建工具的基础镜像。它包含安装构建工具、复制源代码和执行构建命令的命令。
- 最后阶段使用一个较小的基础镜像来运行您的应用程序。它会从构建阶段复制已编译的构件（例如，一个 JAR 文件）。最后，它会定义用于启动应用程序的运行时配置（使用 `CMD` 或 `ENTRYPOINT` ）。

考虑以下 Dockerfile：

```dockerfile
FROM eclipse-temurin:21.0.8_9-jdk-jammy AS builder
WORKDIR /opt/app
COPY .mvn/ .mvn
COPY mvnw pom.xml ./
RUN ./mvnw dependency:go-offline
COPY ./src ./src
RUN ./mvnw clean install

FROM eclipse-temurin:21.0.8_9-jre-jammy AS final
WORKDIR /opt/app
EXPOSE 8080
COPY --from=builder /opt/app/target/*.jar /opt/app/*.jar
ENTRYPOINT ["java", "-jar", "/opt/app/*.jar"]
```


请注意，此 Dockerfile 已分为两个阶段。

- 第一阶段与之前的 Dockerfile 相同，提供用于构建应用程序的 Java 开发工具包 (JDK) 环境。此阶段名为 builder。
- 
  第二个阶段是一个名为 `final` 的新阶段。它使用更精简的 `eclipse-temurin:21.0.2_13-jre-jammy` 镜像，其中仅包含运行应用程序所需的 Java 运行时环境 (JRE)。此镜像提供的 Java 运行时环境 (JRE) 足以运行已编译的应用程序（JAR 文件）。



对于生产环境，强烈建议您使用 jlink 生成自定义的类似 JRE 的运行时。所有版本的 Eclipse Temurin 都提供 JRE 镜像，但 `jlink` 允许您创建仅包含应用程序所需 Java 模块的最小运行时。这可以显著减小最终镜像的大小并提高其安全性.



使用多阶段构建，Docker 构建使用一个基础镜像进行编译、打包和单元测试，然后使用一个单独的镜像进行应用程序运行时。因此，最终镜像的大小会更小，因为它不包含任何开发或调试工具。通过将构建环境与最终运行时环境分离，您可以显著减小镜像大小并提高最终镜像的安全性。

在多阶段 Dockerfile 中，最后一个阶段 (final) 是默认的构建目标。这意味着，如果您未在 `docker build` 命令中使用 `--target` 标志明确指定目标阶段，Docker 将默认自动构建最后一个阶段。您可以使用 `docker build -t spring-helloworld-builder --target builder .` 来仅构建带有 JDK 环境的构建器阶段。
