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


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


参考


目录
相关文章
|
8天前
|
缓存 监控 持续交付
|
5天前
|
Kubernetes 负载均衡 Docker
构建高效微服务架构:Docker与Kubernetes的完美搭档
本文介绍了Docker和Kubernetes在构建高效微服务架构中的应用,涵盖基本概念、在微服务架构中的作用及其实现方法。通过具体实例,如用户服务、商品服务和订单服务,展示了如何利用Docker和Kubernetes实现服务的打包、部署、扩展及管理,确保微服务架构的稳定性和可靠性。
33 7
|
4天前
|
Kubernetes 负载均衡 Docker
构建高效微服务架构:Docker与Kubernetes的完美搭档
【10月更文挑战第22天】随着云计算和容器技术的快速发展,微服务架构逐渐成为现代企业级应用的首选架构。微服务架构将一个大型应用程序拆分为多个小型、独立的服务,每个服务负责完成一个特定的功能。这种架构具有灵活性、可扩展性和易于维护的特点。在构建微服务架构时,Docker和Kubernetes是两个不可或缺的工具,它们可以完美搭档,为微服务架构提供高效的支持。本文将从三个方面探讨Docker和Kubernetes在构建高效微服务架构中的应用:一是Docker和Kubernetes的基本概念;二是它们在微服务架构中的作用;三是通过实例讲解如何使用Docker和Kubernetes构建微服务架构。
25 6
|
3天前
|
负载均衡 应用服务中间件 nginx
基于Nginx和Consul构建自动发现的Docker服务架构——非常之详细
通过使用Nginx和Consul构建自动发现的Docker服务架构,可以显著提高服务的可用性、扩展性和管理效率。Consul实现了服务的自动注册与发现,而Nginx则通过动态配置实现了高效的反向代理与负载均衡。这种架构非常适合需要高可用性和弹性扩展的分布式系统。
13 4
|
4天前
|
负载均衡 应用服务中间件 nginx
基于Nginx和Consul构建自动发现的Docker服务架构——非常之详细
通过使用Nginx和Consul构建自动发现的Docker服务架构,可以显著提高服务的可用性、扩展性和管理效率。Consul实现了服务的自动注册与发现,而Nginx则通过动态配置实现了高效的反向代理与负载均衡。这种架构非常适合需要高可用性和弹性扩展的分布式系统。
16 3
|
4天前
|
存储 缓存 Java
Java应用瘦身记:Docker镜像从674MB优化至58MB的实践指南
【10月更文挑战第22天】 在容器化时代,Docker镜像的大小直接影响到应用的部署速度和运行效率。一个轻量级的Docker镜像可以减少存储成本、加快启动时间,并提高资源利用率。本文将分享如何将一个Java基础Docker镜像从674MB缩减到58MB的实践经验。
11 1
|
9天前
|
jenkins 测试技术 持续交付
Docker最佳实践:构建高效的CI/CD流水线
【10月更文挑战第17天】在现代软件开发实践中,持续集成(Continuous Integration, CI)和持续部署(Continuous Deployment, CD)已成为提高开发效率和软件质量的重要手段。Docker作为一种容器技术,为构建一致且隔离的开发环境提供了强有力的支撑。本文将探讨如何利用Docker来优化CI/CD流程,包括构建环境的标准化、镜像管理以及与CI/CD工具(如Jenkins、GitLab CI)的集成。
30 5
|
8天前
|
JavaScript Docker Python
下个时代的开发工具-Nix:声明式的运行环境构建器、简单场景下的docker替身
Nix 是一个独特的包管理工具和构建系统,采用声明式方法管理软件包和运行环境。它通过精确控制依赖关系和环境配置,确保软件的可重复性、隔离性和可追溯性。Nix 支持多语言开发环境,提供声明式配置、环境隔离、回滚与版本控制等核心功能,适用于复杂开发场景,有效解决依赖冲突和环境不一致问题。
|
11天前
|
Docker 容器
docker:记录如何在x86架构上构造和使用arm架构的镜像
为了实现国产化适配,需将原x86平台上的Docker镜像转换为适用于ARM平台的镜像。本文介绍了如何配置Docker buildx环境,包括检查Docker版本、安装buildx插件、启用实验性功能及构建多平台镜像的具体步骤。通过这些操作,可以在x86平台上成功构建并运行ARM64镜像,实现跨平台的应用部署。
61 2
|
3月前
|
存储 安全 Ubuntu
Docker 镜像与 Docker 容器的区别
【8月更文挑战第27天】
235 5