构建 Go 应用 docker 镜像的十八种姿势
<h2 id="修炼背景">修炼背景</h2><p>我夜以继日,加班加点开发了一个最简单的 Go Hello world 应用,虽然只是跑了打印一下就退出了,但是老板也要求我上线这个我能写出的唯一应用。</p>
<p>项目结构如下:</p>
<pre><code>.
├── go.mod
└── hello.go
</code></pre>
<p><code>hello.go</code> 代码如下:</p>
<pre><code class="language-go">package main
func main() {
println("hello world!")
}
</code></pre>
<p>并且,老板要求用 <code>docker</code> 部署,显得咱们紧跟潮流,高大上一点。。。</p>
<h2 id="第一次尝试">第一次尝试</h2>
<p>我在拜访了一些武林朋友之后,发现把整个过程丢到 <code>docker</code> 里面去编译一下就好了,一番琢磨之后,我得到了如下 <code>Dockerfile</code>:</p>
<pre><code class="language-dockerfile">FROM golang:alpine
WORKDIR /build
COPY hello.go .
RUN go build -o hello hello.go
CMD ["./hello"]
</code></pre>
<p>构建镜像:</p>
<pre><code class="language-shell">$ docker build -t hello:v1 .
</code></pre>
<p>搞定,让我们凑近了看看。</p>
<pre><code class="language-shell">$ docker run -it --rm hello:v1 ls -l /build
total 1260
-rwxr-xr-x 1 root root 1281547 Mar6 15:54 hello
-rw-r--r-- 1 root root 55 Mar6 14:59 hello.go
</code></pre>
<p>好家伙,我好不容易写出来的代码也在里面,看来代码不能写的烂,不然运维妹子偷看了要笑话我。。。</p>
<p>我们再看看镜像到底有多大,据说大了拉取镜像就会比较慢呢</p>
<pre><code class="language-shell">$ docker docker images | grep hello
hello v1 2783ee221014 44 minutes ago 314MB
</code></pre>
<p>哇,居然有314MB,难道 <code>docker build</code> 一下变<code>Java</code> 了吗?不是什么东西都是越大越好的。。。</p>
<p>让我们看看为啥这么大!</p>
<p><img src="https://oscimg.oschina.net/oscnet/up-093b3eb9f7955b6320773d7784c6001c1c8.png" alt="" loading="lazy"></p>
<p>看看,我们跑第一个指令(<code>WORKDIR</code>)前就已经300+MB了,有点猛啊!</p>
<p>不管怎么说,我们先跑一下看看</p>
<pre><code class="language-shell">$ docker run -it --rm hello:v1
hello world!
</code></pre>
<p>没问题呀,好歹可以工作嘛~</p>
<h2 id="第二次尝试">第二次尝试</h2>
<p>经过一番烟酒,加上朋友指点,发现原来我们用的那个基础镜像实在太大了。</p>
<pre><code class="language-shell">$ docker images | grep golang
golang alpine d026981a7165 2 days ago 313MB
</code></pre>
<p>并且朋友告诉我可以把代码先编译好,再拷贝进去,就不用那个巨大的基础镜像了,不过说起来容易,我还是好好花了点功夫的,最后 <code>Dockerfile</code> 长这样:</p>
<pre><code class="language-dockerfile">FROM alpine
WORKDIR /build
COPY hello .
CMD ["./hello"]
</code></pre>
<p>跑一下试试</p>
<pre><code class="language-shell">$ docker build -t hello:v2 .
...
=> ERROR COPY hello . 0.0s
------
> COPY hello .:
------
failed to compute cache key: "/hello" not found: not found
</code></pre>
<p>不对,<code>hello</code> 找不到,忘记先编译一下 <code>hello.go</code> 了,再来~</p>
<pre><code class="language-shell">$ go build -o hello hello.go
</code></pre>
<p>再跑 <code>docker build -t hello:v2 .</code>,没问题,走两步试试。。。</p>
<pre><code class="language-shell">$ docker run -it --rm hello:v2
standard_init_linux.go:228: exec user process caused: exec format error
</code></pre>
<p>失败!好吧,格式不对,原来我们开发机不是 <code>linux</code> 呀,再来~</p>
<pre><code class="language-shell">$ GOOS=linux go build -o hello hello.go
</code></pre>
<p>重新 <code>docker build</code> 终于搞定了,赶紧跑下</p>
<pre><code class="language-shell">$ docker run -it --rm hello:v2
hello world!
</code></pre>
<p>没问题,我们来看看内容和大小。</p>
<pre><code class="language-shell">$ docker run -it --rm hello:v2 ls -l /build
total 1252
-rwxr-xr-x 1 root root 1281587 Mar6 16:18 hello
</code></pre>
<p>里面只有 <code>hello</code> 这个可执行文件,再也不用担心别人鄙视我的代码了~</p>
<pre><code class="language-shell">$ docker images | grep hello
hello v2 0dd53f016c93 53 seconds ago 6.61MB
hello v1 ac0e37173b85 25 minutes ago 314MB
</code></pre>
<p>哇,6.61MB,绝对可以!</p>
<p><img src="https://oscimg.oschina.net/oscnet/up-5cb66e81ff4ec08e88908d75c2a80ae0486.png" alt="" loading="lazy"></p>
<p>看看,我们跑第一个指令(<code>WORKDIR</code>)前面只有 5.3MB 了,开心啊!</p>
<h2 id="第三次尝试">第三次尝试</h2>
<p>一顿炫耀之后,居然有人鄙视我,说现在流行什么多阶段构建,那么第二种方式到底有啥问题呢?细细琢磨之后发现,我们要能从 <code>Go</code> 代码构建出 <code>docker</code> 镜像,其中分为三步:</p>
<ol>
<li>本机编译 <code>Go</code> 代码,如果牵涉到 <code>cgo</code> 跨平台编译就会比较麻烦了</li>
<li>用编译出的可执行文件构建 <code>docker</code> 镜像</li>
<li>编写 <code>shell</code> 脚本或者 <code>makefile</code> 让这几步通过一个命令可以获得</li>
</ol>
<p>多阶段构建就是把这一切都放到一个 <code>Dockerfile</code> 里,既没有源码泄漏,又不需要用脚本去跨平台编译,还获得了最小的镜像。</p>
<p>爱学习,追求完美的我最终写出了如下 <code>Dockerfile</code>,多一行则肥,少一行则瘦:</p>
<pre><code class="language-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"]
</code></pre>
<p>第一个 <code>FROM</code> 开始的部分是构建一个 <code>builder</code> 镜像,目的是在其中编译出可执行文件 <code>hello</code>,第二个 <code>From</code> 开始的部分是从第一个镜像里 <code>copy</code> 出来可执行文件 <code>hello</code>,并且用尽可能小的基础镜像 <code>alpine</code> 以保障最终镜像尽可能小,至于为啥不用更小的 <code>scratch</code>,是因为 <code>scratch</code> 真的啥也没有,有问题连上去看一眼的机会都没有,而 <code>alpine</code> 也才 5MB,对我们的服务不会构成多少影响。</p>
<p>我们先跑了验证一下:</p>
<pre><code class="language-shell">$ docker run -it --rm hello:v3
hello world!
</code></pre>
<p>没问题,正如预期!看看大小如何:</p>
<pre><code class="language-shell">$ 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
</code></pre>
<p>跟第二种方法构建的镜像大小完全一样。再看看镜像里的内容:</p>
<pre><code class="language-shell">$ docker run -it --rm hello:v3 ls -l /build
total 1252
-rwxr-xr-x 1 root root 1281547 Mar6 16:32 hello
</code></pre>
<p>也是只有一个可执行的 <code>hello</code> 文件,完美!</p>
<p><img src="https://oscimg.oschina.net/oscnet/up-4deca7cd59b2d276d1b3407111b60d47d19.png" alt="" loading="lazy"></p>
<p>跟第二个最终镜像基本是一致的,但我们简化了流程,只需要一个 <code>Dockerfile</code>,跑一条命令就好了,不需要我去整那些晦涩难懂的 <code>shell</code> 和 <code>makefile</code> 了。</p>
<h2 id="神功练成">神功练成</h2>
<p>至此,团队小伙伴都觉得完美,纷纷给我点赞!但是,既追求完美,又喜欢偷懒(摸鱼)的我觉得吧,每次都让我写出这么个增一行则肥,减一行则瘦的 <code>Dockerfile</code>,我还是觉得挺烦的,于是我瞒着老板写了个工具,我来秀一秀~~</p>
<pre><code class="language-shell"># 安装一下先
$ GOPROXY=https://goproxy.cn/,direct go install github.com/zeromicro/go-zero/tools/goctl@latest
# 一键编写 Dockerfile
$ goctl docker -go hello.go
</code></pre>
<p>搞定!看看生成的 <code>Dockerfile</code> 哈</p>
<pre><code class="language-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"]
</code></pre>
<p>其中几点可以了解下:</p>
<ul>
<li>默认禁用了 <code>cgo</code></li>
<li>启用了 <code>GOPROXY</code> 加速 <code>go mod download</code></li>
<li>去掉了调试信息 <code>-ldflags="-s -w"</code> 以减小镜像尺寸</li>
<li>安装了 <code>ca-certificates</code>,这样使用 <code>TLS</code>证书就没问题了</li>
<li><code>tzdata</code> 在 <code>builder</code> 镜像安装,并在最终镜像只拷贝了需要的时区</li>
<li>自动设置了本地时区,这样我们在日志里看到的是北京时间了</li>
</ul>
<p>我们看看用这个自动生成的 <code>Dockerfile</code> 构建出的镜像大小:</p>
<pre><code class="language-shell">$ 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
</code></pre>
<p>略微大一点,这是因为我们拷贝了 <code>ca-certificates</code> 和 <code>tzdata</code>。验证一下:</p>
<p><img src="https://oscimg.oschina.net/oscnet/up-8a548594bf09305dfdfbbfc05a2440c5128.png" alt="" loading="lazy"></p>
<p>我们看看镜像里有啥:</p>
<pre><code class="language-shell">$ docker run -it --rm hello:v4 ls -l /app
total 832
-rwxr-xr-x 1 root root 851968 Mar7 08:36 hello
</code></pre>
<p>也是只有 <code>hello</code> 可执行文件,并且文件大小从原来的 1281KB 减到了 851KB。跑一下看看:</p>
<pre><code class="language-shell">$ docker run -it --rm hello:v4
hello world!
</code></pre>
<p>并且你可以在生成 <code>Dockerfile</code> 的时候指定基础镜像为 <code>scratch</code>,这样镜像就更小了,但是你就不能直接通过 <code>sh</code> 登陆进去了。</p>
<pre><code class="language-shell">$ goctl docker -base scratch -go hello.go
</code></pre>
<p>尺寸也是真的好小:</p>
<pre><code class="language-shell">$ 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
</code></pre>
<p>再看看镜像里都有啥</p>
<p><img src="https://oscimg.oschina.net/oscnet/up-e839a1f78588a16e5838d11b58bd17f5781.png" alt="" loading="lazy"></p>
<p>我这是在 <code>Macbook M1</code> 上编译的是 <code>linux/arm64</code> 镜像,我猜你常规的是要打 <code>linux/amd64</code> 的镜像,用下面这个命令就好:</p>
<pre><code class="language-shell">$ docker build --rm --platform linux/amd64 -t hello:v6 .
</code></pre>
<p>好了好了,不再纠缠 <code>Dockerfile</code> 了,我要去学习新技能了~</p>
<h2 id="项目地址">项目地址</h2>
<p>https://github.com/zeromicro/go-zero</p>
<p>觉得不错吗?欢迎打赏吆,打赏只需点亮 <code>GitHub</code> 小星星⭐️</p>
<h2 id="微信交流群">微信交流群</h2>
<p>关注『<strong>微服务实践</strong>』公众号并点击 <strong>交流群</strong> 获取社区群二维码。</p><br><br>
来源:https://www.cnblogs.com/kevinwan/p/16033634.html
頁:
[1]