构建您的 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.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 构建过程可以访问上下文中包含的任何文件。
构建命令可选地接受 --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
这是因为您在构建的第二阶段使用的 “无发行版” 基础镜像非常精简,专为静态二进制文件的精益部署而设计。
多阶段构建还有更多内容,包括多架构构建的可能性,因此请随意查看多阶段构建。然而,这对于您在这里的进展并非必不可少。
后续步骤
在本模块中,您了解了您的示例应用程序并为其构建了容器镜像。
在下一个模块中,您将了解如何将您的镜像作为容器运行。