在我们的开发过程中,少不了与容器打交道,几乎所有常用的应用的都会提供构建好的容器镜像以便用户快速体验。特别是越来越多的团队使用k8s作为容器平台,在应用部署的过程中也就免不了要与容器打交道。
我们会通过编写Dockerfile的方式来将我们的应用打包成镜像。这个步骤很简单,大致可以分为三步:
- 选择合适的基础镜像
- 指定构建的步骤
- 设置容器启动的入口
但要想构建出一个合适的镜像,这里面还涉及了不少的知识点。
如何缩减镜像体积
对于大多数Docker的初学者来说,首要目标是能够成功构建出镜像。一般会直接选择环境较为完成的镜像作为基础镜像。例如golang编写的项目,直接选择已经配置好golang语言环境的镜像作为基础镜像。大致的Dockerfile与下面的类似。
FROM golang:1.17
WORKDIR /opt/app
COPY . .
RUN go build -o example
CMD ["/opt/app/example"]
但是使用这个Dockerfile构建的镜像大小是很惊人的,能够达到900M左右。如何我们的项目编译出来的二进制文件本身只有几M的话,那么这个镜像的大小就显得太过浪费了。
Dockerfile构建出的镜像的大小,很多程度上依赖于基础镜像的大小。为了编译的方便,我们选择了具有完善编译环境的镜像作为基础镜像,这就导致了镜像的体积过大。
但是对于二进制文件的运行来说,只需要满足运行依赖就可以了,完全不需要编译依赖。所以我们可以考虑将编译与运行分开,让可执行文件在最小的基础镜像中运行即可。
我们可以选择在本地构建出可执行文件,再拷贝到更小体积的ubuntu镜像内。但是这样镜像的构建过程就依赖于宿主机了,所以这并不是推荐的做法。
更加推荐的做法是使用多阶段构建。
多阶段构建
多阶段构建的本质就是上面提到的,将镜像的构建过程拆分为编译过程和运行过程。第一个阶段负责构建出可执行文件;第二个阶段拷贝可执行文件到一个更小的镜像中负责提供运行环境。
一个多阶段构建的例子如下:
# Step 1: build golang binary
FROM golang:1.17 as builder
WORKDIR /opt/app
COPY . .
RUN go build -o example
# Step 2: copy binary from step1
FROM ubuntu:latest
WORKDIR /opt/app
COPY --from=builder /opt/app/example ./example
CMD ["/opt/app/example"]
在第一个阶段我们使用了具有go语言编译环境的golang:1.17作为基础镜像来构建出可执行文件。在第二阶段我们选择了ubuntu:latest作为基础镜像,这个镜像的大小要更小一些,我们从第一个阶段的镜像中拷贝可执行文件到这个镜像中。
利用上面的Dockerfile构建出最终的镜像,我们可以发现镜像的大小主要由第二阶段的基础镜像的大小决定,要比使用golang:1.17作为基础镜像的大小要小很多。
基础镜像的选择
前面我们利用多阶段构建,选择更小的基础镜像有效压缩了镜像的体积。
更为极端一些,我们可以将第二阶段的基础镜像选择为alpine和scratch镜像,让构建的镜像大小几乎与可执行文件大小一致。但是因为这两种镜像太过精简,所以作为初学者也并不推荐。
大多数镜像都会提供slim版本,这个版本大都基于Ubuntu、Debian、Centos构建,并且删除了一些不必要的系统应用,所以体积相对较小,非常适合作为首选镜像。
复用构建缓存
这就要求我们编写Dockerfile的时候要有所注意了。对Docker中的层的概念有所了解的应该都知道,Dockerfile中的每一行都对应一个层,如果一个层不变的话,是可以被其它Docker镜像复用的。
所以我们编写Dockerfile的时候,尽量让尽可能多的层保持不变
FROM golang:1.17 as builder
WORKDIR /opt/app
COPY go.* ./
RUN go mod download
COPY . .
RUN go build -o example
在上面的Dockerfile中,我们先只拷贝了go.mod, go.sum等文件,然后下载go包的依赖,然后再拷贝源码进行构建。这样就可以在源码改变而依赖不变的情况下,复用构建缓存,而不必每次构建都取下载go的依赖包了。
多平台构建
使用docker build命令构建镜像,会默认构建本机对应平台的镜像。当然,在大多数情况下也是适用的。
但是如果使用不同的平台设备启动这个镜像,可能会遇到问题。为了保证“一次构建,到处运行”的目标能够实现,Docker提供了构建多平台镜像的方法,我们一次构建出多个平台可用的镜像,不同平台运行时拉去自己对应的镜像即可。
Docker多平台镜像构建的方法叫做:buildx。我们按照以下的步骤来实现多平台构建。
- 初始化buildx。
docker buildx create --name mybuilder
。 - 设置默认的构建器为我们初始化的mybuilder。
docker buildx use mybuilder
。 - 初始化构建器。
docker buildx inspect --bootstrap
。 - 改写Dockerfile文件
# syntax=docker/dockerfile:1
FROM --platform=$BUILDPLATFORM golang:1.18 as build
ARG TARGETOS TARGETARCH
WORKDIR /opt/app
COPY go.* ./
RUN go mod download
COPY . .
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /opt/app/example .
FROM ubuntu:latest
WORKDIR /opt/app
COPY --from=build /opt/app/example ./example
CMD ["/opt/app/example"]
最后使用docker buildx build --platform linux/amd64,linux/arm64 ...
就可以构建出两个平台的镜像了。
在dockerfile中,我们增加了--platform=$BUILDPLATFORM
参数,Docker会根据命令传入的平台,分别为这些平台进行构建过程。使用ARG TARGETOS TARGETARCH
声明的两个内置变量,TARGETOS代表系统,如linux,TARGETARCH代表平台,例如Amd64,会用于交叉编译生成二进制文件。