Clarify image build semantics

This commit is contained in:
yeasy
2026-04-24 10:51:49 -07:00
parent 2e7f7d7227
commit b2218f7728
3 changed files with 39 additions and 46 deletions
+21 -30
View File
@@ -1,17 +1,17 @@
## 4.5 使用 Dockerfile 定制镜像
从刚才的 `docker commit` 的学习中我们可以了解到镜像的定制实际上就是定制每一层所添加的配置文件如果我们可以把每一层修改安装构建操作的命令都写入一个脚本用这个脚本来构建定制镜像那么之前提及的无法重复的问题镜像构建透明性的问题体积的问题就都会解决这个脚本就是 Dockerfile
从刚才的 `docker commit` 的学习中我们可以了解到镜像的定制实际上就是定制每一层所添加的配置文件如果我们可以把每一层修改安装构建操作的命令都写入一个脚本用这个脚本来构建定制镜像那么之前提及的无法重复镜像构建透明体积难以控制等问题就会更容易解决这个脚本就是 Dockerfile
Dockerfile 是一个文本文件其内包含了一条条的 **指令 (Instruction)**每一条指令构建一层因此每一条指令的内容就是描述该应当如何构建
Dockerfile 是一个文本文件其内包含了一条条的 **指令 (Instruction)**其中会修改文件系统的指令通常会创建新层 `LABEL``CMD` 这类只修改镜像元数据的指令则不会新增文件系统层每一条指令的内容都是在描述该镜像应当如何构建
### 4.5.1 使用 docker init 快速创建推荐
Docker 提供了 `docker init` 命令可以根据项目类型自动生成 Dockerfile.dockerignore compose.yaml 文件
Docker 提供了 `docker init` 命令可以根据项目类型自动生成 `Dockerfile``.dockerignore``compose.yaml` `README.Docker.md` 文件
```bash
$ docker init
```
该命令会交互式地询问项目类型支持 GoNode.jsPythonRustJavaASP.NET CorePHP 并生成符合最佳实践的配置文件对于新项目这是推荐的起步方式
该命令会交互式地询问项目类型支持 GoNode.jsPythonRustJavaASP.NET CorePHP with Apache 并生成可作为起点的配置文件对于新项目这是一个很好的起步方式但生成后的内容仍应结合项目实际情况继续调整
### 4.5.2 手动创建 Dockerfile
@@ -59,9 +59,9 @@ FROM scratch
```docker
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
```
* *exec* 格式`RUN [可执行文件, 参数1, 参数2]`这更像是函数调用中的格式
* *exec* 格式`RUN ["可执行文件", "参数1", "参数2"]`这更像是函数调用中的格式
Dockerfile 中每一个指令都会建立一层`RUN` 也不例外每一个 `RUN` 的行为就和刚才我们手工建立镜像的过程一样新建立一层在其上执行这些命令执行结束后`commit` 这一层的修改构成新的镜像
在会修改文件系统的指令里`RUN` 是最典型的一类每一个 `RUN` 的行为都可以类比为我们刚才手工建立镜像的过程先基于当前结果启动一个临时构建环境在其上执行这些命令再把这一步产生的文件系统变化保存为新的结果层
> **注意**
>
@@ -79,6 +79,11 @@ Dockerfile 中每一个指令都会建立一层,`RUN` 也不例外。每一个
```bash
$ docker build -t nginx:v3 .
```
在当前版本的 Docker `docker build` 默认会通过 Buildx 调用 BuildKit因此你更常看到的是 `[+] Building ...` 这类输出为了帮助理解每一步如何形成镜像历史下面仍展示一种较容易阅读的经典输出形式
```bash
Sending build context to Docker daemon 2.048 kB
Step 1 : FROM nginx
---> e43d811ce2f4
@@ -101,11 +106,11 @@ docker build [选项] <上下文路径/URL/->
如果注意会看到 `docker build` 命令最后有一个 `.``.` 表示当前目录 `Dockerfile` 就在当前目录因此不少初学者以为这个路径是在指定 `Dockerfile` 所在路径这么理解其实是不准确的如果对应上面的命令格式你可能会发现这是在指定 **上下文路径**那么什么是上下文呢
首先我们要理解 `docker build` 的工作原理Docker 在运行时分为 Docker 引擎 (也就是服务端守护进程) 和客户端工具Docker 的引擎提供了一组 REST API被称为 [Docker Remote API](https://docs.docker.com/develop/sdk/),而如 `docker` 命令这样的客户端工具,则是通过这组 API 与 Docker 引擎交互,从而完成各种功能。因此,虽然表面上我们好像是在本机执行各种 `docker` 功能,但实际上,一切都是使用的远程调用形式在服务端 (Docker 引擎) 完成。也因为这种 C/S 设计,让我们操作远程服务器的 Docker 引擎变得轻而易举。
首先要理解 `docker build` 的工作原理今天的 `docker build` 默认会通过 Buildx BuildKit 后端发起构建请求无论后端运行在本机还是远端位置参数指定的都是 **构建上下文**也就是构建器可以访问到的文件集合
当我们进行镜像构建的时候并非所有定制都会通过 `RUN` 指令完成经常需要将一些本地文件复制进镜像比如通过 `COPY` 指令`ADD` 指令等 `docker build` 命令构建镜像其实并非在本地构建而是在服务端也就是 Docker 引擎中构建的那么在这种客户端/服务端的架构中如何才能让服务端获得本地文件呢
当我们进行镜像构建的时候并非所有定制都会通过 `RUN` 指令完成经常需要本地文件复制进镜像比如通过 `COPY` 指令`ADD` 指令等因此构建器必须能够访问这些文件而它能访问的范围正是你传给 `docker build` 的那个上下文
这就引入了上下文的概念当构建的时候用户会指定构建镜像上下文的路径`docker build` 命令得知这个路径后会将路径下的所有内容打包然后上传给 Docker 引擎这样 Docker 引擎收到这个上下文包后展开就会获得构建镜像所需的一切文件
如果上下文是本地目录那么这个目录中的文件和子目录就会成为可用输入如果上下文是远端 Git 仓库或 tar 那么构建器会直接获取对应内容对于本地目录BuildKit 会按需读取构建过程中真正需要的文件而不是让 Dockerfile 任意访问宿主机上的任意路径
如果在 `Dockerfile` 中这么写
@@ -114,18 +119,18 @@ COPY ./package.json /app/
```
这并不是要复制执行 `docker build` 命令所在的目录下的 `package.json`也不是复制 `Dockerfile` 所在目录下的 `package.json`而是复制 **上下文 (context)** 目录下的 `package.json`
因此`COPY` 这类指令中的源文件路径都*相对路径*这也是初学者经常会问的为什么 `COPY ../package.json /app` 或者 `COPY /opt/xxxx /app` 无法工作的原因因为这些路径已经超出了上下文的范围Docker 引擎无法获得这些位置的文件如果真的需要那些文件应该将它们复制到上下文目录中去
因此`COPY` 这类指令中的源文件路径都应该以构建上下文为基准来理解对于 legacy builder `COPY ../package.json /app` 这样的写法会直接报错而在 BuildKit 前导的越界 `../` 会被剥离并重新解释为上下文内路径无论是哪种情况构建器都无法读取上下文之外的宿主机文件如果真的需要那些文件应该先把它们放进上下文目录或重新选择合适的上下文
现在就可以理解刚才的命令 `docker build -t nginx:v3 .` 中的这个 `.`实际上是在指定上下文目录`docker build` 命令会将该目录下的内容打包交给 Docker 引擎以帮助构建镜像
现在就可以理解刚才的命令 `docker build -t nginx:v3 .` 中的这个 `.`实际上是在指定上下文目录而不是单纯指定 `Dockerfile` 所在目录
如果观察 `docker build` 输出我们其实已经看到了这个发送上下文的过程
如果观察 `docker build` 的经典输出 BuildKit 输出中的 `transferring context` 提示我们其实都能看到上下文传输的过程
```bash
$ docker build -t nginx:v3 .
Sending build context to Docker daemon 2.048 kB
...
```
理解构建上下文对于镜像构建是很重要的避免犯一些不应该的错误比如有些初学者在发现 `COPY /opt/xxxx /app` 不工作后于是干脆将 `Dockerfile` 放到了硬盘根目录去构建结果发现 `docker build` 执行后在发送一个几十 GB 的东西极为缓慢而且很容易构建失败那是因为这种做法是在让 `docker build` 打包整个硬盘这显然是使用错误
理解构建上下文对于镜像构建是很重要的避免犯一些不应该的错误比如有些初学者在发现需要的文件不在上下文里后干脆把上下文切到硬盘根目录去构建这样做即使在 BuildKit 下也会让可见上下文变得过大并且在使用 `COPY . .``ADD . /app` 之类写法时仍可能触发大规模上下文传输导致构建缓慢甚至失败这显然是使用错误
一般来说应该会将 `Dockerfile` 置于一个空目录下或者项目根目录下如果该目录下没有所需文件那么应该把所需文件复制一份过来如果目录下有些东西确实不希望构建时传给 Docker 引擎那么可以用 `.gitignore` 一样的语法写一个 `.dockerignore`该文件是用于剔除不需要作为上下文传递给 Docker 引擎的
@@ -139,26 +144,12 @@ Sending build context to Docker daemon 2.048 kB
#### 直接用 Git repo 进行构建
或许你已经注意到了`docker build` 还支持从 URL 构建比如可以直接从 Git repo 中构建
或许你已经注意到了`docker build` 还支持从 URL 构建也就是直接把远端 Git 仓库作为上下文传统写法可以使用 URL 片段 `#ref:dir`例如
```bash
## $env:DOCKER_BUILDKIT=0
## export DOCKER_BUILDKIT=0
$ docker build -t hello-world https://github.com/docker-library/hello-world.git#master:amd64/hello-world
Step 1/3 : FROM scratch
--->
Step 2/3 : COPY hello /
---> ac779757d46e
Step 3/3 : CMD ["/hello"]
---> Running in d2a513a760ed
Removing intermediate container d2a513a760ed
---> 038ad4142d2b
Successfully built 038ad4142d2b
$ docker build https://github.com/user/myrepo.git#mybranch:docker
```
这行命令指定了构建所需的 Git repo并且指定分支为 `master`构建目录为 `/amd64/hello-world/`然后 Docker 就会自己去 `git clone` 这个项目切换到指定分支并进入到指定目录后开始构建
这行命令表示 Git 仓库作为构建上下文使用 `mybranch` 分支中的 `docker/` 子目录来构建在较新的 Buildx 也可以改用结构更清晰的查询参数写法例如 `?branch=mybranch&subdir=docker`
#### 用给定的 tar 压缩包构建