镜像的传统构建
我们随便找个Golang代码项目作为案例,来开始构建一个镜像。下面我们以我的一个实战项目开始讲解:https://gitee.com/damon_one/uranus
。
第一步:我们把项目代码克隆到本地:
git clone https://gitee.com/damon_one/uranus
第二步,书写其编译的Dockerfile:
FROM golang:1.20
WORKDIR /opt/app
COPY . .
go build -o hz-zeus ./zeus
CMD ["/opt/app/hz-zeus"]
这个 Dockerfile 描述的构建过程非常简单,我们首选 Golang:1.20 版本的镜像作为编译环境,将源码拷贝到镜像中,然后运行 go build 编译源码生成二进制可执行文件,最后配置启动命令。
第三步,构建镜像:
docker build -t hz-zeus -f Dockerfile .
这样编译构建的镜像会很大,这里就不展示最后的镜像信息了。
从上面的 Dockerfile 可以看出,我们在容器内运行了 go build -o hz-zeus ./zeus,这条命令将会编译生成二进制的可执行文件,由于编译的过程中需要 Golang 编译工具的支持,所以我们必须要使用 Golang 镜像作为基础镜像,这是导致镜像体积过大的直接原因。
既然依赖基础镜像比较大,那么我们是否可以替换为轻量级的镜像呢?发现可以将 Golang:1.20 基础镜像替换为 golang:1.20-alpine 版本。
但,这样的构建之后,发现镜像还是很大。毕竟是在镜像内镜像编译二进制文件后构建镜像。那是否可以在外部进行构建后再同步到镜像内部呢?
$ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o hz-zeus ./zeus
$ ls -lh
...
最简单的办法就是在本地先编译出可执行文件,再将它复制到一个更小体积的 ubuntu 镜像内。具体做法是,首先在本地使用交叉编译生成 Linux 平台的二进制可执行文件。接下来,使用 Dockerfile 文件构建镜像。
FROM ubuntu:latest
WORKDIR /opt/app
COPY hz-zeus ./
CMD ["/opt/app/hz-zeus"]
因为不再需要在容器里进行编译,所以我们直接引入了不包含 Golang 编译工具的 ubuntu 镜像作为基础运行环境,接下来使用 docker build 命令构建镜像。
这种构建方式生成的镜像在体积上比最初的缩小了几乎 90% 。镜像的最终大小就相当于 ubuntu:latest 的大小加上 Golang 二进制可执行文件的大小。不过,这种方式将应用的编译过程拆分到了宿主机上,这会让 Dockerfile 失去描述应用编译和打包的作用,不是一个好的实践。
多阶段构建的本质其实就是将镜像构建过程拆分成编译过程和运行过程。第一个阶段对应编译的过程,负责生成可执行文件;第二个阶段对应运行过程,也就是拷贝第一阶段的二进制可执行文件,并为程序提供运行环境,最终镜像也就是第二阶段生成的镜像。
FROM golang:1.20 as builder
WORKDIR /opt/app
COPY . .
RUN go build -o hz-zeus ./zeus
FROM ubuntu:latest
WORKDIR /opt/app
COPY --from=builder /opt/app/hz-zeus ./hz-zeus
CMD ["/opt/app/hz-zeus"]
这段内容里有两个 FROM 语句,所以这是一个包含两个阶段的构建过程。
第二阶段,它的作用是将第一阶段生成的二进制可执行文件复制到当前阶段,把 ubuntu:latest 作为运行环境,并设置 CMD 启动命令。
最后,我们执行docker build后会发现镜像大小与上面的先编译后copy到镜像种的操作生成的镜像一样大小。
到这里,对镜像大小的优化已经基本上完成了,镜像大小也在可接受的范围内。在实际的项目中,我也推荐你使用 ubuntu:latest 作为第二阶段的程序运行镜像。
在第一阶段的构建过程中,我们先是用 COPY . . 的方式拷贝了源码,又进行了编译,这会产生一个缺点,那就是如果只是源码变了,但依赖并没有变,Docker 将无法复用依赖的镜像层缓存。在实际构建过程中,你会发现 Docker 每次都会重新下载 Golang 依赖。
这就引出了另外一个构建镜像的小技巧:尽量使用 Docker 构建缓存。
要使用 Golang 依赖的缓存,最简单的办法是:先复制依赖文件,再下载依赖,最后再复制源码进行编译。基于这种思路,我们可以将第一阶段的构建修改如下:
FROM golang:1.20 as builder
WORKDIR /opt/app
COPY go.* ./
RUN go mod download
COPY . .
RUN go build -o hz-zeus ./zeus
这样,在每次代码变更而依赖不变的情况下,Docker 都会复用之前产生的构建缓存,这可以加速镜像构建过程。