3.3 构建容器镜像
在当前目录下执行 docker build 命令时,会发生哪些事情?
- 解析 Dockerfile:读取当前目录中的 Dockerfile,解析其中的指令。
- 准备上下文目录:Docker 客户端会将当前目录作为构建上下文目录,将该目录下的所有文件和子目录打包发送给 Docker 守护进程,可以在当前目录下创建 .dockerignore 文件来排除无需的文件和目录。
- 创建构建容器:Docker 守护进程根据 FROM 指令中的基础镜像创建一个临时容器。
- 逐步执行指令:每执行一条指令,Docker 都会创建一个新的镜像层并将其缓存,如果某个步骤发生变化(如命令变更、上下文目录内文件变更等),该步骤及其后的所有步骤都会重新执行并创建镜像层。
- 生成最终镜像:所有指令都执行完毕后,所有层合并成一个最终的镜像,并为该镜像分配一个唯一的镜像 ID。
- 保存镜像:建完成的镜像会被保存到本地 Docker 镜像存储库中,可以使用
docker images命令查看。 - 输出镜像 ID:Docker 客户端输出构建的镜像 ID
下面记录一些最佳实践。
网络问题,依旧是网络问题,构建镜像时能有无数的网络问题可以阻塞,比如使用 apt 安装依赖、使用 pip、npm 下载第三方库等,总会碰到被防火墙阻拦的网址,解决办法与拉取镜像时使用代理的方法类似。
- 使用 –build-arg 传递环境变量,如:
docker build --build-arg HTTP_PROXY="http://proxy.example.com:3128 HTTP_PROXY="https://proxy.example.com:3128" .。 - 在网关配置透明代理,让网关分流网络请求解决绕过防火墙。
如果不想每次构建镜像时都传递 --build-arg,也可以修改文件: ~/.docker/config.json,添加代理服务器,如下:
{
"proxies": {
"default": {
"httpProxy": "http://proxy.example.com:3128",
"httpsProxy": "https://proxy.example.com:3129",
"noProxy": "*.test.example.com,.example.org,127.0.0.0/8"
}
}
}
这样无论是在构建镜像阶段还是运行容器阶段,都会自动注入三个代理相关的环境变量。
不过我认为这并不是最佳方案,在使用 PVE 搭建家庭服务器后,我认为有条件的话应该在网关上配置透明代理。因为除了使用 docker 构建容器外,还有利用容器构建镜像的场景,为每一个构建镜像的实例手动配置代理着实麻烦,不如在网关层面解决网络问题。
在构建镜像时,Docker 会为 Dockerfile 中的每一条指令创建一个新的镜像层并缓存,假设 Dockerfile 以及上下文目录中的文件、子目录都未发生变化,那么将会跳过构建,直接输出上一轮构建生成的镜像 ID。
下面是一些可能导致缓存失效的情况:
- 对 RUN 指令命令的任何更改
- COPY、ADD 指令拷贝到镜像中的任何文件、目录的任何更改(无论是内容更改还是权限等属性的更改)
- 从失效的镜像层开始的后续所有镜像层都将失效
为了规避缓存失效,可以调整 Dockerfile 指令的先后顺序,如:
- 前置不易发生变化的指令,如 FROM、WORKDIR、CMD、VOLUME、ENTRYPOINT 等
- 其次是安装依赖的指令,如执行
RUN apk add --no-cache python3 py3-pip安装依赖库 - 最后是编译和拷贝文件的指令,如执行 RUN 编译代码以及拷贝可执行文件
总之就是尽可能将导致镜像内容变化的指令推后,这样就可以充分利用缓存。
除了缓存外还有两个办法来加速构建,本质上也是缓存的另一种体现:
- 多阶段构建:分离依赖和最终产物,让构建阶段专用镜像编译可执行文件,再使用运行阶段专用镜像保存可执行文件,发布到生产环境
- 基础镜像:提取 Dockerfile 指令分别制作构建阶段和运行阶段使用的专用镜像,将构建阶段的内容变化控制在拷贝文件与执行编译,运行阶段的内容变化控制在使用 COPY 拷贝可执行文件。
下面是一个原始的 Dockerfile,我们会在其中使用基础镜像安装依赖,编译可执行文件,最终拷贝到 /usr/bin 目录下:
# 引用基础镜像
FROM golang:1.22.4-bookworm
# 安装依赖
RUN apt update -y && apt install -y build-essential libvirt-dev
# 复制所有文件到构建目录
COPY . /build
# 编译 Go 应用、拷贝可执行文件、清理构建阶段产生的文件
RUN cd /build && CGO_ENABLED=1 go build -o myapp . && cp -f myapp /usr/bin/myapp && rm -rf /build
# 设置工作目录
WORKDIR /app
# 设置容器启动命令
CMD ["/usr/bin/myapp"]
这是一个依赖 C 动态库调用 libvirtd 的应用,我们可以拆分出两个基础镜像,如下:
构建阶段镜像
# 引用基础镜像
FROM golang:1.22.4-bookworm
# 安装依赖
RUN apt update -y && apt install -y build-essential libvirt-dev
假设镜像标签为 golang:1.22-build
运行阶段镜像
# 引用基础镜像
FROM debian:12
# 安装依赖
RUN apt update -y && apt install -y libvirt-dev
假设镜像标签为 golang:1.22-base
调整后的 Dockerfile
# 引用构建阶段镜像
FROM golang:1.22-build AS build
# 复制所有文件到构建目录
COPY . /build
# 编译 Go 应用、拷贝可执行文件、清理构建阶段产生的文件
RUN cd /build && CGO_ENABLED=1 go build -o myapp .
# 引用运行阶段镜像
FROM golang:1.22-base
# 设置工作目录
WORKDIR /app
# 设置容器启动命令
CMD ["/usr/bin/myapp"]
# 拷贝可执行文件
COPY --from=builder /build/myapp /usr/bin/myapp