Go 语言应用部署 不需要依赖 ,非常简便,这是一个不小的优势。
Go 语言官方镜像非常大,超过 500MB 。镜像之所以如此庞大是因为它包含了构建 Go 程序所需的全部 工具链 。然而运行编译好的(静态)二进制程序,并不需要这些工具。
本文介绍如何制作一个紧凑的 Docker 镜像用于部署 Go 应用,大小控制在 10MB 以内。
本文实验所有操作均在 macOS 下进行,在其他平台进行也是类似的。
示例程序
实验以这个简单程序为例:
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
 | package main
import (
    "fmt"
    "net/http"
    "time"
)
func main() {
    now := time.Now()
    tokyoTZ, _ := time.LoadLocation("Asia/Tokyo")
    tokyoTime := now.In(tokyoTZ)
    fmt.Printf("Local time: %s\nTokyo time: %s\n", now, tokyoTime)
    _, err := http.Get("https://www.baidu.com/")
    if err == nil {
        fmt.Println("Baidu website is UP")
    } else {
        fmt.Printf("Baidu website is DOWN\nErr: %s\n", err.Error())
    }
}
 | 
 
程序先输出当前本地时间以及东京时间;接着请求百度首页并判断是否成功。
先对其进行编译并查看二进制程序大小:
| 1
2
3
 | $ go build demo.go
$ ls -lh demo
-rwxr-xr-x 1 fasion staff 5.8M Nov 29 18:36 demo
 | 
 
注意到,二进制程序大小仅为 5.8MB 。
构建镜像
接下来我们准备一个 Dockerfile 来构建镜像:
| 1
2
3
4
 | FROM scratch
ENV TZ=Asia/Shanghai
ADD demo /
CMD ["/demo"]
 | 
 
执行镜像构建命令:
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
 | $ docker build -t demo .
Sending build context to Docker daemon  6.299MB
Step 1/4 : FROM scratch
 --->
Step 2/4 : ENV TZ=Asia/Shanghai
 ---> Using cache
 ---> f6653a3d26c7
Step 3/4 : ADD demo /
 ---> 437b92b7460a
Step 4/4 : CMD ["/demo"]
 ---> Running in 0bc0aef56fab
Removing intermediate container 0bc0aef56fab
 ---> e8ff5745453e
Successfully built e8ff5745453e
Successfully tagged demo:latest
 | 
 
接着,尝试执行该镜像:
| 1
2
 | $ docker run demo
standard_init_linux.go:190: exec user process caused "exec format error"
 | 
 
执行过程中不幸出错,错误为 exec format error ,即 可执行程序格式错误 。这个错误是由于我们在 Docker 容器内执行 macOS 二进制程序造成的:
| 1
2
 | $ file demo
demo: Mach-O 64-bit executable x86_64
 | 
 
Docker 容器技术是基于 Linux 内核的,因此只能执行 Linux 格式二进制程序。
交叉编译
接着需要通过 交叉编译 来构建 Linux 二进制程序:
| 1
2
3
 | $ CGO_ENABLED=0 GOOS=linux go build demo.go
$ file demo
demo: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
 | 
 
交叉编译生成的程序为 ELF 格式,正是 Linux 二进制程序格式!
注意到交叉编译生成的程序是 静态链接 的,无需依赖任何动态库文件。由于 scratch 母镜像不包含必要的动态库文件,运行动态链接的程序将报错。
有了 Linux 二进制后,便可重新构建镜像并执行:
| 1
2
3
4
5
6
7
8
 | $ docker run demo
panic: time: missing Location in call to Time.In
goroutine 1 [running]:
time.Time.In(...)
        /usr/local/go/src/time/time.go:1086
main.main()
        /Users/fasion/workspace/works/go-book/docs/zh_CN/_src/practices/docker/demo.go:24 +0x311
 | 
 
镜像成功启动,但是遇到另一个错误。原因在于 scratch 镜像并不包含任何 时区信息 ,我们需要从本地系统中复制一份。
时区信息
由于 scratch 镜像几乎不包含任何东西,甚至没有 mkdir 命令。因此,我们需要对时区信息进行打包,再通过 ADD 指令进行添加,以此绕过目录创建:
| 1
2
 | $ tar -chzf zoneinfo.tar.gz /usr/share/zoneinfo
tar: Removing leading '/' from member names
 | 
 
修改后的 Dockerfile 如下:
| 1
2
3
4
5
 | FROM scratch
ENV TZ=Asia/Shanghai
ADD zoneinfo.tar.gz /
ADD demo /
CMD ["/demo"]
 | 
 
再次构建并执行:
| 1
2
3
4
5
 | $ docker run demo
Local time: 2018-11-29 18:47:42.1671632 +0800 CST m=+0.002501501
Tokyo time: 2018-11-29 19:47:42.1671632 +0900 JST
Baidu website is DOWN
Err: Get https://www.baidu.com/: x509: certificate signed by unknown authority
 | 
 
时区问题已经解决了,但发起 HTTPS 请求还存在问题,原因是 scratch 镜像不包含任何 SSL CA 证书。
CA根证书
接下来,从 https://curl.haxx.se/docs/caextract.html 下载 CA 证书:
| 1
 | $ curl -o cacert.pem https://curl.haxx.se/ca/cacert.pem
 | 
 
更新 Dockerfile 将证书添加到镜像:
| 1
2
3
4
5
6
 | FROM scratch
ENV TZ=Asia/Shanghai
ADD zoneinfo.tar.gz /
ADD cacert.pem /etc/ssl/certs/
ADD demo /
CMD ["/demo"]
 | 
 
再次构建并执行:
| 1
2
3
4
 | $ docker run demo
Local time: 2018-11-29 19:01:19.5481835 +0800 CST m=+0.001815301
Tokyo time: 2018-11-29 20:01:19.5481835 +0900 JST
Baidu website is UP
 | 
 
至此,大功告成!
最后,我们分别检查二进制程序以及镜像的大小:
| 1
2
3
4
5
 | $ ls -lh demo
-rwxr-xr-x 1 fasion staff 5.8M Nov 29 18:47 demo
$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
demo                latest              a9ae4b8e7f1e        8 minutes ago       7.06MB
 | 
 
正如你所见,二进制程序大小是 5.8MB ,而 Docker 镜像只有 7MB 左右!
总结
构建紧凑的 Go 应用部署镜像,需要注意以下要点:
- 以 scratch 为母版;
- 静态链接;
- 添加时区信息;
- 添加 CA 证书;
这个经验适用于任何二进制程序,不局限于 Go ,其他语言如 C 也是类似的。
【小菜学Go语言】系列文章首发于公众号【小菜学编程】,敬请关注:
