Files
docker_practice/04_image/4.5_build.md
T

184 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 4.5 使用 Dockerfile 定制镜像
从刚才的 `docker commit` 的学习中我们可以了解到镜像的定制实际上就是定制每一层所添加的配置文件如果我们可以把每一层修改安装构建操作的命令都写入一个脚本用这个脚本来构建定制镜像那么之前提及的无法重复镜像构建不透明体积难以控制等问题就会更容易解决这个脚本就是 Dockerfile
Dockerfile 是一个文本文件其内包含了一条条的 **指令 (Instruction)**其中会修改文件系统的指令通常会创建新层 `LABEL``CMD` 这类只修改镜像元数据的指令则不会新增文件系统层每一条指令的内容都是在描述该镜像应当如何构建
### 4.5.1 使用 docker init 快速创建推荐
Docker 提供了 `docker init` 命令可以根据项目类型自动生成 `Dockerfile``.dockerignore``compose.yaml` `README.Docker.md` 等文件
```bash
$ docker init
```
该命令会交互式地询问项目类型支持 GoNode.jsPythonRustJavaASP.NET CorePHP with Apache ),并生成可作为起点的配置文件对于新项目这是一个很好的起步方式但生成后的内容仍应结合项目实际情况继续调整
### 4.5.2 手动创建 Dockerfile
还以之前定制 `nginx` 镜像为例这次我们使用 Dockerfile 来定制
在一个空白目录中建立一个文本文件并命名为 `Dockerfile`
```bash
$ mkdir mynginx
$ cd mynginx
$ touch Dockerfile
```
其内容为
> **版本提示**下面示例中 `FROM nginx` 使用的是 `latest` 标签在实际应用中应使用明确的版本号 `FROM nginx:1.30`),以确保 Dockerfile 的可重现性和稳定性
```docker
FROM nginx
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
```
这个 Dockerfile 很简单一共就两行涉及到了两条指令`FROM` `RUN`
### 4.5.3 FROM 指定基础镜像
所谓定制镜像那一定是以一个镜像为基础在其上进行定制就像我们之前运行了一个 `nginx` 镜像的容器再进行修改一样基础镜像是必须指定的 `FROM` 就是指定 **基础镜像**因此一个 `Dockerfile` `FROM` 是必备的指令并且必须是第一条指令
> **版本号最佳实践** `FROM` 指令中 **务必指定具体版本号** `FROM ubuntu:24.04` `FROM python:3.12-slim`而非 `FROM ubuntu` `FROM python:latest`这样可以确保 Dockerfile 在不同时间不同环境下构建出的镜像内容一致避免因基础镜像更新导致的不可预期的变化
[Docker Hub](https://hub.docker.com/search?q=&type=image&image_filter=official) 上有非常多的高质量的官方镜像,有可以直接拿来使用的服务类的镜像,如 [`nginx`](https://hub.docker.com/_/nginx/)、[`redis`](https://hub.docker.com/_/redis/)、[`mongo`](https://hub.docker.com/_/mongo/)、[`mysql`](https://hub.docker.com/_/mysql/)、[`httpd`](https://hub.docker.com/_/httpd/)、[`php`](https://hub.docker.com/_/php/)、[`tomcat`](https://hub.docker.com/_/tomcat/) 等;也有一些方便开发、构建、运行各种语言应用的镜像,如 [`node`](https://hub.docker.com/_/node)、[`openjdk`](https://hub.docker.com/_/openjdk/)、[`python`](https://hub.docker.com/_/python/)、[`ruby`](https://hub.docker.com/_/ruby/)、[`golang`](https://hub.docker.com/_/golang/) 等。可以在其中寻找一个最符合我们最终目标的镜像为基础镜像进行定制。
如果没有找到对应服务的镜像官方镜像中还提供了一些更为基础的操作系统镜像 [`ubuntu`](https://hub.docker.com/_/ubuntu/)、[`debian`](https://hub.docker.com/_/debian/)、[`centos`](https://hub.docker.com/_/centos/)、[`fedora`](https://hub.docker.com/_/fedora/)、[`alpine`](https://hub.docker.com/_/alpine/) 等,这些操作系统的软件库为我们提供了更广阔的扩展空间。
除了选择现有镜像为基础镜像外Docker 还存在一个特殊的镜像名为 `scratch`这个镜像是虚拟的概念并不实际存在它表示一个空白的镜像
```docker
FROM scratch
...
```
如果你以 `scratch` 为基础镜像的话意味着你不以任何镜像为基础接下来所写的指令将作为镜像第一层开始存在
不以任何系统为基础直接将可执行文件复制进镜像的做法并不罕见对于 Linux 下静态编译的程序来说并不需要有操作系统提供运行时支持所需的一切库都已经在可执行文件里了因此直接 `FROM scratch` 会让镜像体积更加小巧使用 [Go 语言](https://golang.google.cn/)开发的应用很多会使用这种方式来制作镜像,这也是有人认为 Go 是特别适合容器微服务架构的语言的原因之一。
### 4.5.4 RUN 执行命令
`RUN` 指令是用来执行命令行命令的由于命令行的强大能力`RUN` 指令在定制镜像时是最常用的指令之一其格式有两种
* *shell* 格式`RUN <命令>`就像直接在命令行中输入的命令一样刚才写的 Dockerfile 中的 `RUN` 指令就是这种格式
```docker
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
```
* *exec* 格式`RUN ["可执行文件", "参数1", "参数2"]`这更像是函数调用中的格式
在会修改文件系统的指令里`RUN` 是最典型的一类每一个 `RUN` 的行为都可以类比为我们刚才手工建立镜像的过程先基于当前结果启动一个临时构建环境在其上执行这些命令再把这一步产生的文件系统变化保存为新的结果层
> **注意**
>
> 每一个 `RUN` 指令都会产生一个新的镜像层为了减少镜像体积和层数我们通常会将多个命令合并到一个 `RUN` 指令中执行
>
> 更多关于 `RUN` 指令的详细用法最佳实践 (如清理缓存使用 pipefail ) `Union FS` 的层数限制等内容请参阅 **[第七章 Dockerfile 指令详解](../07_dockerfile/README.md)** 中的 **[RUN 指令](../07_dockerfile/7.1_run.md)** 小节
要想编写优秀的 `Dockerfile`必须了解每一条指令的作用和副作用 **[第七章 Dockerfile 指令详解](../07_dockerfile/README.md)** 我们将对 `COPY``ADD``CMD``ENTRYPOINT` 等指令进行详细讲解
### 4.5.5 构建镜像
好了让我们再回到之前定制的 nginx 镜像的 Dockerfile 现在我们明白了这个 Dockerfile 的内容那么让我们来构建这个镜像吧
`Dockerfile` 文件所在目录执行
```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
Step 2 : RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
---> Running in 9cdc27646c7b
---> 44aa4490ce2c
Removing intermediate container 9cdc27646c7b
Successfully built 44aa4490ce2c
```
从命令的输出结果中我们可以清晰的看到镜像的构建过程 `Step 2` 如同我们之前所说的那样`RUN` 指令启动了一个容器 `9cdc27646c7b`执行了所要求的命令并最后提交了这一层 `44aa4490ce2c`随后删除了所用到的这个容器 `9cdc27646c7b`
这里我们使用了 `docker build` 命令进行镜像构建其格式为
```bash
docker build [选项] <上下文路径/URL/->
```
在这里我们指定了最终镜像的名称 `-t nginx:v3`构建成功后我们可以像之前运行 `nginx:v2` 那样来运行这个镜像其结果会和 `nginx:v2` 一样
### 4.5.6 镜像构建上下文
如果注意会看到 `docker build` 命令最后有一个 `.``.` 表示当前目录 `Dockerfile` 就在当前目录因此不少初学者以为这个路径是在指定 `Dockerfile` 所在路径这么理解其实是不准确的如果对应上面的命令格式你可能会发现这是在指定 **上下文路径**那么什么是上下文呢
首先要理解 `docker build` 的工作原理今天的 `docker build` 默认会通过 Buildx BuildKit 后端发起构建请求无论后端运行在本机还是远端位置参数指定的都是 **构建上下文**也就是构建器可以访问到的文件集合
当我们进行镜像构建的时候并非所有定制都会通过 `RUN` 指令完成经常还需要把本地文件复制进镜像比如通过 `COPY` 指令`ADD` 指令等因此构建器必须能够访问这些文件而它能访问的范围正是你传给 `docker build` 的那个上下文
如果上下文是本地目录那么这个目录中的文件和子目录就会成为可用输入如果上下文是远端 Git 仓库或 tar 那么构建器会直接获取对应内容对于本地目录BuildKit 会按需读取构建过程中真正需要的文件而不是让 Dockerfile 任意访问宿主机上的任意路径
如果在 `Dockerfile` 中这么写
```docker
COPY ./package.json /app/
```
这并不是要复制执行 `docker build` 命令所在的目录下的 `package.json`也不是复制 `Dockerfile` 所在目录下的 `package.json`而是复制 **上下文 (context)** 目录下的 `package.json`
因此`COPY` 这类指令中的源文件路径都应该以构建上下文为基准来理解对于 legacy builder `COPY ../package.json /app` 这样的写法会直接报错而在 BuildKit 前导的越界 `../` 会被剥离并重新解释为上下文内路径无论是哪种情况构建器都无法读取上下文之外的宿主机文件如果真的需要那些文件应该先把它们放进上下文目录或重新选择合适的上下文
现在就可以理解刚才的命令 `docker build -t nginx:v3 .` 中的这个 `.`实际上是在指定上下文目录而不是单纯指定 `Dockerfile` 所在目录
如果观察 `docker build` 的经典输出 BuildKit 输出中的 `transferring context` 提示我们其实都能看到上下文传输的过程
```bash
$ docker build -t nginx:v3 .
Sending build context to Docker daemon 2.048 kB
...
```
理解构建上下文对于镜像构建是很重要的避免犯一些不应该的错误比如有些初学者在发现需要的文件不在上下文里后干脆把上下文切到硬盘根目录去构建这样做即使在 BuildKit 下也会让可见上下文变得过大并且在使用 `COPY . .``ADD . /app` 之类写法时仍可能触发大规模上下文传输导致构建缓慢甚至失败这显然是使用错误
一般来说应该会将 `Dockerfile` 置于一个空目录下或者项目根目录下如果该目录下没有所需文件那么应该把所需文件复制一份过来如果目录下有些东西确实不希望构建时传给 Docker 引擎那么可以用 `.gitignore` 一样的语法写一个 `.dockerignore`该文件是用于剔除不需要作为上下文传递给 Docker 引擎的
那么为什么会有人误以为 `.` 是指定 `Dockerfile` 所在目录呢这是因为在默认情况下如果不额外指定 `Dockerfile` 的话会将上下文目录下的名为 `Dockerfile` 的文件作为 Dockerfile
这只是默认行为实际上 `Dockerfile` 的文件名并不要求必须为 `Dockerfile`而且并不要求必须位于上下文目录中比如可以用 `-f ../Dockerfile.php` 参数指定某个文件作为 `Dockerfile`
当然一般大家习惯性的会使用默认的文件名 `Dockerfile`以及会将其置于镜像构建上下文目录中
### 4.5.7 其它 `docker build` 的用法
#### 直接用 Git repo 进行构建
或许你已经注意到了`docker build` 还支持从 URL 构建也就是直接把远端 Git 仓库作为上下文传统写法可以使用 URL 片段 `#ref:dir`例如
```bash
$ docker build https://github.com/user/myrepo.git#mybranch:docker
```
这行命令表示 Git 仓库作为构建上下文使用 `mybranch` 分支中的 `docker/` 子目录来构建在较新的 Buildx 也可以改用结构更清晰的查询参数写法例如 `?branch=mybranch&subdir=docker`
#### 用给定的 tar 压缩包构建
```bash
$ docker build http://server/context.tar.gz
```
如果所给出的 URL 不是个 Git repo而是个 `tar` 压缩包那么 Docker 引擎会下载这个包并自动解压缩以其作为上下文开始构建
#### 从标准输入中读取 Dockerfile 进行构建
```bash
docker build - < Dockerfile
```
```bash
cat Dockerfile | docker build -
```
如果标准输入传入的是文本文件则将其视为 `Dockerfile`并开始构建这种形式由于直接从标准输入中读取 Dockerfile 的内容它没有上下文因此不可以像其他方法那样可以将本地文件 `COPY` 进镜像之类的事情
#### 从标准输入中读取上下文压缩包进行构建
```bash
$ docker build - < context.tar.gz
```
如果发现标准输入的文件格式是 `gzip``bzip2` 以及 `xz` 的话将会使其为上下文压缩包直接将其展开将里面视为上下文并开始构建