云计算:Docker 部署 Go 应用
构建镜像
下载地址:git clone https://github.com/olliefr/docker-gs-ping
创建 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("/ping", func(c echo.Context) error {
return c.JSON(http.StatusOK, struct{ Status string }{Status: "OK"})
})
httpPort := os.Getenv("HTTP_PORT")
if httpPort == "" {
httpPort = "8080"
}
e.Logger.Fatal(e.Start(":" + httpPort))
}
- 当请求
/
时它返回心形形状的文本内容 (“❤️”) - 正确请求
/ping
时,它返回{"Status" : "OK"}
的 JSON 字符串 - 通过使用环境变量
HTTP_PORT
来配置监听端口,默认端口为8080
go run main.go
curl http://localhost:8080/
创建 Dockerfile
在项目的根目录下创建一个名为 Dockerfile 的文件(没有后缀)
向 Dockerfile 添加的第一行是 # syntax parser 指令。
虽然这个指令是可选的,但它指示 Docker 构建器在解析 Dockerfile 时使用什么语法,并允许使用 BuildKit 的旧版本在开始构建之前升级解析器。
解析器指令必须出现在 Dockerfile 中的任何其他注释、空格或 Dockerfile 指令之前,并且应该位于 Dockerfiles 的第一行。
# syntax=docker/dockerfile:1
我们建议使用 docker/dockerfile:1,它总是指向版本1语法的最新版本。
BuildKit 会在构建之前自动检查语法更新,确保您使用的是最新版本。
接下来,我们需要在 Dockerfile 中添加一行,告诉 Docker 我们希望为应用程序使用什么基本镜像。
FROM golang:1.16-alpine
Docker 镜像可以从其他镜像继承。
因此,我们不需要创建自己的基本镜像,而是使用官方的 Go 映像,它已经拥有编译和运行 Go 应用程序的所有工具和包。
当我们使用 FROM
命令时,我们告诉 Docker 在我们的镜像中包含 golang:1.16-alpine 镜像的所有功能。
我们所有的后续命令都建立在这个“基础”映像之上。
为了简化运行其余命令时的工作,我们在构建的镜像中创建一个目录。
这指示 Docker 使用此目录作为所有后续命令的默认目标。
这样我们就不必输入完整的文件路径,而是可以根据这个目录使用相对路径。
WORKDIR /app
通常,一旦你下载了一个用 Go 编写的项目,你要做的第一件事就是安装编译它所需要的模块。
但是在运行 go mod download
之前,我们需要将 go.mod
和 go.sum
文件复制到镜像中。
我们使用 COPY
命令来实现这一点。
在最简单的形式中,COPY
命令有两个参数。
第一个参数告诉 Docker 要将哪些文件复制到镜像中。最后一个参数告诉 Docker 您希望将该文件复制到何处。
我们将 go.mod
和 go.sum
文件复制到我们的项目目录 /app
中,由于我们使用了 WORKDIR
,/app
这个目录就是镜像里面的当前目录 ./
。
COPY go.mod ./
COPY go.sum ./
现在我们已经在构建的 Docker 镜像中有了模块文件,我们可以使用 RUN
命令执行命令 go mod download
。
这与我们在本地机器上运行 Go 的工作方式完全相同,但是这次这些 Go 模块将被安装到镜像中的一个目录中。
RUN go mod download
现在,我们有了一个基于 Go 1.16
环境的镜像,并且我们已经安装了依赖项。
下一步我们需要做的是将源代码复制到镜像中。
我们将使用 COPY
命令,就像我们之前对模块文件所做的那样。
COPY *.go ./
此 COPY
命令使用通配符复制所有 go 文件到镜像内部的工作目录。
现在,我们要编译我们的应用程序
RUN go build -o /docker-gs-ping
该命令的结果是一个名为 docker-gs-ping
的二进制文件,它位于我们正在构建的映像的文件系统的根目录中。
我们可以将二进制文件放到镜像中任何我们想要的地方,根目录在这方面没有特殊意义。
使用它来保存较短的文件路径可以提高可读性。
现在,剩下要做的就是告诉 Docker 当我们的镜像用于启动容器时执行什么命令。
我们使用 CMD
命令来实现:
CMD [ "/docker-gs-ping" ]
下面是完整的 Dockerfile:
# syntax=docker/dockerfile:1
FROM golang:alpine as builder
WORKDIR /app
COPY go.mod ./
COPY go.sum ./
ENV GOPROXY https://goproxy.cn,direct
RUN go mod download
COPY *.go ./
RUN go build -o /docker-gs-ping
EXPOSE 8080
CMD [ "/docker-gs-ping" ]
文件中的注释以 #
符号开头,并且必须在一行的开头。
文件指令,例如我们添加的 syntax 指令,必须始终位于 Dockerfile 的顶部。
构建镜像
现在我们已经创建了 Dockerfile,让我们从它构建一个映像。
docker build
命令根据 Dockerfile 和一个 “context” 创建 Docker 镜像。
生成上下文是位于指定路径或 URL 中的一组文件。Docker 构建过程可以访问上下文中的任何文件。
Build 命令可以选择使用 --tag
标志。这个标志用于给镜像标上字符串值,这易于阅读和识别。
如果不传递 --tag
,Docker 将使用 latest 作为默认值。
让我们构建我们的第一个 Docker 映像!
docker build --tag docker-gs-ping:latest .
您应该可以在构建输出中看到 FINISHED 行。
这意味着 Docker 已经成功地构建了我们的映像,并为它分配了一个 docker-gs-ping
标记。
查看本地镜像
要查看本地机器上的镜像列表,我们有两个选项。
一种是使用 CLI,另一种是使用 Docker 桌面。
因为我们目前正在终端中工作,所以让我们来看一下 CLI 中列出的图像。
若要列出图像,请运行 docker image ls
命令 (或 docker images
简写) :
docker image ls
真实的输出可能会有所不同,但是您应该看到最新标记的 docker-gs-ping
映像。
标签镜像
镜像名称可以包含小写字母、数字和分隔符。
你可以为镜像使用多个标记,事实上,大多数镜像都有多个标记。让我们为构建的图像创建第二个标记,并查看其层。
使用 docker image tag
(或 docker tag
简写)命令为我们的镜像创建一个新标记。
该命令接受两个参数,第一个参数是 “source” 映像,第二个参数是要创建的新标记。
下面的命令为 docker-gs-ping 创建一个新的 docker-gs-ping:v1.0 标记,该标记是我们在上面构建的最新标记:
docker image tag docker-gs-ping:latest docker-gs-ping:v1.0
Docker 标记命令为图像创建一个新标记。它不会创建一个新的图像。标记指向相同的图像,只是引用图像的另一种方式。
现在再次运行 docker image ls 命令,查看本地镜像的更新列表:
docker image ls
您可以看到,我们有两个镜像以 docker-gs-ping 开始。
我们知道它们是相同的镜像,因为如果查看 IMAGE ID 列,您可以看到这两个图像的值是相同的。
这个值是 Docker 内部用来识别镜像的唯一标识符。
让我们移除刚才创建的标记。
要做到这一点,我们将使用 docker image rm 命令,或者简写 docker rmi (表示 “remove image”) :
docker image rm docker-gs-ping:v1.0
请注意,Docker 的回复告诉我们,图像没有被删除,只是 Untagged。通过运行 images 命令来验证这一点:
docker image ls
标记 v1.0已被删除,但我们仍然有 docker-gs-ping:latest 的标记可用于我们的机器,因此映像就在那里。
多阶段构建
您可能已经注意到我们的 docker-gs-ping 映像为 540MB,您可能认为这是一个很大的数字。
您可能还想知道,在编译应用程序二进制代码之后,我们的 dockerized 应用程序是否仍然需要完整的 Go 工具套件,包括编译器。
这些都是合理的担忧。这两个问题都可以通过使用多阶段构建来解决。
下面的例子没有给出什么解释,因为这会使我们偏离我们当前的关注点,但是请随时自行探索。
我们的主要想法是,我们使用一个镜像来生成一些人工制品,然后将其放置到另一个更小的图像中,只包含运行我们所建立的人工制品所必需的部分。
在下面的简单应用仓库中,dockerfile.multigrade 包含以下内容:
# syntax=docker/dockerfile:1
##
## Build
##
FROM golang:alpine as builder
WORKDIR /app
COPY go.mod ./
COPY go.sum ./
RUN go env -w GO111MODULE=on \
&& go env -w GOPROXY=https://goproxy.cn,direct \
&& go env -w CGO_ENABLED=0 \
&& go mod download
COPY *.go ./
RUN go build -o /docker-gs-ping
##
## Deploy
##
FROM alpine:latest as deployer
WORKDIR /
COPY --from=builder /docker-gs-ping /docker-gs-ping
EXPOSE 8080
USER nonroot:nonroot
ENTRYPOINT ["/docker-gs-ping"]
由于我们现在有两个 dockerfiles,我们必须告诉 Docker 我们想要使用新的 Dockerfile 来构建。
我们也会给新镜像加上多级标签,但是这个单词没有特殊含义,我们这样做只是为了把这个新镜像和我们之前构建的镜像进行比较,也就是我们用 latest 标记的那个镜像:
docker build -t docker-gs-ping:multistage -f Dockerfile.multistage .
比较 docker-gs-ping:multistage 的大小和 docker-gs-ping:latest 的大小,我们可以看到一个数量级的差异!
这是因为我们用于部署 Go 应用程序的 “distroless” 基本映像非常轻量,它是用于静态二进制文件的。
有关多阶段构建的更多信息,请随意查看 Docker 文档的其他部分。
运行容器
在终端中执行以下命令
docker run docker-gs-ping
运行此命令时,您将注意到没有返回到命令提示符。
这是因为我们的应用程序是一个 REST 服务器,它将在循环运行,等待传入的请求,而不将控制返回到操作系统,直到容器停止。
让我们使用 curl 命令向服务器发出 GET 请求。
curl http://localhost:8080/
由于与服务器的连接被拒绝,我们的 curl 命令失败。
这意味着我们无法在端口8080上连接到本地主机。
这是意料之中的,因为我们的容器是隔离运行的,其中包括网络。
让我们停止容器并重新启动在本地网络上发布的端口8080。
要停止容器,按 ctrl-c。这将返回到终端提示符。
为了公开容器的端口,我们将在 docker run 命令上使用 --publish 标志(简称-p)。
--publish 命令的格式是 [host_port]:[container_port]。
因此,如果我们希望将容器内的 8080 端口暴露到容器外的 3000 端口,我们将传递 3000:8080 到 --publish
标志。
启动容器并将端口8080暴露给主机上的端口8080。
docker run --publish 8080:8080 docker-gs-ping
现在让我们从上面重新运行 curl 命令。
curl http://localhost:8080/
成功了!我们能够连接到运行在8000港口集装箱内部的应用程序。
切换回运行容器的终端,您应该会看到 GET 请求被记录到控制台。
按 ctrl-c
停止容器。
以分离模式运行
我们的示例应用程序是一个 web 服务器,我们不应该让我们的终端连接到容器。
Docker 可以在分离模式下运行您的容器,即在后台。
为此,我们可以使用 --detach 或简称 -d。
多克尔将启动您的容器与以前相同,但这一次将“分离”从容器和返回到终端提示符。
docker run -d -p 8080:8080 docker-gs-ping
Docker 在后台启动我们的容器,并在终端打印容器 ID。
同样,让我们确保我们的容器正常运行:
curl http://localhost:8080/
列表容器
既然我们在后台运行了容器,那么我们如何知道我们的容器是否正在运行,或者我们的机器上还运行着哪些其他容器呢?
我们可以运行 docker ps 命令。就像在 Linux 上一样,要查看机器上的进程列表,我们将运行 ps 命令。
本着同样的精神,我们可以运行 docker ps 命令,它将显示在我们的机器上运行的容器列表。
docker ps
ps 命令会告诉我们一些关于正在运行的容器的信息。
我们可以看到容器 ID、容器内部运行的映像、创建容器时用于启动容器的命令、状态、公开的端口以及容器的名称。
您可能想知道我们容器的名字是从哪里来的。由于在启动容器时没有为它提供名称,因此 Docker 生成了一个随机名称。
我们马上就能解决这个问题,但是首先我们需要停下容器。
要停止容器,运行 docker stop 命令,这样做可以停止容器。
您将需要传递容器的名称,或者您可以使用容器 ID。
docker stop <container name/container ID>
现在重新运行 docker ps 命令以查看正在运行的容器列表。
docker ps
停止、启动和命名容器
可以启动、停止和重新启动 Docker 容器。
当我们停止一个容器时,它不会被删除,但是状态会被更改为停止,并且容器内的进程会被停止。
当我们运行 docker ps 命令时,默认输出是只显示正在运行的容器。
如果我们传递 --all 或简称 -a,我们将看到系统上的所有容器,即停止运行的容器和正在运行的容器。
docker ps --all
如果您一直在跟踪,那么您应该会看到列出了几个容器。这些是我们已经启动和停止但还没有移除的容器。
让我们重新启动刚才停止的容器。找到容器的名称,并在 restart 命令中替换下面的容器名称:
docker restart <container name/container ID>
现在,再次使用 ps 命令列出所有的容器:
docker ps -a
请注意,我们刚刚重新启动的容器已经以分离模式启动,并且已经公开了端口8080。
另外,请注意容器的状态是 “Up X seconds”。
当您重新启动容器时,它将使用与最初启动时相同的标志或命令启动。
让我们停下来移除所有的容器,看看如何修复随机命名问题。
停下我们刚开始的集装箱。查找您的运行容器的名称,并将下面命令中的名称替换为您系统上的容器的名称:
docker stop <container name/container ID>
现在我们所有的容器都停止了,让我们移除它们。
当容器被移除时,它不再运行,也不处于停止状态。
相反,容器内的进程被终止,容器的元数据被删除。
要删除容器,运行 docker rm 命令传递容器名称。可以在一个命令中将多个容器名传递给该命令。
同样,请确保将下面命令中的容器名称替换为系统中的容器名称:
docker rm <container name> <container name> <container name> <container name>
再次运行 docker --all 命令,以验证所有容器都已消失。
现在让我们来解决讨厌的随机姓名问题。
标准的做法是为容器命名,原因很简单,因为这样可以更容易地识别容器中运行的内容以及与之关联的应用程序或服务。
就像在代码中为变量命名一样,好的命名约定会让代码更容易阅读。所以,给容器起名字也是如此。
要命名容器,我们必须将 --name 标志传递给 run 命令:
docker run -d -p 8080:8080 --name rest-server docker-gs-ping
docker ps
现在,我们可以根据名称轻松地识别容器。
使用容器进行开发
在这个模块中,我们将研究如何在容器中运行数据库引擎,并将其连接到示例应用程序的扩展版本。
我们将看到一些保持持久性数据的选项,以及将容器连接起来以便彼此通信的选项。
最后,我们将学习如何使用 Docker Compose 有效地管理这种多容器的本地开发环境。
本地数据库和容器
我们将要使用的数据库引擎叫做 CockroachDB,它是一个现代的、本地化的、分布式的 SQL 数据库。
代替从源代码编译 CockroachDB 或者使用操作系统的本地包管理器来安装 CockroachDB,我们将使用 Docker 映像来处理 CockroachDB 并在容器中运行它。
CockroachDB 在很大程度上与 PostgreSQL 兼容,并与后者共享许多约定,特别是环境变量的默认名称。
因此,如果您熟悉 Postgres,不要惊讶我们将使用一些熟悉的环境变量名称。
与 Postgres 兼容的 Go 模块(如 pgx、 pq、 GORM 和 upper/db)也对 CockroachDB 兼容。
有关 Go 和 CockroachDB 之间关系的更多信息,请参考 CockroachDB 文档,不过在本指南中不需要继续介绍。
储存
数据库的意义在于持久存储数据。卷是保存 Docker 容器生成和使用的数据的首选机制。
因此,在启动 CockroachDB 之前,让我们为它创建一个卷。
若要创建托管卷,请运行:
docker volume create roach
我们可以使用以下命令查看 Docker 实例中所有托管卷的列表:
docker volume list
网络
示例应用程序和数据库引擎将通过网络彼此通信。
有不同种类的网络配置可能,我们将使用所谓的用户定义桥接网络。
它将为我们提供一个 DNS 查找服务,这样我们就可以通过它的主机名来引用我们的数据库引擎容器。
下面的命令创建一个名为 mynet 的新桥接网络:
docker network create -d bridge mynet
与管理卷一样,这里有一个命令列出在 Docker 实例中设置的所有网络:
docker network list
我们的桥梁网络 mynet 已经成功创建。
其他三个网络,分别是 bridge
, host
, and none
,它们都是由 Docker 自己创建的。
虽然它与我们当前的讨论无关,但是您可以在 网络概述 部分了解更多关于 Docker 网络的信息。
为卷和网络选择好的名字
俗话说,计算机科学只有两个难题: 缓存失效和命名。而且还有一个错误。
在为网络或托管卷选择名称时,最好选择指示预期用途的名称。
不过,在这个模块中,我们的目标是简洁,因此我们采用了简短的通用名称。
启动数据库引擎
现在已经完成了内部管理任务,我们可以在容器中运行 CockroachDB,并将其附加到刚才创建的卷和网络。
当你运行下面的命令时,Docker 会从 Docker Hub 中提取图像并在本地运行:
docker run -d \
--name roach \
--hostname db \
--network mynet \
-p 26257:26257 \
-p 8080:8080 \
-v roach:/cockroach/cockroach-data \
cockroachdb/cockroach:latest-v20.1 start-single-node \
--insecure
请注意,我们巧妙地使用了 latest-v20.1 标记,以确保我们使用的是20.1的最新补丁版本。
可用标记的多样性取决于图像维护人员。
在这里,我们的目的是有最新的补丁版本的 CockroachDB。
要查看可用于 CockroachDB 镜像的标记,我们访问了 dockerhub 上的 CockroachDB 页面。
配置数据库引擎
现在数据库引擎已经启动,在我们的应用程序开始使用它之前需要进行一些配置。
- 创建一个空白数据库
- 用数据库引擎注册一个新的用户帐户
- 授予新用户对数据库的访问权限
我们可以借助 CockroachDB 内置的 SQL shell 来实现这一点。
要在运行数据库引擎的同一个容器中启动 SQL shell,输入:
docker exec -it roach ./cockroach sql --insecure
- 在 SQL shell 中,创建我们的示例程序将要使用的数据库:
CREATE DATABASE mydb;
- 用数据库引擎注册一个新的 SQL 用户帐户。我们选择用户名 totoro。
CREATE USER totoro;
3.给新用户必要的权限:
GRANT ALL ON DATABASE mydb TO totoro;
- 键入 quit 退出 shell。
quit
来看看示例应用程序
现在我们已经启动并配置了数据库引擎,我们可以将注意力转移到应用程序上。
这个模块的示例应用程序是我们在前面的模块中使用的 docker-gs-ping 应用程序的扩展版本。你有两个选择:
- You can update your local copy of
docker-gs-ping
to match the new extended version presented in this chapter; or - You can clone the olliefr/docker-gs-ping-roach repo. This latter approach is recommended.
要 checkout 示例应用程序,请运行:
git clone https://github.com/olliefr/docker-gs-ping-roach.git
应用程序的 main.go 现在包含数据库初始化代码,以及实现新业务需求的代码:
- An HTTP
POST
request to/send
containing a{ "value" : string }
JSON must save the value to the database.
We also have an update for another business requirement. The requirement was:
- The application responds with a text message containing a heart symbol (“
<3
”) on requests to/
.
And now it’s going to be:
-
The application responds with the string containing the count of messages stored in the database, enclosed in the parentheses.
Example output:
Hello, Docker! (7)
下面是 main.go 的完整源代码清单。
package main
import (
"context"
"database/sql"
"fmt"
"log"
"net/http"
"os"
"github.com/cenkalti/backoff/v4"
"github.com/cockroachdb/cockroach-go/v2/crdb"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
func main() {
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
db, err := initStore()
if err != nil {
log.Fatalf("failed to initialise the store: %s", err)
}
defer db.Close()
e.GET("/", func(c echo.Context) error {
return rootHandler(db, c)
})
e.GET("/ping", func(c echo.Context) error {
return c.JSON(http.StatusOK, struct{ Status string }{Status: "OK"})
})
e.POST("/send", func(c echo.Context) error {
return sendHandler(db, c)
})
httpPort := os.Getenv("HTTP_PORT")
if httpPort == "" {
httpPort = "8080"
}
e.Logger.Fatal(e.Start(":" + httpPort))
}
type Message struct {
Value string `json:"value"`
}
func initStore() (*sql.DB, error) {
pgConnString := fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s sslmode=disable",
os.Getenv("PGHOST"),
os.Getenv("PGPORT"),
os.Getenv("PGDATABASE"),
os.Getenv("PGUSER"),
os.Getenv("PGPASSWORD"),
)
var (
db *sql.DB
err error
)
openDB := func() error {
db, err = sql.Open("postgres", pgConnString)
return err
}
err = backoff.Retry(openDB, backoff.NewExponentialBackOff())
if err != nil {
return nil, err
}
if _, err := db.Exec(
"CREATE TABLE IF NOT EXISTS message (value STRING PRIMARY KEY)"); err != nil {
return nil, err
}
return db, nil
}
func rootHandler(db *sql.DB, c echo.Context) error {
r, err := countRecords(db)
if err != nil {
return c.HTML(http.StatusInternalServerError, err.Error())
}
return c.HTML(http.StatusOK, fmt.Sprintf("Hello, Docker! (%d)\n", r))
}
func sendHandler(db *sql.DB, c echo.Context) error {
m := &Message{}
if err := c.Bind(m); err != nil {
return c.JSON(http.StatusInternalServerError, err)
}
err := crdb.ExecuteTx(context.Background(), db, nil,
func(tx *sql.Tx) error {
_, err := tx.Exec(
"INSERT INTO message (value) VALUES ($1) ON CONFLICT (value) DO UPDATE SET value = excluded.value",
m.Value,
)
if err != nil {
return c.JSON(http.StatusInternalServerError, err)
}
return nil
})
if err != nil {
return c.JSON(http.StatusInternalServerError, err)
}
return c.JSON(http.StatusOK, m)
}
func countRecords(db *sql.DB) (int, error) {
rows, err := db.Query("SELECT COUNT(*) FROM message")
if err != nil {
return 0, err
}
defer rows.Close()
count := 0
for rows.Next() {
if err := rows.Scan(&count); err != nil {
return 0, err
}
rows.Close()
}
return count, nil
}
该存储库还包括 Dockerfile,它与前面模块中引入的多阶段 Dockerfile 几乎完全相同。它使用官方的 Docker Go 映像来构建应用程序,然后通过将编译后的二进制映像放入更薄的“ distroless”映像来构建最终映像。
无论我们是更新了旧的示例应用程序,还是签出了新的示例应用程序,都必须构建这个新的 Docker 映像来反映对应用程序源代码的更改。
构建应用程序
我们可以使用熟悉的 build 命令来构建图像:
docker build --tag docker-gs-ping-roach .
运行应用程序
现在,我们来检查一下我们的集装箱。这一次,我们需要设置一些环境变量,以便我们的应用程序知道如何访问数据库。
现在,我们将在 docker run 命令中完成这项工作。
稍后我们将看到使用 Docker Compose 的更方便的方法。
Note
因为我们在“不安全”模式下运行 CockroachDB 集群,所以密码的值可以是任何值。
但是,不要在生产环境中以不安全的模式运行!
docker run -it --rm -d \
--network mynet \
--name rest-server \
-p 80:8080 \
-e PGUSER=totoro \
-e PGPASSWORD=myfriend \
-e PGHOST=db \
-e PGPORT=26257 \
-e PGDATABASE=mydb \
docker-gs-ping-roach
关于这个命令有几点需要注意。
-
这次我们把集装箱港口8080映射到主港口80。因此,对于 GET 请求,我们可以直接使用 curl localhost:
curl localhost
或者,如果你愿意的话,一个合适的 URL 也可以:
curl http://localhost/
-
下载所有存储的信息为
0
,因为我们还没有在我们的应用程序中发布任何东西 -
我们通过数据库容器的主机名(即 db)来引用它。这就是我们在启动数据库容器时使用的 --name db。
-
实际的密码并不重要,但必须将其设置为某个值,以避免混淆示例应用程序
-
我们刚才运行的容器名为 rest-server,这些名字对于管理容器生命周期非常有用:
# Don't do this just yet, it's only an example: $ docker container rm --force rest-server
测试应用程序
在前面的部分中,我们已经测试了使用 GET 查询应用程序,对于存储的消息计数器,它返回零。现在,让我们发布一些信息给它:
curl --request POST \
--url http://localhost/send \
--header 'content-type: application/json' \
--data '{"value": "Hello, Docker!"}'
应用程序以消息的内容作为响应,这意味着它已经保存在数据库中:
{"value":"Hello, Docker!"}
让我们发出另一个信息:
curl --request POST \
--url http://localhost/send \
--header 'content-type: application/json' \
--data '{"value": "Hello, Oliver!"}'
然后,我们再次得到消息的价值:
{"value":"Hello, Oliver!"}
让我们看看留言板上是怎么说的:
curl localhost
嘿,说得太对了!我们发送了两条消息,数据库保存了它们。
或者是这样?让我们停下来,移走所有的容器,但不是卷,然后再试一次。
首先,让我们停止容器:
docker container stop rest-server roach
然后,让我们移除它们:
docker container rm rest-server roach
确认他们已经离开:
docker container list --all
然后重新启动它们,首先是数据库:
docker run -d \
--name roach \
--hostname db \
--network mynet \
-p 26257:26257 \
-p 8080:8080 \
-v roach:/cockroach/cockroach-data \
cockroachdb/cockroach:latest-v20.1 start-single-node \
--insecure
接下来的服务是:
docker run -it --rm -d \
--network mynet \
--name rest-server \
-p 80:8080 \
-e PGUSER=totoro \
-e PGPASSWORD=myfriend \
-e PGHOST=db \
-e PGPORT=26257 \
-e PGDATABASE=mydb \
docker-gs-ping-roach
最后,让我们查询一下我们的服务:
curl localhost
太好了!数据库中的记录计数是正确的,尽管我们不仅停止了容器,而且在启动新实例之前删除了它们。不同之处在于我们重用了 CockroachDB 的托管卷。新的 CockroachDB 容器已经从磁盘读取了数据库文件,就像它通常在容器外运行时那样。
这就是管理卷的力量。明智地使用它。
放松一切
请记住,我们是在“不安全”模式下运行 CockroachDB。既然我们已经构建并测试了我们的应用程序,现在是时候在继续前进之前将所有的东西都关闭了。可以使用 list 命令列出正在运行的容器:
docker container list
现在您已经知道了容器 id,可以使用 docker 容器停止和 docker 容器 rm,如前面的模块所示。
请确保在继续之前停止 CockroachDB 和 docker-gs-ping-roach 容器。
使用 Docker Compose 提高生产效率
此时,您可能想知道是否有一种方法可以避免必须处理 docker 命令的长列表参数。我们在本系列中使用的玩具示例需要五个环境变量来定义到数据库的连接。一个真正的应用程序可能需要更多更多的东西。然后还有一个依赖性的问题——理想情况下,我们希望确保在运行应用程序之前启动数据库。而且,数据库实例的旋转可能需要另一个具有许多选项的 Docker 命令。但是,有一种更好的方法可以为本地开发目的编排这些部署。
在本节中,我们将创建一个 Docker Compose 文件,用一个命令启动 Docker-gs-ping-roach 应用程序和 CockroachDB 数据库引擎。
配置 Docker Compose
在应用程序的目录中,创建一个名为 docker-compose.yml
的新文本文件,其内容如下。
version: '3.8'
services:
docker-gs-ping-roach:
depends_on:
- roach
build:
context: .
container_name: rest-server
hostname: rest-server
networks:
- mynet
ports:
- 80:8080
environment:
- PGUSER=${PGUSER:-totoro}
- PGPASSWORD=${PGPASSWORD:?database password not set}
- PGHOST=${PGHOST:-db}
- PGPORT=${PGPORT:-26257}
- PGDATABASE=${PGDATABASE:-mydb}
deploy:
restart_policy:
condition: on-failure
roach:
image: cockroachdb/cockroach:latest-v20.1
container_name: roach
hostname: db
networks:
- mynet
ports:
- 26257:26257
- 8080:8080
volumes:
- roach:/cockroach/cockroach-data
command: start-single-node --insecure
volumes:
roach:
networks:
mynet:
driver: bridge
这个 Docker Compose 配置非常方便,因为我们不必输入所有参数传递给 Docker run 命令。我们可以在 dockercompose 文件中以声明方式完成这一操作。Dockercompose 文档页面非常广泛,包括 dockercompose 文件格式的完整参考。
.env
文件
Docker Compose 将自动从 .env
文件中读取环境变量。由于 Compose 文件要求设置 PGPASSWORD,因此我们将以下内容添加到 .env
文件
PGPASSWORD=whatever
对于我们的示例来说,确切的值并不重要,因为我们在不安全的模式下运行 CockroachDB,但是我们必须将变量设置为某个值,以避免出现错误。
合并 Compose 文件
文件名 docker-compose.yml 是 docker-compose 命令识别的默认文件名,如果没有提供 -f 标志的话。
这意味着如果您的环境有这样的需求,您可以拥有多个 Docker Compose 文件。
此外,Docker Compose files are... composable (pun intended),因此可以在命令行中指定多个文件,以将配置的各个部分合并到一起。下面的列表只是这种功能非常有用的几个场景的例子:
- Using a bind mount for the source code for local development but not when running the CI tests;
- Switching between using a pre-built image for the frontend for some API application vs creating a bind mount for source code;
- Adding additional services for integration testing;
- And many more...
We are not going to cover any of these advanced use cases here.
Docker Compose 中的变量替换
Docker Compose 最酷的特性之一就是变量替换。您可以在 Compose file,environment 部分中看到一些示例。举例说明:
PGUSER=${PGUSER:-totoro}
means that inside the container, the environment variablePGUSER
shall be set to the same value as it has on the host machine where Docker Compose is run. If there is no environment variable with this name on the host machine, the variable inside the container gets the default value oftotoro
.PGPASSWORD=${PGPASSWORD:?database password not set}
means that if the environment variablePGPASSWORD
is not set on the host, Docker Compose will display an error. This is OK, because we don’t want to hard-code default values for the password. We set the password value in the.env
file, which is local to our machine. It is always a good idea to add.env
to.gitignore
to prevent the secrets being checked into the version control.
Other ways of dealing with undefined or empty values exist, as documented in the variable substitution section of the Docker documentation.
验证 Docker Compose 配置
在应用对 Compose 配置文件所做的更改之前,可以使用以下命令验证配置文件的内容:
docker-compose config
运行此命令时,Docker Compose 将读取文件 Docker-Compose。Yml,将其解析为内存中的数据结构,在可能的情况下进行验证,并从其内部表示返回配置文件的重构。如果由于错误而无法执行此操作,则将打印错误消息。
使用 Docker Compose 构建并运行应用程序
让我们启动应用程序并确认它正常运行。
docker-compose up --build
我们传递 --build 标志,这样 Docker 将编译我们的映像,然后启动它。
注意
Docker Compose 是一个有用的工具,但它有自己的特点。例如,如果不提供 --build 标志,源代码的更新不触发重新生成。
编辑源代码,并在运行 docker-compose 时忘记使用 --build 标志,这是一个非常常见的陷阱。
由于我们的设置现在由 docker compose 运行,它为它分配了一个“项目名称”,因此我们为 CockroachDB 实例获得了一个新的卷。这意味着我们的应用程序将无法连接到数据库,因为数据库在这个新卷中不存在。终端将显示数据库的身份验证错误:
由于我们使用 restart_policy 设置部署的方式,故障容器每20秒重新启动一次。因此,为了解决这个问题,我们需要登录到数据库引擎并创建用户,我们在[]中已经做过了
这没什么大不了的。我们所要做的就是连接到 CockroachDB 实例,并运行三个 SQL 命令来创建数据库和用户,如上面配置数据库引擎部分所述。
因此,我们从另一个终端登录到数据库引擎:
docker exec -it roach ./cockroach sql --insecure
并执行与前面相同的命令来创建数据库 mydb,即用户 totoro,并授予该用户必要的权限。一旦我们这样做(并且示例应用程序容器自动重新启动) ,rest-service 就会停止故障并重新启动,控制台也会变得安静。
它本来可以连接我们之前使用的卷,但是为了我们的例子的目的,它更多的麻烦而不是它的价值,它也提供了一个机会来展示如何通过重启策略撰写文件功能将弹性引入到我们的部署中。
测试应用程序
现在让我们测试一下我们的 API 端点,在新的终端中,运行以下命令:
curl http://localhost/
关闭
要停止由 Docker Compose 启动的容器,请在运行 Docker-Compose up 的终端中按 ctrl + c。要删除这些容器后,他们已被停止,运行 docker-compose 下来。
分离模式
可以使用-d 标志以分离模式运行由 docker-compose 命令启动的容器,就像使用 docker 命令一样。
要在分离模式下启动由 Compose 文件定义的堆栈,请运行:
docker-compose up --build -d
然后,可以使用 docker-compose stop
停止容器并使用 docker-compose down
删除容器。
持久性存储
托管卷并不是为容器提供持久存储的唯一方法。强烈建议您熟悉可用的存储选项及其用例,这些在 Docker 文档的下面部分中已经涉及到了: 在 Docker 中管理数据。
CockroachDB 簇
我们运行了一个 CockroachDB 实例,这对于我们的演示来说已经足够了。但是可以运行一个由多个 CockroachDB 实例组成的 CockroachDB 集群,每个实例在自己的容器中运行。由于 CockroachDB 引擎是按设计分布式的,因此我们对运行一个有多个节点的集群的过程只做了很少的修改。
这种分布式设置提供了有趣的可能性,例如应用混沌工程技术来模拟集群的部分故障,并评估我们的应用程序处理此类故障的能力。
如果你对实验 CockroachDB 集群感兴趣,请查看:
- Start a CockroachDB Cluster in Docker article; and
- Documentation for Docker Compose keywords
deploy
andreplicas
.
其他数据库
由于我们没有运行一个 CockroachDB 实例集群,您可能想知道我们是否可以使用非分布式数据库引擎。答案是肯定的,如果我们选择一个更加传统的 SQL 数据库,比如 PostgreSQL,那么本章描述的过程将会非常相似。
使用 Go test 运行您的测试
测试是现代软件开发的重要组成部分。然而,测试对于不同的开发团队来说意味着很多东西。出于简洁的名义,我们将只关注运行孤立的、高级的、功能性测试。
测试结构
每个测试都是为了验证我们的示例应用程序的单个业务需求。下面的测试摘自 main_test.go 测试套件在我们的示例应用程序。
func TestRespondsWithLove(t *testing.T) {
pool, err := dockertest.NewPool("")
require.NoError(t, err, "could not connect to Docker")
resource, err := pool.Run("docker-gs-ping", "latest", []string{})
require.NoError(t, err, "could not start container")
t.Cleanup(func() {
require.NoError(t, pool.Purge(resource), "failed to remove container")
})
var resp *http.Response
err = pool.Retry(func() error {
resp, err = http.Get(fmt.Sprint("http://localhost:", resource.GetPort("8080/tcp"), "/"))
if err != nil {
t.Log("container not ready, waiting...")
return err
}
return nil
})
require.NoError(t, err, "HTTP error")
defer resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode, "HTTP status code")
body, err := io.ReadAll(resp.Body)
require.NoError(t, err, "failed to read HTTP body")
// Finally, test the business requirement!
require.Contains(t, string(body), "<3", "does not respond with love?")
}
正如您所看到的,这是一个高级测试,与我们的示例应用程序的实现细节无关。
- the test is using
ory/dockertest
Go module; - the test assumes that the Docker engine instance is running on the same machine where the test is being run.
The second test in main_test.go
has almost identical structure but it tests another business requirement of our application. You are welcome to have a look at all available tests in docker-gs-ping/main_test.go
.
在本地运行测试
为了运行测试,我们必须确保应用程序 Docker 映像是最新的。
docker build -t docker-gs-ping:latest .
在上面的示例中,我们省略了大部分输出,只显示了表示构建成功的第一行。
注意,这个图片被标记为 latest,这和我们在 main _ test 中选择使用的标签是一样的。去测试。
现在我们的应用程序的 Docker 映像已经构建好了,我们可以运行依赖于它的测试:
go test ./...
That was a bit... underwhelming? Let’s ask it to print a bit more detail, just to be sure:
go test -v ./...
所以,这些测试确实通过了。注意,在容器初始化时,重新尝试使用指数回退如何帮助避免失败的测试。在每个测试中发生的是 ory/dockertest 模块连接到本地 Docker 引擎实例,并指示它使用映像(由标记 Docker-gs-ping: latest 标识)启动一个容器。启动容器可能需要一段时间,因此我们的测试会重试访问容器,直到容器准备好响应请求。
为您的应用程序配置 CI/CD
本页指导您使用 Docker 容器设置 GitHub Action CI/CD 管道的过程。在建立一个新的管道之前,我们建议您看一下 Ben 的关于 CI/CD 最佳实践的博客。
本指南包含如何:
- 使用 GitHub action 建立持续集成(CI)管道;
- 启用连续部署(CD)工具的 Docker Hub 访问;
- 优化基于 GitHub actions 的 CI/CD 管道,以减少拉请求的数量和总的构建时间; 以及
- 只将应用程序的特定版本发布到 dockerhub
Note
在我们开始之前,必须指出持续集成(CI)和持续部署(CD)都是一个巨大的主题,有许多不同的方法和观点。本指南中选择的方法是为了教学的清晰性和简单性而优化的,并不是为了成为测试和发布软件的唯一真正方法。
选择一个示例项目
我们开始吧。本指南使用了一个简单的 Go 项目作为示例。事实上,这也是我们在本指南的构建图像部分中熟悉的项目。
olliefr/docker-gs-ping 存储库包含完整的源代码和 Dockerfile。您可以按照本部分中描述的方式分支它或者跟随它,并设置您自己的 Go 项目之一。
因此,只要你有一个带有项目和 Dockerfile 的 GitHub repo,你就可以完成教程的这一部分。
启用对 Docker Hub 的访问
Dockerhub 是由 Docker 提供的托管存储库服务,用于查找和共享容器映像。
在我们将 Docker 映像发布到 Docker Hub 之前,我们必须授予对 Docker Hub API 的 GitHub Actions 访问权限。
设置对 Docker Hub API 的访问:
- Create a new Personal Access Token (PAT) for Docker Hub.
- Go to the Docker Hub Account Settings and then click New Access Token.
- Let’s call this token
docker-gs-ping-ci
. Input the name and click Create. - Copy the token value, we’ll need it in a second.
- Add your Docker ID and PAT as secrets to your GitHub repo.
- Navigate to your GitHub repository and click Settings > Secrets > New secret.
- Create a new secret with the name
DOCKER_HUB_USERNAME
and your Docker ID as value. - Create a new secret with the name
DOCKER_HUB_ACCESS_TOKEN
and use the token value from the step (1).
Your GitHub repository Secrets section would look like the following.
现在可以从我们的工作流中引用这两个变量。这将为将我们的图像发布到 dockerhub 打开一个机会。
建立 CI 工作流程
在上一节中,我们创建了一个 PAT 并将其添加到 GitHub,以确保我们可以从任何 GitHub 动作工作流中访问 Docker Hub。但是在开始构建用于发布我们的软件的映像之前,让我们先构建一个 CI 管道来运行测试。
To set up the workflow:
- Go to your repository in GitHub and then click Actions > New workflow.
- Click set up a workflow yourself and update the starter template to match the following:
首先,我们将这个工作流命名为:
name: Run CI
然后,我们将选择何时运行这个工作流。在我们的示例中,我们将对项目的主要分支进行每次推进:
on:
push:
branches: [ main ]
workflow_dispatch:
The workflow_dispatch
is optional. It enables to run this workflow manually from the Actions tab.
现在,我们需要指定我们真正想要在工作流中发生的事情。工作流运行由一个或多个可以顺序运行或并行运行的作业组成。
我们希望设置的第一个作业是构建和运行测试的作业。让它在最新的 Ubuntu 实例上运行:
jobs:
build-and-test:
runs-on: ubuntu-latest
A job is a sequence of steps. For this simple CI pipeline we would like to:
- Set up Go compiler environment.
- Check out our code from its GitHub repository.
- Fetch Go modules used by our application.
- (Optional) Build the binary for our application.
- Build the Docker Image for our application.
- Run the functional tests for our application against that Docker image.
在步骤4中构建二进制文件实际上是可选的。这是一个“烟雾测试”。如果我们的应用程序甚至没有编译,我们不希望构建一个 Docker 映像并尝试进行功能测试。如果我们有“单元测试”或其他一些小测试,我们也会在步骤4和步骤5之间运行它们。
下面的一系列步骤实现了我们刚刚设定的目标。
steps:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: 1.16.4
- name: Checkout code
uses: actions/checkout@v2
- name: Fetch required Go modules
run: go mod download
- name: Build
run: go build -v ./...
- name: Build Docker image
uses: docker/build-push-action@v2
with:
push: false
tags: ${{ github.event.repository.name }}:latest, ${{ github.repository }}:latest
- name: Run functional tests
run: go test -v ./...
As is usual with YAML files, be aware of indentation. The complete workflow file for reference is available in the project’s repo, under the name of .github/workflows/ci.yml
.
This should be enough to test our approach to CI. Change the workflow file name from main.yml
to ci.yml
and press Start commit button. Fill out the commit details in your preferred style and press Commit new file. GitHub Actions are saved as YAML files in .github/workflows
directory and GitHub web interface would do that for us.
Select Actions from the navigation bar for your repository. Since we’ve enabled workflow_dispatch
option in our Action, GitHub will have started it already. If not, select “CI/CD to Docker Hub” action on the left, and then press Run workflow button on the right to start the workflow.
如果运行失败,您可以单击失败条目来查看日志并相应地修改工作流 YAML 文件。
设置 CD 工作流程
现在,让我们创建一个 GitHub action 工作流来为我们的应用程序在 Docker Hub 建立和存储映像。我们可以通过创建两个 Docker 操作来实现这一点:
- 第一个操作允许我们使用存储在 GitHub 仓库设置中的秘密登录到 Docker Hub
- 第二个是构建和推动动作
在这个示例中,让我们将 push 标志设置为 true,因为我们也希望 push。然后我们将添加一个标记来指定总是转到最新版本。最后,我们将回显图像摘要,看看推送了什么。
现在,我们可以添加所需的步骤。启动一个空白的新工作流程,就像我们之前做的那样。让我们给它一个名为 release.yml 的文件,并修改模板主体以匹配以下内容。
name: Release to Docker Hub
on:
push:
tags:
- "*.*.*"
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: 1.16.4
- name: Checkout code
uses: actions/checkout@v2
- name: Fetch required Go modules
run: go mod download
- name: Build
run: go build -v ./...
- name: Build and push Docker image
id: docker_build
uses: docker/build-push-action@v2
with:
push: true
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/${{ github.event.repository.name }}:latest
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}
这个工作流程类似于 CI 工作流程,具有以下变化:
- This workflow is only triggered when a git tag of the format
*.*.*
is pushed to the repo. The tag meant to be a semantic version, such as3.5.0
or0.0.1
. - The very first step is to login into Docker Hub using the two secrets that we had saved in the repository settings previously.
- The build and push step now has
push: true
and since we had logged into Docker Hub this will result in the latest image being published. - The image digest step prints out the image metadata to the log.
让我们保存这个工作流程,并检查 GitHub 上存储库的 Actions 页面。不像 CI 工作流,这个新的工作流不能手动触发-这是我们如何设置它。因此,为了测试它,我们必须标记一些 commit。让我们标记主分支的 HEAD:
git tag -a 0.0.1 -m "Test release workflow"
git push --tags
This means our tag was successfully pushed to the main repo. If we switch to the GitHub UI, we would see that the workflow has already been triggered:
剧情转折!尽管我们已经解释了如何向存储库中添加秘密,但是我们忘记了自己来做这件事。并且工作流运行结果是错误的:
这个问题很容易解决。我们按照自己的指南,将秘密添加到存储库设置中。但是我们如何重新运行工作流呢?我们需要移除标签并重新应用它。
To remove the tag on the remote
:
git push origin :refs/tags/0.0.1
And to re-apply it locally and push:
git tag -fa 0.0.1 -m "Test release workflow"
git push origin --tags
然后再次触发工作流,这一次它完成时没有问题:
由于我们刚刚推送到 Docker Hub 的图片是公开的,现在它可以被任何人从任何地方获取:
docker pull olliefr/docker-gs-ping
我们已经构建了一个简单的(甚至是幼稚的) CI/CD 工作流。有许多方法可以改进它。我们将在下一节中研究其中的一些方法。
优化工作流程
接下来,让我们看看如何通过构建缓存来优化 GitHub action 工作流:
- 构建缓存可以减少构建时间,因为它不必重新下载所有的映像,而且
- 它还减少了我们针对 dockerhub 完成的拉取数量。我们需要使用 GitHub 缓存来使用这个
让我们设置一个带有构建缓存的 Builder。首先,我们需要设置构建器,然后配置缓存。在这个例子中,让我们添加路径和键来存储它,并使用 GitHub 缓存来实现。
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
- name: Cache Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
最后,在向 Actions 文件顶部添加构建器和构建缓存代码段之后,我们需要向构建和推送步骤添加一些额外的属性。这包括:
- 设置构建器以使用 buildx 步骤的输出,然后
- 使用我们前面设置的缓存来存储和检索
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
builder: ${{ steps.buildx.outputs.name }}
push: false
load: true
tags: ${{ github.event.repository.name }}:latest, ${{ github.repository }}:latest
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache
现在,再次运行 CI 工作流,通过检查工作流日志来验证它是否使用了构建缓存。
收工
GitHub Actions 是一种非常强大的自动化 CI 和 CD 管道的方法,并且有大量的文档可以帮助您完成这个任务。然而,这并不是将容器集成到您的工作流中的唯一方法。本节的目的是向您展示一些可能的基本事物。市场上有大量的 CI 和 CD 工具,欢迎您调查它们与容器生态系统的集成。定义良好的自动化 CI 管道可以为您和您的团队节省大量的工作。
部署你的应用程序
现在,我们已经配置了一个 CI/CD 管道线,让我们看看如何部署应用程序。Docker 支持在 Azure ACI 和 AWS ECS 上部署容器。如果您在 Docker Desktop 中启用了 Kubernetes,那么您还可以将应用程序部署到 Kubernetes。
Docker Azure 集成允许开发人员在构建本地云应用程序时使用本地 Docker 命令在 Azure 容器实例(ACI)中运行应用程序。新的体验提供了 Docker 桌面和微软 Azure 之间的紧密集成,允许开发者使用 Docker CLI 或 VS Code 扩展快速运行应用程序,从本地开发无缝切换到云部署。
有关详细说明,请参阅 在 Azure 上部署 Docker 容器。
Docker and AWS ECS
Docker ECS 集成允许开发人员在构建云本地应用程序时,使用 Docker Compose CLI 中的本地 Docker 命令在 Amazon EC2 Container Service (ECS)中运行应用程序。
Docker 和 Amazon ECS 之间的集成允许开发人员使用 Docker Compose CLI 在一个 Docker 命令中设置 AWS 上下文,从而允许您从本地上下文切换到云上下文,并使用 Compose 文件快速简单地在 Amazon ECS 上运行应用程序,简化多容器应用程序开发。
有关详细说明,请参阅 在 ECS 上部署 Docker 容器。
Kubernetes
Docker Desktop 包括一个独立的 Kubernetes 服务器和客户端,以及在您的机器上运行的 Docker CLI 集成。当您启用 Kubernetes 时,您可以在 Kubernetes 上测试工作负载。
To enable Kubernetes:
-
From the Docker menu, select Preferences (Settings on Windows).
-
Select Kubernetes and click Enable Kubernetes.
This starts a Kubernetes single-node cluster when Docker Desktop starts.
For detailed information, see Deploy on Kubernetes and Describing apps using Kubernetes YAML.