如何构建 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 简化构建流程。


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


参考


目录
相关文章
|
7天前
|
关系型数据库 MySQL Shell
4.Docker 应用部署
4.Docker 应用部署
|
7天前
|
存储 缓存 NoSQL
【Go语言专栏】Go语言中的Redis操作与缓存应用
【4月更文挑战第30天】本文探讨了在Go语言中使用Redis进行操作和缓存应用的方法。文章介绍了Redis作为高性能键值存储系统,用于提升应用性能。推荐使用`go-redis/redis`库,示例代码展示了连接、设置、获取和删除键值对的基本操作。文章还详细阐述了缓存应用的步骤及常见缓存策略,包括缓存穿透、缓存击穿和缓存雪崩的解决方案。利用Redis和合适策略可有效优化应用性能。
|
1天前
|
运维 Linux 数据安全/隐私保护
Docker详解(九)——Docker镜像发布
Docker详解(九)——Docker镜像发布
11 2
|
1天前
|
运维 Linux Apache
Docker详解(八)——Docker镜像制作
Docker详解(八)——Docker镜像制作
26 1
|
2天前
|
负载均衡 监控 Go
Golang深入浅出之-Go语言中的服务网格(Service Mesh)原理与应用
【5月更文挑战第5天】服务网格是处理服务间通信的基础设施层,常由数据平面(代理,如Envoy)和控制平面(管理配置)组成。本文讨论了服务发现、负载均衡和追踪等常见问题及其解决方案,并展示了使用Go语言实现Envoy sidecar配置的例子,强调Go语言在构建服务网格中的优势。服务网格能提升微服务的管理和可观测性,正确应对问题能构建更健壮的分布式系统。
21 1
|
4天前
|
安全 Go
Golang深入浅出之-Go语言中的并发安全队列:实现与应用
【5月更文挑战第3天】本文探讨了Go语言中的并发安全队列,它是构建高性能并发系统的基础。文章介绍了两种实现方法:1) 使用`sync.Mutex`保护的简单队列,通过加锁解锁确保数据一致性;2) 使用通道(Channel)实现无锁队列,天生并发安全。同时,文中列举了并发编程中常见的死锁、数据竞争和通道阻塞问题,并给出了避免这些问题的策略,如明确锁边界、使用带缓冲通道、优雅处理关闭以及利用Go标准库。
20 5
|
6天前
|
JSON 监控 安全
Golang深入浅出之-Go语言中的反射(reflect):原理与实战应用
【5月更文挑战第1天】Go语言的反射允许运行时检查和修改结构,主要通过`reflect`包的`Type`和`Value`实现。然而,滥用反射可能导致代码复杂和性能下降。要安全使用,应注意避免过度使用,始终进行类型检查,并尊重封装。反射的应用包括动态接口实现、JSON序列化和元编程。理解反射原理并谨慎使用是关键,应尽量保持代码静态类型。
20 2
|
7天前
|
存储 负载均衡 监控
【Go 语言专栏】构建高可靠性的 Go 语言服务架构
【4月更文挑战第30天】本文探讨了如何利用Go语言构建高可靠性的服务架构。Go语言凭借其高效、简洁和并发性能,在构建服务架构中备受青睐。关键要素包括负载均衡、容错机制、监控预警、数据存储和服务治理。文章详细阐述了实现这些要素的具体步骤,通过实际案例分析和应对挑战的策略,强调了Go语言在构建稳定服务中的作用,旨在为开发者提供指导。
|
7天前
|
存储 缓存 监控
【Go语言专栏】Go语言应用的性能调优实践
【4月更文挑战第30天】本文介绍了Go语言应用的性能调优技巧,包括使用`pprof`进行性能分析、选择正确算法与数据结构、减少内存分配、优化并发及避免阻塞操作、选用合适锁机制。此外,文章还提到了编译选项如`-trimpath`和`-ldflags`,以及系统资源和环境调优。通过实例展示了代码优化、并发处理和锁的使用。最后,推荐了进一步学习资源,鼓励读者深入探索Go语言的性能优化。
|
7天前
|
存储 NoSQL Go
【Go语言专栏】Go语言中的MongoDB操作与NoSQL应用
【4月更文挑战第30天】本文介绍了Go语言中操作MongoDB的方法和NoSQL应用的优势。MongoDB作为流行的NoSQL数据库,以其文档型数据模型、高性能和可扩展性被广泛应用。在Go语言中,通过mongo-go-driver库可轻松实现与MongoDB的连接及插入、查询、更新和删除等操作。MongoDB在NoSQL应用中的优点包括灵活的数据模型、高性能、高可用性和易于扩展,使其成为处理大规模数据和高并发场景的理想选择。