如何构建 Go 应用的 Docker 镜像

简介: 在部署 Go 应用时,我们通常会使用 Docker 镜像来部署,那么如何构建一个 Go 应用的 Docker 镜像呢?镜像构建过程中有没有什么最佳实践呢?

在部署 Go 应用时,我们通常会使用 Docker 镜像来部署,那么如何构建一个 Go 应用的 Docker 镜像呢?镜像构建过程中有没有什么最佳实践呢?


这正是本文想要讲解的内容。总的来说,本文会包含 Dockerfile 编写、镜像构建、多阶段构建、交叉编译以及使用 Makefile 简化构建流程等知识点。


创建一个简单的 Go 应用


为了说明整个镜像构建流程,让我们先从一个简单的 Go REST 应用开始。


该应用主要有以下功能:


  • 访问 /,返回  Hello, Docker! <3
  • 访问 /ping ,返回 JSON 字符串 {"Status":"OK"}
  • 可以通过环境变量设置 HTTP_PORT,默认值为8080。


应用源码地址在 https://github.com/nodejh/docker-go-server-ping ,你可以直接下载使用,也可以按照下面的步骤从零开始编写代码。


方式一:下载源码


$ git clone git@github.com:nodejh/docker-go-server-ping.git


安装依赖模块:


$ go mod download


方式二:从零编写 Go 应用


新建一个 docker-go-server-ping 目录,然后初始化 Go 模块:


$ mkdir docker-go-server-ping && cd docker-go-server-ping
$ go mod init github.com/nodejh/docker-go-server-ping


安装 Echo 模块:


$ goget github.com/labstack/echo/v4

接下来创建一个 main.go 文件,并实现一个简单的 Go 服务:


packagemainimport (
"net/http""os""github.com/labstack/echo/v4""github.com/labstack/echo/v4/middleware")
funcmain() {
e :=echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.GET("/", func(cecho.Context) error {
returnc.HTML(http.StatusOK, "Hello, Docker! <3")
 })
e.GET("/ping", func(cecho.Context) error {
returnc.JSON(http.StatusOK, struct{ Statusstring }{Status: "OK"})
 })
httpPort :=os.Getenv("HTTP_PORT")
ifhttpPort=="" {
httpPort="8080" }
e.Logger.Fatal(e.Start(":"+httpPort))
}

接下来可能需要执行 go mod tidy 来确保 go.mod 和源码中的模块一致:


$ go mod tidy


测试 Go 应用


让我们启动我们的 Go 应用并确保它正常运行。进入项目目录并通过 go run 命令执行源码:


$ go run main.go 
   ____    __
  / __/___/ /  ___
 / _// __/ _ \/ _ \
/___/\__/_//_/\___/ v4.6.1
High performance, minimalist Go web framework
https://echo.labstack.com
____________________________________O/_______
                                    O\
⇨ http server started on [::]:8080


让我们对应用进行一个简单的测试,打开一个新的终端,使用 curl 命令或在浏览器打开 http://localhost:8080/ 进行测试。以 curl 为例:


$ curl http://localhost:8080/
Hello, Docker! <3


可以看到应用正常返回了,正如开头描述的那样。


确定服务器正在运行并且可以访问后,我们就可以继续针对应用构建 Docker 镜像了。


为 Go 应用创建一个 Dockerfile


Dockerfile 是 Docker 镜像的描述文件,是一个文本文件。当我们执行 docker build 构建镜像时,Docker 就会读取 Dockerfile 中的指令来创建 Docker 镜像。


从零创建 Dockerfile


让我们先来看一下创建 Dockerfile 的详细过程。


在项目根目录中创建一个名为 Dockerfile 的文件并在编辑器中打开。


添加到 Dockerfile 的第一行是 # syntax 解析器指令,这是一个可选项,表示 Docker 构建器在解析 Dockerfile 时使用什么语法。解析器指令必须在 Dockerfile 其他任何注释、空格或指令之前,并且应该是 Dockerfile 的第一行。建议使用 docker/dockerfile:1 ,它始终指向版本 1 语法的最新版本。


# syntax=docker/dockerfile:1


接下来在 Dockerfile 中再添加一行,告诉 Docker 我们的应用使用什么基础镜像:


# syntax=docker/dockerfile:1FROM golang:1.16-alpine


这里我们使用了 Golang 官方镜像 中的 1.16-alpine 版本作为基础镜像,alpine 是专门为容器设计的小型 Linux 发行版。使用基础镜像的好处是,基础镜像中内置了 Go 运行环境和工具,我们就不用自己再去安装了。


为了更好地在镜像中管理我们的应用程序,让我们在镜像中创建一个工作目录,之后源码或编译产物都存放在该目录中:


WORKDIR /app

接下来我们就需要在镜像中编译 Go 应用,这样做是为了保证编译和最终运行的环境一致。


通常我们编译 Go 应用的第一步是安装依赖,所以要先把 go.modgo.sum 复制到镜像中:


COPY go.mod ./
COPY go.sum ./


COPY 命令可以把文件复制到镜像中,这里的 ./ 对应的目录就是上一个命令 WORKDIR 指定的 /app 目录。


然后通过 RUN 命令在镜像中执行 go mod download 安装依赖,这与我们在本机直接运行命令的作用完全相同,不同的是这次会将依赖安装在镜像中:


RUN go mod download


此时我们已经有了一个基于 Go 1.16 的镜像,并安装了 Go 应用所需的依赖。下一步要做的就是把源码复制到镜像中:


RUN COPY *.go ./


该 COPY 命令使用通配符将本机当前目录(即 Dockerfile 所在目录)中后缀为 .go 的文件全部复制到镜像中。


接下来我们就可以编译 Go 应用了,依然使用熟悉的 RUN 命令:


RUN go build -o /docker-go-server-ping


该命令的结果就是产生一个名为 docker-go-server-ping 的二进制文件,并存放在镜像的系统根目录中。当然你也可以将二进制文件放在其他任何位置,根目录在这里没有特殊意义,只是它的路径较短且保持了可读性,使用起来更方便。


现在,剩下要做的就是告诉 Docker 当我们用该镜像来启动容器时要执行什么命令,这时可以使用 CMD 命令:


CMD [ "/docker-go-server-ping" ]


完整的 Dockerfile


# syntax=docker/dockerfile:1FROM golang:1.16-alpine
WORKDIR /app
COPY go.mod ./
COPY go.sum ./
RUN go mod download
COPY *.go ./
RUN go build -o /docker-go-server-ping
EXPOSE8080CMD [ "/docker-go-server-ping" ]


构建镜像


Dockerfile 编写完成后就可以使用 docker build 命令来构建镜像了。


构建镜像


让我们进入 Dockerfile 所在目录构建镜像,并通过可选的 --tag 给镜像定义一个方便阅读和识别的名字和标签,格式为 <镜像名称>:<标签>,默认是标签 latest:


$ docker build --tag docker-go-server-ping:latest .
[+] Building 32.2s (16/16) FINISHED 
=> [internal] load build definition from Dockerfile                                                                             0.1s
=> => transferring dockerfile: 37B                                                                                              0.0s
=> [internal] load .dockerignore                                                                                                0.0s
=> => transferring context: 2B                                                                                                  0.0s
=> resolve image config for docker.io/docker/dockerfile:1                                                                      15.2s
=> CACHED docker-image://docker.io/docker/dockerfile:1@sha256:42399d4635eddd7a9b8a24be879d2f9a930d0ed040a61324cfdf59ef1357b3b2  0.0s
=> [internal] load build definition from Dockerfile                                                                             0.0s
=> [internal] load .dockerignore                                                                                                0.0s
=> [internal] load metadata for docker.io/library/golang:1.16-alpine                                                           15.6s
=> [1/7] FROM docker.io/library/golang:1.16-alpine@sha256:45412fe3f5016509fc448b83faefc34e6f9e9bcc8ca1db1c54505d5528264e16      0.0s
=> [internal] load build context                                                                                                0.1s
=> => transferring context: 80B                                                                                                 0.0s
=> CACHED [2/7] WORKDIR /app                                                                                                    0.0s
=> CACHED [3/7] COPY go.mod ./                                                                                                  0.0s
=> CACHED [4/7] COPY go.sum ./                                                                                                  0.0s
=> CACHED [5/7] RUN go mod download                                                                                             0.0s
=> CACHED [6/7] COPY *.go ./                                                                                                    0.0s
=> CACHED [7/7] RUN go build -o /docker-go-server-ping                                                                          0.0s
=> exporting to image                                                                                                           0.1s
=> => exporting layers                                                                                                          0.0s
=> => writing image sha256:17fa05342ccef20e73abc48210fff449cc49683ffd817ccc6aef35500cf91ae2                                     0.0s
=> => naming to docker.io/library/docker-go-server-ping


构建完成后可以通过 docker image ls (或 docker images 简写)来查看镜像列表:

$ docker image lsREPOSITORY                   TAG            IMAGE ID       CREATED         SIZE
docker-go-server-ping        latest         17fa05342cce   9 minutes ago   407MB


为镜像设置标签


我们也可以通过 docker image tag 命令来为镜像设置新的标签,例如:


$ docker image tag docker-go-server-ping:latest docker-go-server-ping:v1.0.0


这时通过 docker image ls 就可以看到 docker-go-server-ping 镜像的两个标签:


$ docker image lsREPOSITORY                   TAG            IMAGE ID       CREATED         SIZE
docker-go-server-ping        latest         17fa05342cce   12 minutes ago   407MB
docker-go-server-ping        v1.0.0         17fa05342cce   1 minutes ago   407MB


我们还可以通过 docker image rm (简写为 docker rmi)删除镜像:


$ docker image rm docker-go-server-ping:v1.0.0
Untagged: docker-go-server-ping:v1.0.0


这时再查看镜像列表,v1.0.0 版本的镜像已被删除,只剩下 latest 版本了:


$ docker image lsREPOSITORY                   TAG            IMAGE ID       CREATED         SIZE
docker-go-server-ping        latest         17fa05342cce   15 minutes ago   407MB


单元测试


既然本文主要将 Go 的 Docker 镜像,这样就顺便简单说明如何使用 dockertest 对 Go 应用进行单元测试。


dockertest 可以在 Docker 容器中启动 Go 应用镜像并执行测试用例。


相关测试用例可以参考 main_test.go


在 main_test.go 中我们使用了 docker-go-server-ping:latest 镜像来运行 Go 应用:


resource, err :=pool.Run("docker-go-server-ping", "latest", []string{})


所以在执行 go test 之前,需要先构建 docker-go-server-ping:latest 镜像:


$ docker build --tag docker-go-server-ping:latest .
...
$ go test -v ./
=== RUN   TestRespondsWithLove
body
Hello, Docker! <3
--- PASS: TestRespondsWithLove (1.12s)
=== RUN   TestHealthCheck
    main_test.go:85: container not ready, waiting...
--- PASS: TestHealthCheck (1.66s)
PASS
ok      command-line-arguments  2.799s


多阶段构建


可能你已经注意到了,docker-go-server-ping 镜像的大小有 407MB,这着实有点大,并且镜像中还有全套的 Go 工具、Go 应用的依赖等,但实际我们应用运行时不需要这些文件,只需要编译后的二进制文件。那么能不能减小镜像的体积呢?


要减小镜像体积,我们可以使用多阶段构建。Docker 在 17.05 版本以后,新增了多阶段构建功能。多阶段构建实际上是允许一个 Dockerfile 中出现多个 FROM 指令。通常我们使用多阶段构建时,会先使用一个(或多个)镜像来构建一些中间制品,然后再将中间制品放入另一个最新且更小的镜像中,这个最新的镜像就只包含最终需要的构建制品。


多阶段构建 Dockerfile


我们先创建一个多阶段构建的 Dockerfile,名为 Dockerfile.multistage,文件名中的 multistage 没有特殊含义,只是为了和之前的 Dockerfile 作区分,下面是完整的 Dockerfile:


# syntax=docker/dockerfile:1#### Build##FROM golang:1.16-alpine AS build
WORKDIR /app
COPY go.mod ./
COPY go.sum ./
RUN go mod download
COPY *.go ./
RUN CGO_ENABLED=0 GOOS=linux go build -o /docker-go-server-ping
#### Deploy##FROM scratch
WORKDIR /
COPY --from=build /docker-go-server-ping /docker-go-server-ping
EXPOSE8080ENTRYPOINT ["/docker-go-server-ping"]


Dockerfile.multistage 中使用了两次 FROM 指令,分别对应两个构建阶段。第一个阶段构建的 FROM 指令依然使用 golang:1.16-alpine 作为基础镜像,并将该阶段命名为 build。第二个构建阶段的 FROM 指令使用 scratch 作为基础镜像,告诉 Docker 接下来从一个全新的基础镜像开始构建,scratch 镜像是 Docker 项目预定义的最小的镜像。第二阶段构建主要是将上个阶段中编译好的二进制文件复制到新的镜像中。


在 Go 应用中,多阶段构建非常常见,可以减小镜像的体积、节省大量的存储空间。


Dockerfile.multistage 中需要额外关注的是 RUN 指令,这里使用到了交叉编译。


交叉编译


交叉编译是指在一个平台上生成另一个平台的可执行程序。


在其他编程语言中进行交叉编译可能要借助第三方工具,但 Go 内置了交叉编译工具,使用起来非常方便,通常设置 CGO_ENABLED、GOOS 和 GOARCH 这几个环境变量就够了。


CGO_ENABLED


默认值是 1,即默认开启 cgo,允许在 Go 代码中调用 C 代码。


  • CGO_ENABLED=1 进行编译时,会将文件中引用 libc 的库(比如常用的 net 包)以动态链接的方式生成目标文件;
  • CGO_ENABLED=0 进行编译时,则会把在目标文件中未定义的符号(如外部函数)一起链接到可执行文件中。


所以交叉编译时,我们需要将 CGO_ENABLED 设置为 0。


GOOS 和 GOARCH


GOOS 是目标平台的操作系统,如 linux、windows,注意 macOS 的值是 darwin。默认是当前操作系统。


GOARCH 是目标平台的 CPU 架构,如 amd64、arm、386 等。默认值是当前平台的 CPU 架构。


Go 支持的所有操作系统和 CPU 架构可以查看 syslist.go


我们可以使用 go env 命令获取当前 GOOS 和 GOARCH 的值。例如我当前的操作系统是 macOS:


$ go env GOOS GOARCH
darwin
amd64


所以在本文的多阶段构建 Dockerfile.multistage 中,构建命令是:


CGO_ENABLED=0GOOS=linux GOARCH=amd64 go build -o /docker-go-server-ping


构建镜像


由于我们现在有两个 Dockerfile,所以我们必须告诉 Docker 我们要使用新的 Dockerfile 进行构建。


$ docker build -t docker-go-server-ping:multistage -f Dockerfile.multistage .

构建完成后,你会发现 docker-go-server-ping:multistage 只有不到 8MB,比 docker-go-server-ping:latest 小了几十倍。


$ docker image lsREPOSITORY                   TAG            IMAGE ID       CREATED         SIZE
docker-go-server-ping        latest         17fa05342cce   20 minutes ago   407MB
docker-go-server-ping        multistage     55fa4ded3360   1 minutes ago    7.93MB


运行 Go 镜像


现在我们有了 Go 应用的镜像,接下来就可以运行 Go 镜像查看应用程序是否正常运行。


要在 Docker 容器中运行镜像,我们可以使用 docker run 命令,参数是镜像名称:


$ docker run -p8080:8080 docker-go-server-ping:multistage
   ____    __
  / __/___/ /  ___
 / _// __/ _ \/ _ \
/___/\__/_//_/\___/ v4.6.1
High performance, minimalist Go web framework
https://echo.labstack.com
____________________________________O/_______
                                    O\
⇨ http server started on [::]:8080


可以看到 Go 应用成功启动了。


让我们再打开一个新的终端,通过 curl 向 Go 服务器发起一个请求:


$ curl http://localhost:8080
Hello, Docker! <3



使用 Makefile 简化构建流程


在前面的步骤中,我们使用到了非常多的命令,维护起来非常麻烦,这时我们就可以使用 make 来简化构建流程。


make 是一个自动化构建工具,会在根据当前目录下名为 Makefile(或 makefile)的文件来执行相应的构建任务。


所以让我们先创建一个 Makefile 文件,内容如下:


APP=docker-go-server-ping
all: clean test build-docker-multistage
test:
 go test -v ./
run: clean
 go build -o ${APP} ./${APP}deps:
 go mod download
clean:
 go clean
build-docker:
 docker build -t ${APP}:latest -f Dockerfile .
build-docker-multistage:
 docker build -t ${APP}:multistage -f Dockerfile.multistage .


接下来就可以通过 make 命令进行测试或构建了。


例如:


  • make:执行 all 中定义的命令
  • make test:执行单元测试
  • make build-docker:构建 Docker 镜像
  • build-docker-multistage:多阶段构建镜像,构建的镜像通常用于生产环境


当然你也可以在 Makefile 中定义其他命令。


总结


在本文中,我们首先开发了一个简单的 Go REST 服务应用,然后针对该应用详细讲解了如何构建 Docker 镜像。要构建镜像首先需要编写 Dockerfile,但基础的 Dockerfile 体积过大,所以我们又学习了如何通过多阶段构建减小镜像体积。在多阶段构建时,由于构建机和部署服务器可能存在操作系统和 CPU 架构的差异,又学习了如何通过交叉编译构建出可在其他平台直接使用的二进制文件。最后由于整个构建流程涉及命令比较多,真实 Go 项目可能构建流程会更复杂,所以学习了如何通过 Makefile 简化构建流程。


最后感谢你的阅读,希望本文的内容能让你有所收获。


参考


目录
相关文章
|
1天前
|
存储 安全 Ubuntu
docker中的镜像
【10月更文挑战第1天】
15 4
|
1天前
|
定位技术 文件存储 网络架构
Docker部署PhotoPrism、Immich图片管理应用,无需公网IP远程访问教程
除了Synology、QNAP、TerraMaster等品牌的NAS设备内置图库功能,市面上还有多种备受欢迎的第三方应用,如PhotoPrism、Immich、LibrePhotos、Piwigo、Photoview等。这些应用不仅提供强大的图片管理能力,还可通过Docker轻松部署。借助贝锐花生壳服务,即使没有公网IP也能实现远程访问,突破地理限制,提升数据可访问性和安全性,让用户随时随地管理私人图库。
15 1
|
2天前
|
Docker 容器
docker的导入本地镜像和导出本地镜像
本文介绍了如何使用Docker对本地镜像进行导入和导出操作,包括从本地导入`nginx.tar`镜像以及将`open-webui`镜像导出并压缩为`open-webui.tar.gz`。
10 1
|
2天前
|
前端开发 应用服务中间件 nginx
docker运行nginx镜像
这篇文章详细说明了如何在Docker中部署并运行Nginx服务,包括拉取镜像、配置文件的挂载以及容器的启动配置。
26 0
docker运行nginx镜像
|
2天前
|
存储 应用服务中间件 开发工具
docker镜像上传至Harbor及从Harbor下载
docker镜像上传至Harbor及从Harbor下载
|
2天前
|
缓存 NoSQL Redis
docker运行redis镜像
这篇文章介绍了如何使用Docker运行Redis镜像,并提供了启动和配置Redis容器的具体命令和步骤。
17 0
|
2天前
|
NoSQL Redis Docker
Docker获取镜像和运行镜像
这篇文章介绍了如何使用Docker获取镜像以及运行镜像的具体步骤和命令。
36 0
|
存储 分布式计算 Hadoop
基于docker的Hadoop环境搭建与应用实践(脚本部署)
本文介绍了Hadoop环境的搭建与应用实践。对Hadoop的概念和原理进行了简要说明,包括HDFS分布式文件系统和MapReduce计算模型等,主要通过脚本的方式进行快捷部署,在部署完成后对HDFS和mapreduce进行了测试,确保其功能正常。
|
2月前
|
运维 开发者 Docker
Docker容器化技术在运维中的应用实践
【8月更文挑战第27天】本文旨在探讨Docker容器化技术如何在现代运维工作中发挥核心作用,通过深入浅出的方式介绍Docker的基本概念、优势以及实际应用场景。文章将结合具体案例,展示如何利用Docker简化部署流程、提高资源利用率和加强应用的可移植性。读者将获得对Docker容器技术在实际运维中应用的全面认识,并能够理解其在提升运维效率与质量方面的重要性。
|
2月前
|
数据可视化 数据安全/隐私保护 开发者
堪称最优秀的Docker可视化管理工具——Portainer深度解析与应用实践
【8月更文挑战第7天】在容器化技术日益盛行的今天,Docker以其轻量级、可移植性和灵活性的优势,成为了开发者和管理员的首选。然而,随着Docker容器的增多,如何高效地管理和监控这些容器成为了一个挑战。Portainer,作为一款开源的Docker可视化管理工具,凭借其直观的操作界面和强大的功能,赢得了广泛的赞誉。今天,我们就来深入探讨Portainer的使用技巧,看看你是否真的会用它。
95 0