构建你的 Go 镜像

概述

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

所需软件

要完成本教程,您需要具备以下条件:

  • Docker 在本地运行。按照说明下载并安装 Docker
  • 用于编辑文件的 IDE 或文本编辑器。 Visual Studio Code是一个免费且流行的选择,但您可以使用任何您觉得舒服的东西。
  • 一个 Git 客户端。本指南使用基于命令行的git客户端,但您可以自由使用适合您的任何内容。
  • 命令行终端应用程序。本模块中显示的示例来自 Linux shell,但它们应该可以在 PowerShell、Windows 命令提示符或 OS X 终端中运行,只需进行最少的修改(如果有的话)。

了解示例应用程序

该示例应用程序是微服务的漫画。有目的地专注于学习 Go 应用程序容器化的基础知识是很琐碎的事情。

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

  • 它以包含心形符号 ( ) 的字符串响应 的<3请求/
  • 它使用{"Status" : "OK"}JSON 响应对/health.

它以 HTTP 错误 404 响应任何其他请求。

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

该应用程序是无状态的。

该应用程序的完整源代码位于 GitHub 上: github.com/docker/docker-gs-ping。我们鼓励您分叉它并根据需要进行尝试。

要继续,请将应用程序存储库克隆到本地计算机:

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

main.go如果您熟悉 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.com/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.mod文件go.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命令使用通配符将.go主机当前目录(所在目录Dockerfile )中扩展名的所有文件复制到镜像内的当前目录中。

现在,要编译您的应用程序,请使用熟悉的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://docker.github.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://docker.github.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创建 Docker 映像。Dockerfile构建上下文是位于指定路径或 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
...

您的确切输出可能会有所不同,但您应该会看到docker-gs-ping带有latest标签的图像。由于您在构建映像时没有指定自定义标签,因此 Docker 假定该标签为latest,这是一个特殊值。

标记图像

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

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

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

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

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

现在再次运行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

之所以如此,是因为 您在构建的第二阶段使用的“distroless”基础映像非常简单,并且是为静态二进制文件的精益部署而设计的。

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

下一步

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

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