构建 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
该应用程序的 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.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.19Docker 镜像可以从其他镜像继承。因此,您可以使用已经包含编译和运行 Go 应用程序所需的所有工具和库的官方 Go 镜像,而不是从头开始创建自己的基础镜像。
注意
如果您对创建自己的基础镜像感兴趣,您可以查看本指南的以下部分:创建基础镜像。但是,请注意,这对于继续手头的任务来说不是必需的。
现在您已经为即将创建的容器镜像定义了基础镜像,您可以开始在其基础上进行构建。
为了在运行其余命令时更轻松,请在要构建的镜像中创建一个目录。这也指示 Docker 将此目录用作所有后续命令的默认目标。这样,您就不必在 Dockerfile 中键入完整的路径,相对路径将基于此目录。
WORKDIR /app通常,您在下载 Go 编写的项目后,首先要做的就是安装编译它所需的模块。请注意,基础镜像已经拥有工具链,但您的源代码还没有。
因此,在您可以在镜像中运行 go mod download 之前,您需要将 go.mod 和 go.sum 文件复制到其中。使用 COPY 命令来完成此操作。
在最简单的情况下,COPY 命令接受两个参数。第一个参数告诉 Docker 您想要复制到镜像中的文件。最后一个参数告诉 Docker 您想要将该文件复制到哪里。
将 go.mod 和 go.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 构建过程可以访问上下文中的任何文件。
build 命令可以选择接受 --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:multistage 和 docker-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" 基础图像非常精简,专为静态二进制文件的精简部署而设计。
多阶段构建还有很多内容,包括多架构构建的可能性,因此您可以随意查看 多阶段构建。但是,这对于您在此处的进展来说并不是必需的。
下一步
在本模块中,您遇到了示例应用程序,并为其构建了容器图像。
在下一个模块中,您将了解如何将您的图像作为容器运行。