构建你的 Go 镜像

概述

在本节中,你将构建一个容器镜像。该镜像包含运行应用程序所需的一切——编译后的应用程序二进制文件、运行时、库以及应用程序所需的所有其他资源。

所需软件

要完成本教程,你需要以下内容:

  • 本地运行的 Docker。请按照说明下载并安装 Docker
  • 用于编辑文件的 IDE 或文本编辑器。Visual Studio Code是一个免费且流行的选择,但你可以使用任何你感觉舒适的工具。
  • 一个 Git 客户端。本指南使用基于命令行的git客户端,但你可以随意使用任何适合你的客户端。
  • 一个命令行终端应用程序。本模块中显示的示例来自 Linux shell,但它们应该在 PowerShell、Windows 命令提示符或 OS X 终端中都能正常工作,最多只需要进行少量修改。

了解示例应用程序

示例应用程序是微服务的仿制品。它故意很简单,以便专注于学习 Go 应用程序容器化的基础知识。

应用程序提供两个 HTTP 端点:

  • 它对/的请求返回包含心形符号 (<3) 的字符串。
  • 它对/health的请求返回{"Status" : "OK"} JSON。

它对任何其他请求返回 HTTP 错误 404。

应用程序监听由环境变量PORT的值定义的 TCP 端口。默认值为8080

应用程序是无状态的。

应用程序的完整源代码位于 GitHub 上:github.com/docker/docker-gs-ping。我们鼓励你复制它并随意进行实验。

要继续,请将应用程序存储库克隆到你的本地机器。

$ git clone https://github.com/docker/docker-gs-ping

如果你熟悉 Go,应用程序的main.go文件很简单。

package main

import (
	"net/http"
	"os"

	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
)

func main() {

	e := echo.New()

	e.Use(middleware.Logger())
	e.Use(middleware.Recover())

	e.GET("/", func(c echo.Context) error {
		return c.HTML(http.StatusOK, "Hello, Docker! <3")
	})

	e.GET("/health", func(c echo.Context) error {
		return c.JSON(http.StatusOK, struct{ Status string }{Status: "OK"})
	})

	httpPort := os.Getenv("PORT")
	if httpPort == "" {
		httpPort = "8080"
	}

	e.Logger.Fatal(e.Start(":" + httpPort))
}

// Simple implementation of an integer minimum
// Adapted from: https://gobyexample.golang.ac.cn/testing-and-benchmarking
func IntMin(a, b int) int {
	if a < b {
		return a
	}
	return b
}

为应用程序创建 Dockerfile

要使用 Docker 构建容器镜像,需要包含构建指令的Dockerfile

你的Dockerfile以(可选)解析器指令行开头,该指令指示 BuildKit 根据指定语法版本的语法规则来解释你的文件。

然后,你告诉 Docker 你想为你的应用程序使用什么基础镜像。

# syntax=docker/dockerfile:1

FROM golang:1.19

Docker 镜像可以从其他镜像继承。因此,与其从头开始创建自己的基础镜像,不如使用官方的 Go 镜像,它已经拥有编译和运行 Go 应用程序所需的所有工具和库。

注意

如果你对创建自己的基础镜像感兴趣,可以查看本指南的以下部分:创建基础镜像。但是,要继续执行手头的任务,这并非必要。

现在你已经为即将创建的容器镜像定义了基础镜像,你可以开始在其基础上构建了。

为了在运行其余命令时更方便,请在正在构建的镜像中创建一个目录。这还指示 Docker 将此目录用作所有后续命令的默认目标。这样,你就不必在Dockerfile中键入完整的文件路径,相对路径将基于此目录。

WORKDIR /app

通常,一旦你下载了用 Go 编写的项目,首先要做的事情就是安装编译所需的模块。请注意,基础镜像中已经有了工具链,但是你的源代码还没有在其中。

因此,在可以在你的镜像中运行go mod download之前,你需要将你的go.modgo.sum文件复制到其中。使用COPY命令来执行此操作。

COPY命令最简单的形式采用两个参数。第一个参数告诉 Docker 你想将哪些文件复制到镜像中。最后一个参数告诉 Docker 你想将该文件复制到哪里。

go.modgo.sum文件复制到你的项目目录/app中,由于你使用了WORKDIR,所以在镜像内它是当前目录 (./)。与一些似乎对尾部斜杠 (/) 的使用漠不关心的现代 shell 不同,它们大多数情况下都能弄清楚用户的意思,Docker 的COPY命令对其对尾部斜杠的解释非常敏感。

COPY go.mod go.sum ./

注意

如果你想熟悉COPY命令对尾部斜杠的处理方式,请参阅Dockerfile 参考。这个尾部斜杠可能比你想象的更以多种方式造成问题。

现在你已经在正在构建的 Docker 镜像中有了模块文件,你也可以使用RUN命令在那里运行命令go mod download。这与在本地机器上运行go完全相同,但是这次这些 Go 模块将安装到镜像中的一个目录中。

RUN go mod download

此时,你已在镜像中安装了 Go 工具链版本 1.19.x 和所有 Go 依赖项。

接下来你需要做的是将你的源代码复制到镜像中。你将像之前使用模块文件一样使用COPY命令。

COPY *.go ./

COPY命令使用通配符将主机上当前目录(Dockerfile所在的目录)中所有扩展名为.go的文件复制到镜像中的当前目录。

现在,要编译你的应用程序,请使用熟悉的RUN命令。

RUN CGO_ENABLED=0 GOOS=linux go build -o /docker-gs-ping

这应该很熟悉。该命令的结果将是一个名为docker-gs-ping的静态应用程序二进制文件,位于正在构建的镜像的文件系统的根目录中。你可以将二进制文件放在镜像中你想要的任何其他位置,根目录在这方面没有任何特殊含义。使用它只是为了使文件路径更短,从而提高可读性。

现在,剩下要做的就是告诉 Docker 在使用你的镜像启动容器时要运行什么命令。

你可以使用CMD命令来实现。

CMD ["/docker-gs-ping"]

这是完整的Dockerfile

# syntax=docker/dockerfile:1

FROM golang:1.19

# Set destination for COPY
WORKDIR /app

# Download Go modules
COPY go.mod go.sum ./
RUN go mod download

# Copy the source code. Note the slash at the end, as explained in
# https://docs.docker.top/reference/dockerfile/#copy
COPY *.go ./

# Build
RUN CGO_ENABLED=0 GOOS=linux go build -o /docker-gs-ping

# Optional:
# To bind to a TCP port, runtime parameters must be supplied to the docker command.
# But we can document in the Dockerfile what ports
# the application is going to listen on by default.
# https://docs.docker.top/reference/dockerfile/#expose
EXPOSE 8080

# Run
CMD ["/docker-gs-ping"]

Dockerfile也可以包含注释。它们总是以#符号开头,并且必须位于一行的开头。注释是为了方便你记录你的Dockerfile

还有一个 Dockerfile 指令的概念,例如你添加的syntax指令。指令必须始终位于Dockerfile的最顶部,因此在添加注释时,请确保注释位于你可能使用过的任何指令之后。

# syntax=docker/dockerfile:1
# A sample microservice in Go packaged into a container image.

FROM golang:1.19

# ...

构建镜像

现在你已经创建了Dockerfile,请从中构建一个镜像。docker build命令根据Dockerfile和上下文创建 Docker 镜像。构建上下文是在指定路径或 URL 中找到的文件集。Docker 构建过程可以访问上下文中找到的任何文件。

构建命令可以选择使用--tag标志。此标志用于使用易于人类读取和识别的字符串值标记镜像。如果你不传递--tag,Docker 将使用latest作为默认值。

构建你的第一个 Docker 镜像。

$ docker build --tag docker-gs-ping .

构建过程将在执行构建步骤时打印一些诊断消息。以下是这些消息可能的样子示例。

[+] Building 2.2s (15/15) FINISHED
 => [internal] load build definition from Dockerfile                                                                                       0.0s
 => => transferring dockerfile: 701B                                                                                                       0.0s
 => [internal] load .dockerignore                                                                                                          0.0s
 => => transferring context: 2B                                                                                                            0.0s
 => resolve image config for docker.io/docker/dockerfile:1                                                                                 1.1s
 => CACHED docker-image://docker.io/docker/dockerfile:1@sha256:39b85bbfa7536a5feceb7372a0817649ecb2724562a38360f4d6a7782a409b14            0.0s
 => [internal] load build definition from Dockerfile                                                                                       0.0s
 => [internal] load .dockerignore                                                                                                          0.0s
 => [internal] load metadata for docker.io/library/golang:1.19                                                                             0.7s
 => [1/6] FROM docker.io/library/golang:1.19@sha256:5d947843dde82ba1df5ac1b2ebb70b203d106f0423bf5183df3dc96f6bc5a705                       0.0s
 => [internal] load build context                                                                                                          0.0s
 => => transferring context: 6.08kB                                                                                                        0.0s
 => CACHED [2/6] WORKDIR /app                                                                                                              0.0s
 => CACHED [3/6] COPY go.mod go.sum ./                                                                                                     0.0s
 => CACHED [4/6] RUN go mod download                                                                                                       0.0s
 => CACHED [5/6] COPY *.go ./                                                                                                              0.0s
 => CACHED [6/6] RUN CGO_ENABLED=0 GOOS=linux go build -o /docker-gs-ping                                                                  0.0s
 => exporting to image                                                                                                                     0.0s
 => => exporting layers                                                                                                                    0.0s
 => => writing image sha256:ede8ff889a0d9bc33f7a8da0673763c887a258eb53837dd52445cdca7b7df7e3                                               0.0s
 => => naming to docker.io/library/docker-gs-ping                                                                                          0.0s

你的实际输出可能会有所不同,但如果没有错误,你应该会在输出的第一行看到单词FINISHED。这意味着 Docker 已成功构建名为docker-gs-ping的镜像。

查看本地镜像

要查看本地机器上的镜像列表,你有两种选择。一种是使用命令行界面 (CLI),另一种是使用Docker Desktop。由于你目前正在终端中工作,让我们看看如何使用 CLI 列出镜像。

要列出镜像,请运行docker image ls命令(或简写docker images

$ docker image ls

REPOSITORY                       TAG       IMAGE ID       CREATED         SIZE
docker-gs-ping                   latest    7f153fbcc0a8   2 minutes ago   1.11GB
...

你的实际输出可能会有所不同,但你应该会看到带有latest标签的docker-gs-ping镜像。因为你在构建镜像时没有指定自定义标签,所以 Docker 假设标签为latest,这是一个特殊值。

标记镜像

镜像名称由斜杠分隔的名称组件组成。名称组件可以包含小写字母、数字和分隔符。分隔符定义为句点、一个或两个下划线或一个或多个短横线。名称组件不能以分隔符开头或结尾。

镜像由清单和一系列层组成。简单来说,标签指向这些构件的组合。你可以为镜像添加多个标签,事实上,大多数镜像都有多个标签。为已构建的镜像创建一个第二个标签,并查看其层。

使用docker image tag(或简写docker tag)命令为你的镜像创建新的标签。此命令需要两个参数;第一个参数是源镜像,第二个参数是要创建的新标签。以下命令为已构建的docker-gs-ping:latest镜像创建新的docker-gs-ping:v1.0标签

$ docker image tag docker-gs-ping:latest docker-gs-ping:v1.0

Docker 的tag命令为镜像创建新的标签。它不会创建新的镜像。该标签指向同一个镜像,只是引用镜像的另一种方式。

现在再次运行docker image ls命令以查看更新的本地镜像列表。

$ docker image ls

REPOSITORY                       TAG       IMAGE ID       CREATED         SIZE
docker-gs-ping                   latest    7f153fbcc0a8   6 minutes ago   1.11GB
docker-gs-ping                   v1.0      7f153fbcc0a8   6 minutes ago   1.11GB
...

你可以看到有两个以docker-gs-ping开头的镜像。你知道它们是同一个镜像,因为如果你查看IMAGE ID列,你会发现这两个镜像的值相同。此值是 Docker 用于内部识别镜像的唯一标识符。

删除刚刚创建的标签。为此,你将使用docker image rm命令,或简写docker rmi(代表“remove image”)

$ docker image rm docker-gs-ping:v1.0
Untagged: docker-gs-ping:v1.0

请注意,Docker 的响应会告诉你镜像并没有被删除,只是取消了标签。

通过运行以下命令来验证这一点

$ docker image ls

你会看到标签v1.0不再出现在你的 Docker 实例保留的镜像列表中。

REPOSITORY                       TAG       IMAGE ID       CREATED         SIZE
docker-gs-ping                   latest    7f153fbcc0a8   7 minutes ago   1.11GB
...

标签v1.0已被删除,但你仍然可以在你的机器上使用docker-gs-ping:latest标签,因此镜像仍然存在。

多阶段构建

你可能已经注意到,你的docker-gs-ping镜像大小超过 1GB,对于一个微小的已编译 Go 应用程序来说,这很大。你可能还会想知道,在你构建镜像后,完整的 Go 工具套件(包括编译器)发生了什么。

答案是完整的工具链仍然存在于容器镜像中。这不仅因为文件大小很大而不方便,而且在部署容器时也可能存在安全风险。

可以使用多阶段构建来解决这两个问题。

简而言之,多阶段构建可以将构件从一个构建阶段转移到另一个构建阶段,并且每个构建阶段都可以从不同的基础镜像实例化。

因此,在下面的示例中,你将使用一个完整的官方 Go 镜像来构建你的应用程序。然后,你将应用程序二进制文件复制到另一个镜像中,该镜像的基础非常精简,不包含 Go 工具链或其他可选组件。

示例应用程序存储库中的Dockerfile.multistage包含以下内容

# syntax=docker/dockerfile:1

# Build the application from source
FROM golang:1.19 AS build-stage

WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY *.go ./

RUN CGO_ENABLED=0 GOOS=linux go build -o /docker-gs-ping

# Run the tests in the container
FROM build-stage AS run-test-stage
RUN go test -v ./...

# Deploy the application binary into a lean image
FROM gcr.io/distroless/base-debian11 AS build-release-stage

WORKDIR /

COPY --from=build-stage /docker-gs-ping /docker-gs-ping

EXPOSE 8080

USER nonroot:nonroot

ENTRYPOINT ["/docker-gs-ping"]

由于你现在有两个 Dockerfile,你必须告诉 Docker 你想使用哪个 Dockerfile 来构建镜像。使用multistage标记新镜像。这个标签(除了latest之外的任何标签)对 Docker 没有特殊意义,它只是你选择的一个标签。

$ docker build -t docker-gs-ping:multistage -f Dockerfile.multistage .

比较docker-gs-ping:multistagedocker-gs-ping:latest的大小,你会看到数量级的差异。

$ docker image ls
REPOSITORY       TAG          IMAGE ID       CREATED              SIZE
docker-gs-ping   multistage   e3fdde09f172   About a minute ago   28.1MB
docker-gs-ping   latest       336a3f164d0f   About an hour ago    1.11GB

这是因为你在构建的第二阶段使用的"distroless"基础镜像非常精简,专为静态二进制文件的精简部署而设计。

多阶段构建还有更多内容,包括多架构构建的可能性,因此请随时查看多阶段构建。但是,这对于你在此处的进度来说并不是必需的。

后续步骤

在本模块中,你学习了示例应用程序,并为其构建了容器镜像。

在下一个模块中,你将了解如何将你的镜像作为容器运行。