我们一般都是使用dockerfile来制作镜像。但是在镜像制作中,也有着很多的注意点,比如优化构建后的体积,还要优化构建速度,时区,证书等等。
基本使用
整个文章的说明过程都是通过对实例的操作来展示,先看实例,我们假设要构建一个 http 服务。
package main
import (
"fmt"
"net/http"
"time"
"github.com/gin-gonic/gin"
)
func main() {
fmt.Println("Server Ready")
router := gin.Default()
router.GET("/", func(c *gin.Context) {
c.String(200, "hello world, this time is: "+time.Now().Format(time.RFC1123Z))
})
router.GET("/github", func(c *gin.Context) {
_, err := http.Get("https://api.github.com/")
if err != nil {
c.String(500, err.Error())
return
}
c.String(200, "access github api ok")
})
if err := router.Run(":9900"); err != nil {
panic(err)
}
}
说明:
- 这里选择 Gin 作为例子,是为了演示我们有第三方包条件下要优化构建速度
- main函数第一行打印了一行字,为了演示后面启动时遇到的一个坑
- 跟路由打印了时间,为了演示后面遇到的关于时区的坑
- 路由 github 尝试访问
https://api.github.com
,为了演示后面遇到的证书坑
这里我们可以先试一试构建后包的体积
$ go build -o server
$ ls -alh | grep server
-rwxrwxrwx 1 eyas eyas 14.6M May 29 10:26 server
14.6MB,这是一个http服务的 hello world,当然这是因为使用了 gin ,所以有些大,如果用标准包 net/http
写的 hello world,体积大概是接近 7 MB。
镜像优化过程
分层编译
直接看dockerfile,关于dockerfile的使用这边就不多说了。
FROM golang:1.14-alpine as builder
WORKDIR /usr/src/app
ENV GOPROXY=https://goproxy.cn
COPY ./go.mod ./
COPY ./go.sum ./
RUN go mod download
COPY . .
RUN go build -ldflags "-s -w" -o server
FROM scratch as runner
COPY --from=builder /usr/src/app/server /opt/app/
CMD ["/opt/app/server"]
复制代码
说明:
- 选择
golang:1.14-alpine
作为编译环境,是因为这是体积最小的golang编译环境 - 设置 GOPROXY 是为了提升构建速度
- 先复制
go.mod
和go.sum
,然后go mod download
,是为了防止每次构建都会重新下载依赖包,利用docker构建缓存提升构建速度 - go build 时加上
-ldflags "-s -w"
去除构建包的调试信息,减小go构建后程序体积,大概能减小1/4
吧 - 使用了多阶段构建,也就是
FROM XXX as xxx
,在构建程序包的时候,使用带编译环境的镜像去构建,运行的时候其实完全不需要go的编译环境,所以在运行阶段使用docker的空镜像scratch
(只有linux内核)去运行。这部是减小镜像体积最有效的方法了。
好了,下面开始构建镜像
$ docker build -t server .
...
Successfully built 8d3b91210721
Successfully tagged server:latest
到了这一步,构建成功,看看镜像大小
$ docker images
server latest 8d3b91210721 1 minutes ago 11MB
11MB,还行,现在运行一下
$ docker run -p 9900:9900 server
standard_init_linux.go:211: exec user process caused "no such file or directory"
发现启动报错了,而且main函数的第一行打印语句都没有出现,所以整个程序完全没有运行。
解决报错
上面的错误原因是缺少库依赖文件。这其实是构建的 go 程序还依赖底层的 so 库文件,不信可以在物理机编译后看看它的依赖
$ go build -o server
$ ldd server
linux-vdso.so.1 (0x00007ffcfb775000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f9a8dc47000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f9a8d856000)
/lib64/ld-linux-x86-64.so.2 (0x00007f9a8de66000)
这是因为go build 是默认启用 CGO 的,可以通过命令 go env CGO_ENABLED
查看,在 CGO 开启情况下,无论代码有没有用CGO,都会有库依赖文件,解决方法也很简单,手动指定关闭CGO就行,而且包体积并不会增加,正常还会减少。
$ CGO_ENABLED=0 go build -o server
$ ldd server
not a dynamic executable
修改dockerfile
FROM golang:1.14-alpine as builder
WORKDIR /usr/src/app
ENV GOPROXY=https://goproxy.cn
COPY ./go.mod ./
COPY ./go.sum ./
RUN go mod download
COPY . .
-RUN go build -ldflags "-s -w" -o server
+RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o server
FROM scratch as runner
COPY --from=builder /usr/src/app/server /opt/app/
CMD ["/opt/app/server"]
改动点: go build 前加了 CGO_ENABLED=0
$ docker build -t server .
...
Successfully built a81385160e25
Successfully tagged server:latest
$ docker run -p 9900:9900 server
[GIN-debug] GET / --> main.main.func1 (3 handlers)
[GIN-debug] GET /github --> main.main.func2 (3 handlers)
[GIN-debug] Listening and serving HTTP on :9900
正常启动了,我们访问一下试试,访问之前看看当前时间
$ date
Fri May 29 13:11:28 CST 2020
$ curl http://localhost:9900
hello world, this time is: Fri, 29 May 2020 05:18:28 +0000
$ curl http://localhost:9900/github
Get "https://api.github.com/": x509: certificate signed by unknown authority
发现有问题
- 当前系统时间是
13:11:28
,但是根据由显示的时间是05:11:53
,其实是docker 容器内的时区不对,默认是 0 时区,可是我们国家是 东8区 - 尝试访问
https://api.github.com/
这是 https 站点,报证书错误
解决问题
- 在容器放置根证书
- 设置容器时区
解决运行环境时区与证书问题
FROM golang:1.14-alpine as builder
WORKDIR /usr/src/app
ENV GOPROXY=https://goproxy.cn
+RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \
+ apk add --no-cache ca-certificates tzdata
COPY ./go.mod ./
COPY ./go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o server
FROM scratch as runner
+COPY --from=builder /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
+COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /usr/src/app/server /opt/app/
CMD ["/opt/app/server"]
在 builder 阶段,安装了 ca-certificates tzdata 两个库,在runner阶段,将时区配置和根证书复制了一份
$ docker build -t server .
...
Successfully built e0825838043d
Successfully tagged server:latest
$ docker run -p 9900:9900 server
[GIN-debug] GET / --> main.main.func1 (3 handlers)
[GIN-debug] GET /github --> main.main.func2 (3 handlers)
[GIN-debug] Listening and serving HTTP on :9900
访问一下试试
$ date
Fri May 29 13:27:16 CST 2020
$ curl http://localhost:9900
hello world, this time is: Fri, 29 May 2020 13:27:16 +0800
$ curl http://localhost:9900/github
access github api ok
一切正常了,看看当前镜像大小
$ docker images
server latest e0825838043d 9 minutes ago 11.3MB
才 11.3MB,已经很小了,但是,还可以更小,就是把构建后的包再压缩一次
进一步减小体积
FROM golang:1.14-alpine as builder
WORKDIR /usr/src/app
ENV GOPROXY=https://goproxy.cn
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \
- apk add --no-cache ca-certificates tzdata
+ apk add --no-cache upx ca-certificates tzdata
COPY ./go.mod ./
COPY ./go.sum ./
RUN go mod download
COPY . .
-RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o server
+RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o server &&\
+ upx --best server -o _upx_server && \
+ mv -f _upx_server server
FROM scratch as runner
COPY --from=builder /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /usr/src/app/server /opt/app/
CMD ["/opt/app/server"]
在 builder 阶段,安装了 upx ,并且go build 完成后,使用 upx 压缩了一下,执行一下构建,你会发现这个构建时间变长了,这是因为我给 upx 设置的参数是 --best
,也就是最大压缩级别,这样压缩出来的后会尽可能的小,如果嫌慢,可以降低压缩级别从 -1
到 -9
,数字越大压缩级别越高,也越慢。我使用 --best
构建完成后看看镜像体积。
$ docker build -t server .
...
Successfully built 80c3f3cde1f7
Successfully tagged server:latest
$ docker images
server latest 80c3f3cde1f7 1 minutes ago 4.26MB
这下子可小了,才 4.26MB,再去试试那两个接口,一切正常,优化到此结束,但是我们正常还是很少压缩的,一般都是解决上面几个问题。
最终的Dockerfile
FROM golang:1.14-alpine as builder
WORKDIR /usr/src/app
ENV GOPROXY=https://goproxy.cn
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \
apk add --no-cache upx ca-certificates tzdata
COPY ./go.mod ./
COPY ./go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o server &&\
upx --best server -o _upx_server && \
mv -f _upx_server server
FROM scratch as runner
COPY --from=builder /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /usr/src/app/server /opt/app/
CMD ["/opt/app/server"]
总结
要减小镜像体积,首先多阶段构建这很重要,这样就可以把编译环境和运行环境分开。
另外,选择 scratch
这个镜像其实很不明智,它虽然很小,但是它太原始了,里面什么工具都没有,程序启动后,连容器都进不去,就算进去了什么都做不了。所以就算一昧的追求尽可能小的镜像体积,也不建议选择 scratch
作为运行环境,
- 编译环境建议选择语言包,比如golang的程序一般使用golang的镜像golang:1.14-alpine作为编译的基础容器环境。
- 运行环境一般建议选择
alpine
,alpine 的镜像大小是5.61MB
这个大小其实还是镜像解压后的大小,实际上下载镜像的时候,只需要下载2.68 MB
。 - 还有个很小的镜像是
busybox
,它的体积是1.22MB
,下载705.6 KB
,有大部分的linux命令可用。
无论是 alpine 还是 busybox ,他们都会上述时区和证书问题,同样按照上面方法就能解决。