构建 Golang 应用最小 Docker 镜像

我一般使用docker运行个人 golang 程序,在这里分享一下我构建 docker 镜像的经验。我构建 docker 镜像不只优化构建后的体积,还要优化构建速度。linux

示例应用

首先贴出代码例子,咱们假设要构建一个 http 服务git

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)
	}
}


复制代码

说明:github

  • 这里选择 Gin 做为例子,是为了演示咱们有第三方包条件下要优化构建速度
  • main函数第一行打印了一行字,为了演示后面启动时遇到的一个坑
  • 跟路由打印了时间,为了演示后面遇到的关于时区的坑
  • 路由 github 尝试访问 https://api.github.com,为了演示后面遇到的证书坑

这里咱们能够先试一试构建后包的体积golang

$ 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 MBdocker

Dockerfile 的进化

版本一,初步优化

先看看第一个版本api

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.modgo.sum ,而后 go mod download,是为了防止每次构建都会从新下载依赖包,利用docker构建缓存提高构建速度
  • go build 时加上 -ldflags "-s -w" 去除构建包的调试信息,减少go构建后程序体积,大概能减少 1/4
  • 使用了多阶段构建,也就是 FROM XXX as xxx ,在构建程序包的时候,使用带编译环境的镜像去构建,运行的时候其实彻底不须要go的编译环境,因此在运行阶段使用docker的空镜像 scratch 去运行。这部是减少镜像体积最有效的方法了。

好了,下面开始构建镜像bash

$ docker build -t server .
...
Successfully built 8d3b91210721
Successfully tagged server:latest
复制代码

到了这一步,构建成功,看看镜像大小app

$ docker images
server          latest         8d3b91210721      1 minutes ago        11MB
复制代码

11MB,还行,如今运行一下curl

$ 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)
复制代码

这是否是跟咱们的认知有点出入呢,说好无依赖的呢,结果仍是有几个依赖库文件呢,虽然这几个依赖都是最底层的,通常操做系统都会有,可谁叫咱们选了 scratch,这个镜像里面除了linux内核之外真的什么都没了。

这是由于go build 是默认启用 CGO 的,不信你能够试试这个命令 go env CGO_ENABLED,在 CGO 开启状况下,不管代码有没有用CGO,都会有库依赖文件,解决方法也很简单,手动指定关闭CGO就行,并且包体积并不会增长哦,还会减小呢

$ CGO_ENABLED=0 go build -o server
$ ldd server
        not a dynamic executable
复制代码

版本二,解决运行时报错

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 做为运行环境,我暂时只踩到小部分的坑,后面还有更多坑没踩,我也没有兴趣继续踩 scratch 的坑。

建议选择 alpine ,alpine 的镜像大小是 5.61MB 这个大小其实仍是镜像解压后的大小,实际上下载镜像的时候,只须要下载 2.68 MB 。还有,上文全部我说的镜像体积,全都是指解压后的镜像体积,和实际上传下载时的体积是不同的,docker本身会压缩一次再传输镜像

还有个很小的镜像是 busybox,它的体积是 1.22MB,下载 705.6 KB ,有大部分的linux命令可用,可是运行环境仍是很原始,有兴趣能够去尝试

不管是 alpine 仍是 busybox ,他们都会上述时区和证书问题,一样按照上面方法就能解决,切换到 alpine 或者 busybox 也很简单,只须要修改 runner 基础镜像就行

-FROM scratch as runner
+FROM alpine as runner
复制代码

或者

-FROM scratch as runner
+FROM busybox as runne
复制代码
相关文章
相关标签/搜索