缘起

在现代化的开发流程 DevOps 中,经常需要让 gitlab 在提交代码后自动进行单元测试、构建甚至发布。这时,通常会让一个 gitlab-runner 执行 gitlab-ci 脚本。执行时,一般使用一个 docker 镜像。

在 gitlab-ci 脚本中,会需要对这个 go 项目进行单元测试(go test ./...)或者构建(go build .)。因此这个 docker 镜像必须预装 go 语言。

然而有一个问题。

一方面,golang 是一个高度依赖互联网连接的语言,其依赖包都是来自网上,甚至编译过程中还会下载所需要的包。

另一方面,很多企业为了安全,gitlab-runner 运行在不联网的服务器上。

这使得 gitlab-runner 执行go build(或者在那之前的go mod tidy)时,会因为网络不通而失败。

问题分析

无法编译的原因在于,编译过程所依赖的第三方包,由于网络原因无法下载到本地(上面情况就是下载到 docker 容器里)。

那么解决方案有三种。

  1. 设法让 gitlab-runner 可以联网。
  2. 将项目依赖的第三方包保存到项目内部的 vendor 目录下。
  3. 直接让编译器镜像自带所需要的第三方包。

本文专注第三种方案。

解决方案

准备动作:我使用 windows 命令行。需要安装 Docker Desktop。

Step 1: 拉取最新 golang 基础镜像

由于项目很可能在go mod init的时候生成了一个基于最新 go 版本的go.mod文件,因此编译环境必须尽可能最新。

命令:

1
docker pull golang:latest

效果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
>docker pull golang:latest
latest: Pulling from library/golang
c19952135643: Already exists
7bbf972c6c2f: Already exists
900e2c02f17f: Already exists
fa7b7c560647: Pull complete
a3e4aa2eec44: Pull complete
45fc98f4f8c5: Pull complete
4f4fb700ef54: Pull complete
Digest: sha256:a9219eb99cd2951b042985dbec09d508b3ddc20c4da52a3a55b275b3779e4a05
Status: Downloaded newer image for golang:latest
docker.io/library/golang:latest

What's next:
    View a summary of image vulnerabilities and recommendations → docker scout quickview golang:latest

确认:

1
2
>docker run golang:latest go version
go version go1.24.5 linux/amd64

Step 2: 将所有依赖包都打包压缩成一个 tar.gz

虽然理论上只需要将当前项目所需的第三方包打包到编译器镜像中,但为了让这个镜像通用于其他项目的编译,并且(更主要的)为了方便操作,干脆把所有本地的第三方包都打包。

确认第三方包位置:

1
2
C:\Users\seedj>echo %GOPATH%
C:\Users\seedj\go

进入该目录,可以看到里面有 bin 和 pkg 两个目录。我们需要打包的是整个 pkg 文件夹。打包成单个 pkg.tar.gz 文件。

Windows 标准命令行(cmd)没有提供 tar 命令,不过 PowerShell 有提供。所以用 PowerShell 可以完成打包压缩(也可以切换到 WSL 来使用 linux 的 tar 工具)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
PS C:\Users\seedj> cd go
PS C:\Users\seedj\go> ls

    目录: C:\Users\seedj\go

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----  2023/03/20/星期一     16:0                bin
                                 8
d-----  2025/07/08/星期二     10:5                pkg
                                 1

PS C:\Users\seedj\go> tar -czf pkg.tar.gz pkg

Step 3: 编写编译器镜像的 DockerFile

基本上,应该用 DockerFile 来规定 Docker 镜像的生成规则。

随便找个目录,在里面放上纯文本的 DockerFile 文件,内容是:

1
2
3
FROM golang:latest

ADD pkg.tar.gz /go/

上面这个 DockerFile 将打包后的 pkg.tar.gz 添加到镜像中。注意,虽然添加的是 pkg.tar.gz 但添加时会自动解压缩。

将这个 DockerFile 和 pkg.tar.gz 放在同一个目录下。

1
2
3
D:\DOCKER-IMAGES\GOBUILDER
    Dockerfile
    pkg.tar.gz

进入这个目录,执行docker build -t <TAG>

1
>docker build -t gobuilder:0.56 .

这里 gobuilder 是镜像名称,0.11 是镜像版本号。均可随意改动。

确认编译结果:

1
2
3
4
>docker images
REPOSITORY                      TAG                                IMAGE ID       CREATED         SIZE
golang                          latest                             e76576dde521   15 hours ago    801MB
gobuilder                       0.56                               a3f6c5c0e34a   29 hours ago    3.02GB

Step 4: 将新镜像上传到 docker 镜像仓库

基本上,docker 镜像要上传到哪个镜像仓库,是由镜像标题决定的。只需要给已有镜像打上一个「带有镜像仓库路径」的tag,然后推送就行了。推送的时候会自动从路径识别出目标镜像仓库。

1
2
>docker tag gobuilder:0.56 your.company.com/somepath/gobuilder:0.56
>docker push your.company.com/myteamname/gobuilder:0.56

Step 5: 在待编译项目的 .gitlab-ci.yml 里指定使用新编译器镜像

直接在开头或者某个job里使用image命令即可。比如image: your.company.com/myteamname/gobuilder:0.56

日常维护建议

golang 官方会时不时地更新golang:latest的版本。各第三方库也可能时常更新版本。所以要定期更新gobuilder(暂定名),方便使用。

总结

这个方案并不完美。

理想情况下,应该是在已有镜像的基础上动态更新,这样可以节约 docker 镜像仓库的空间。不过由于golang:latest作为基础镜像,每次更新gobuilder都需要重新构建整个镜像。

考虑到各模块对编译环境的依赖,或许应该让 gitlab-ci 里依赖your.company.com/myteamname/gobuilder:latest更好。