mirror of
https://github.com/yeasy/docker_practice.git
synced 2026-08-10 16:37:34 +00:00
Clarify image build semantics
This commit is contained in:
+21
-30
@@ -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
|
||||
```
|
||||
该命令会交互式地询问项目类型(支持 Go、Node.js、Python、Rust、Java、ASP.NET Core、PHP 等),并生成符合最佳实践的配置文件。对于新项目,这是推荐的起步方式。
|
||||
该命令会交互式地询问项目类型(支持 Go、Node.js、Python、Rust、Java、ASP.NET Core、PHP 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 压缩包构建
|
||||
|
||||
|
||||
Reference in New Issue
Block a user