构建您的 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.container.net.cn/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.container.net.cn/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(代表“移除镜像”)

$ 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 镜像大小超过 1 GB,这对于一个微小的编译 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

这是因为您在构建的第二阶段使用的 “无发行版” 基础镜像非常精简,专为静态二进制文件的精益部署而设计。

多阶段构建还有更多内容,包括多架构构建的可能性,因此请随意查看多阶段构建。然而,这对于您在这里的进展并非必不可少。

后续步骤

在本模块中,您了解了您的示例应用程序并为其构建了容器镜像。

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

© . This site is unofficial and not affiliated with Kubernetes or Docker Inc.