如何创建一个小的容器镜像
对 golang 程序上线到了容器,在使用 scratch 的时候遇到了一个小小的问题,顺手对编译做了一些整理. 以下:
编写一个简单的Go应用程序,例如 gin 的例子 **main.go
**:
1 | package main |
例子1: Dockerfile 编译
使用容器来编译运行程序
1 | # 使用官方的 Golang 镜像作为基础镜像 |
通过构建的到了容器大小为 1.16GB
1 | docker-golang-build-example1 latest cd34e318087d 40 seconds ago 1.16GB |
这个时候会容器的内容又一些大了. 可以看看 https://hub.docker.com/_/golang
golang:1.20 是基于 Debian 来作为基础镜像,本身就很大.
例子2: 使用 apline
我们可以看到提供了 alpine 作为基础镜像的包 golang:<version>-alpine
,只需要将 FROM 后面的镜像加上 alpine 即可
1 | # 使用官方的 Golang alpine 镜像作为基础镜像 |
使用 alpine 作为基础镜像会存在一些问题, 它使用了 musl libc 来代替 glibc ,小的镜像和稳定性的讨论会有一些不同的意见: https://news.ycombinator.com/item?id=10782897
镜像的大小从 1.16G 变成了 570M
例子3: 分段编译,使用 busybox
在编译过程中, go build 会拉取依赖,使用分段将编译和运行的镜像分开. golang 在运行时不需要编译环境,这里我直接使用 busybox 来作为基础镜像,同时例子2中使用 alpine 会存在一些问题,所以我们使用 busybox 的 glibc 的包.
- 编译使用例子1中的基础镜像 golang:1.20
- 运行使用 busybox:glibc https://hub.docker.com/_/busybox
1 | FROM golang:1.20 AS build |
编译后得到了 15.5M 的镜像.
例子4: 静态编译,使用 scratch
在容器镜像中有一个 0M 的基础镜像 scratch ,它是所以镜像的基础.
容器本质上是基于Linux内核的Namespace、Cgroups和Union FS等技术对进程进行封装隔离的操作系统层面的虚拟化技术。
我们可以通过静态编译的方式,基于 scratch 来运行. 构建Go应用程序,并确保它是静态链接的。可以使用以下命令来构建:
1 | CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app main.go |
在编译中添加了 -a -installsuffix cgo
两个条件,并指定了CGO_ENABLED=0
:
-a : 强制重新构建
-installsuffix cgo: 指示编译器将生成的目标文件和包安装到一个特定的目录中,以便将与C语言绑定的包(使用**cgo
**工具)与其他Go包区分开。
CGO_ENABLED=0 : cgo 工具标识为禁用
对 例子三 中的 dockerfile 做编译做调整,这个Dockerfile分为两个阶段:构建阶段和最终阶段。在构建阶段,它使用了官方的**golang
镜像来构建Go应用程序,然后在最终阶段使用了scratch
**基础镜像来创建最终的容器。
1 | # 构建阶段 |
编译后 docker-golang-build-example4 的大小从 15.5M 西江到了 11.2M, 这一点点的差值几乎就是 busybox 的存储占用.
要注意的问题
使用 scratch 镜像是一个空镜像,所以需要特别注意一些依赖和系统调用.
涉及到依赖时,需要解依赖,比如证书依赖:
COPY --from=builder /etc/ssl/certs/ /etc/ssl/certs/
涉及到系统调用,无法使用 scratch ,就需要考虑 apline 的方案.
例子5 加快编译速度
当把运行镜像的大小降下来后,还存在另一个问题,运行 go build 的时候都需要重新拉一遍golang 的环境. 可以通过挂在缓存的位置来减少应用程序编译时的时间.比如 golang 的 pkg目录 $GOPATH/pkg .在编译前提前做好 pkg 缓存,将缓存复制到目录中.
假设已经准备好了缓存目录 pkg. 在 例子4 中添加 COPY pkg/* /go/pkg/
来加快编译速度.
1 | # 构建阶段 |
这个例子可能对 golang 的意义不是特别大, 对一些依赖比较多的 node 程序的 node_modele 目录缓存就很有用.
总结
通过四种方式,不断的降低了容器镜像的大小. 但是四种方式并没有特别的优劣之分,在不同的场景下,可以通过不同的策略来做镜像编译的方式.
- 使用更小的镜像 apline 、busybox、scratch 来降低镜像大小
- 使用分阶段构建来减少
- 使用缓存来加快编译
在没有多阶段编译的时期,还有将多条命令写成一条的方式来降低容器的层数,或者借助一些工具来合并层.
以上例子放在了 github ,可以使用 docker-compose 来验证