构建 Go 应用 docker 镜像的十八种姿势

修炼背景

我夜以继日,加班加点开发了一个最简单的 Go Hello world 应用,虽然只是跑了打印一下就退出了,但是老板也要求我上线这个我能写出的唯一应用。

项目结构如下:

.
├── go.mod
└── hello.go

hello.go 代码如下:

package main

func main() {
    println("hello world!")
}

并且,老板要求用 docker 部署,显得咱们紧跟潮流,高大上一点。。。

第一次尝试

我在拜访了一些武林朋友之后,发现把整个过程丢到 docker 里面去编译一下就好了,一番琢磨之后,我得到了如下 Dockerfile:

FROM golang:alpine

WORKDIR /build

COPY hello.go .

RUN go build -o hello hello.go

CMD ["./hello"]

构建镜像:

$ docker build -t hello:v1 .

搞定,让我们凑近了看看。

$ docker run -it --rm hello:v1 ls -l /build
total 1260
-rwxr-xr-x    1 root     root       1281547 Mar  6 15:54 hello
-rw-r--r--    1 root     root            55 Mar  6 14:59 hello.go

好家伙,我好不容易写出来的代码也在里面,看来代码不能写的烂,不然运维妹子偷看了要笑话我。。。

我们再看看镜像到底有多大,据说大了拉取镜像就会比较慢呢

$ docker images | grep hello
hello         v1    2783ee221014   44 minutes ago   314MB

哇,居然有314MB,难道 docker build 一下变 Java 了吗?不是什么东西都是越大越好的。。。

让我们看看为啥这么大!

看看,我们跑第一个指令(WORKDIR)前就已经300+MB了,有点猛啊!

不管怎么说,我们先跑一下看看

$ docker run -it --rm hello:v1
hello world!

没问题呀,好歹可以工作嘛~

第二次尝试

经过一番烟酒,加上朋友指点,发现原来我们用的那个基础镜像实在太大了。

$ docker images | grep golang
golang    alpine     d026981a7165   2 days ago          313MB

并且朋友告诉我可以把代码先编译好,再拷贝进去,就不用那个巨大的基础镜像了,不过说起来容易,我还是好好花了点功夫的,最后 Dockerfile 长这样:

FROM alpine

WORKDIR /build

COPY hello .

CMD ["./hello"]

跑一下试试

$ docker build -t hello:v2 .
...
=> ERROR [3/3] COPY hello .                         0.0s
------
 > [3/3] COPY hello .:
------
failed to compute cache key: "/hello" not found: not found

不对,hello 找不到,忘记先编译一下 hello.go 了,再来~

$ go build -o hello hello.go

再跑 docker build -t hello:v2 .,没问题,走两步试试。。。

$ docker run -it --rm hello:v2
standard_init_linux.go:228: exec user process caused: exec format error

失败!好吧,格式不对,原来我们开发机不是 linux 呀,再来~

$ GOOS=linux go build -o hello hello.go

重新 docker build 终于搞定了,赶紧跑下

$ docker run -it --rm hello:v2
hello world!

没问题,我们来看看内容和大小。

$ docker run -it --rm hello:v2 ls -l /build
total 1252
-rwxr-xr-x    1 root     root       1281587 Mar  6 16:18 hello

里面只有 hello 这个可执行文件,再也不用担心别人鄙视我的代码了~

$ docker images | grep hello
hello    v2   0dd53f016c93   53 seconds ago      6.61MB
hello    v1   ac0e37173b85   25 minutes ago      314MB

哇,6.61MB,绝对可以!

看看,我们跑第一个指令(WORKDIR)前面只有 5.3MB 了,开心啊!

第三次尝试

一顿炫耀之后,居然有人鄙视我,说现在流行什么多阶段构建,那么第二种方式到底有啥问题呢?细细琢磨之后发现,我们要能从 Go 代码构建出 docker 镜像,其中分为三步:

  1. 本机编译 Go 代码,如果牵涉到 cgo 跨平台编译就会比较麻烦了
  2. 用编译出的可执行文件构建 docker 镜像
  3. 编写 shell 脚本或者 makefile 让这几步通过一个命令可以获得

多阶段构建就是把这一切都放到一个 Dockerfile 里,既没有源码泄漏,又不需要用脚本去跨平台编译,还获得了最小的镜像。

爱学习,追求完美的我最终写出了如下 Dockerfile,多一行则肥,少一行则瘦:

FROM golang:alpine AS builder

WORKDIR /build

ADD go.mod .
COPY . .
RUN go build -o hello hello.go


FROM alpine

WORKDIR /build
COPY --from=builder /build/hello /build/hello

CMD ["./hello"]

第一个 FROM 开始的部分是构建一个 builder 镜像,目的是在其中编译出可执行文件 hello,第二个 From 开始的部分是从第一个镜像里 copy 出来可执行文件 hello,并且用尽可能小的基础镜像 alpine 以保障最终镜像尽可能小,至于为啥不用更小的 scratch,是因为 scratch 真的啥也没有,有问题连上去看一眼的机会都没有,而 alpine 也才 5MB,对我们的服务不会构成多少影响。

我们先跑了验证一下:

$ docker run -it --rm hello:v3
hello world!

没问题,正如预期!看看大小如何:

$ docker images | grep hello
hello    v3     f51e1116be11   8 hours ago    6.61MB
hello    v2     0dd53f016c93   8 hours ago    6.61MB
hello    v1     ac0e37173b85   8 hours ago    314MB

跟第二种方法构建的镜像大小完全一样。再看看镜像里的内容:

$ docker run -it --rm hello:v3 ls -l /build
total 1252
-rwxr-xr-x    1 root     root       1281547 Mar  6 16:32 hello

也是只有一个可执行的 hello 文件,完美!

跟第二个最终镜像基本是一致的,但我们简化了流程,只需要一个 Dockerfile,跑一条命令就好了,不需要我去整那些晦涩难懂的 shellmakefile 了。

神功练成

至此,团队小伙伴都觉得完美,纷纷给我点赞!但是,既追求完美,又喜欢偷懒(摸鱼)的我觉得吧,每次都让我写出这么个增一行则肥,减一行则瘦的 Dockerfile,我还是觉得挺烦的,于是我瞒着老板写了个工具,我来秀一秀~~

# 安装一下先
$ GOPROXY=https://goproxy.cn/,direct go install github.com/zeromicro/go-zero/tools/goctl@latest
# 一键编写 Dockerfile
$ goctl docker -go hello.go

搞定!看看生成的 Dockerfile

FROM golang:alpine AS builder

LABEL stage=gobuilder

ENV CGO_ENABLED 0
ENV GOPROXY https://goproxy.cn,direct

RUN apk update --no-cache && apk add --no-cache tzdata

WORKDIR /build

ADD go.mod .
ADD go.sum .
RUN go mod download
COPY . .
RUN go build -ldflags="-s -w" -o /app/hello ./hello.go


FROM alpine

RUN apk update --no-cache && apk add --no-cache ca-certificates
COPY --from=builder /usr/share/zoneinfo/Asia/Shanghai /usr/share/zoneinfo/Asia/Shanghai
ENV TZ Asia/Shanghai

WORKDIR /app
COPY --from=builder /app/hello /app/hello

CMD ["./hello"]

其中几点可以了解下:

  • 默认禁用了 cgo
  • 启用了 GOPROXY 加速 go mod download
  • 去掉了调试信息 -ldflags="-s -w" 以减小镜像尺寸
  • 安装了 ca-certificates,这样使用 TLS证书就没问题了
  • tzdatabuilder 镜像安装,并在最终镜像只拷贝了需要的时区
  • 自动设置了本地时区,这样我们在日志里看到的是北京时间了

我们看看用这个自动生成的 Dockerfile 构建出的镜像大小:

$ docker images | grep hello
hello     v4    94ba3ece3071   4 hours ago     6.66MB
hello     v3    f51e1116be11   8 hours ago     6.61MB
hello     v2    0dd53f016c93   8 hours ago     6.61MB
hello     v1    ac0e37173b85   9 hours ago     314MB

略微大一点,这是因为我们拷贝了 ca-certificatestzdata。验证一下:

我们看看镜像里有啥:

$ docker run -it --rm hello:v4 ls -l /app
total 832
-rwxr-xr-x    1 root     root        851968 Mar  7 08:36 hello

也是只有 hello 可执行文件,并且文件大小从原来的 1281KB 减到了 851KB。跑一下看看:

$ docker run -it --rm hello:v4
hello world!

并且你可以在生成 Dockerfile 的时候指定基础镜像为 scratch,这样镜像就更小了,但是你就不能直接通过 sh 登陆进去了。

$ goctl docker -base scratch -go hello.go

尺寸也是真的好小:

$ docker images | grep hello
hello   v5   d084eed88d88   4 seconds ago   1.07MB
hello   v4   94ba3ece3071   15 hours ago    6.66MB
hello   v3   f51e1116be11   4 days ago      6.61MB
hello   v2   0dd53f016c93   4 days ago      6.61MB
hello   v1   ac0e37173b85   4 days ago      314MB

再看看镜像里都有啥

我这是在 Macbook M1 上编译的是 linux/arm64 镜像,我猜你常规的是要打 linux/amd64 的镜像,用下面这个命令就好:

$ docker build --rm --platform linux/amd64 -t hello:v6 .

好了好了,不再纠缠 Dockerfile 了,我要去学习新技能了~

项目地址

github.com/zeromicro/go-zero

觉得不错吗?欢迎打赏吆,打赏只需点亮 GitHub 小星星⭐️

微信交流群

关注『微服务实践』公众号并点击 交流群 获取社区群二维码。

本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由系统于 7个月前 自动加精
kevwan
讨论数量: 10

从无到有的过程是最难的,而且是要精致的那种更难。好文!

7个月前 评论

睡前看了一下,笑死我了 :joy:,文章写的真好,保姆级叫使用工具,有趣且有用。 :+1:

7个月前 评论

第二遍,发现这里多了个docker

docker docker images | grep hello
hello         v1    2783ee221014   44 minutes ago   314MB
7个月前 评论
kevwan (楼主) 7个月前
kinyou

喜欢这样风格的博文, 谢谢博主

7个月前 评论

有学到东西,谢谢博主!
请问图片中查看空间大小的指令是什么啊?
file

7个月前 评论
kevwan (楼主) 7个月前

厉害厉害

5个月前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!