在本地开发和测试连接到数据库的 API 并不容易。数据库经常成为一个痛点。然而,使用 Docker 可以使这个过程变得更简单、更容易,并且易于复制。在这篇博客中,我们将看到如何将一个 Golang API 与 MySQL 数据库容器化,并使其可以用于 Docker Compose。
为了演示,我创建了 这个[1] RESTful Golang API。我们可以对 MySQL 数据库执行 CRUD 操作,如创建、删除和编辑schedules。我们可以在项目的 README[2] 中了解更多关于端点、方法等的信息。我们不会深入探讨 API 的工作原理,因为我们的主要目标是专注于容器化部分。
要容器化一个应用程序,我们需要创建一个 Dockerfile。让我告诉你,有数百种方法可以编写 Dockerfile,没有对错之分,每个个人/公司都有自己的一套实践和编写方式。在我们的例子中,我们将在 Dockerfile 中遵循四个最佳实践,以获得更好和优化的镜像,使其更小更安全。让我们在开始编写 Dockerfile 之前先了解所有 4 个实践及其实施原因。
- 使用更轻量级的基础镜像:几乎每种语言都有一个更轻量级版本的镜像。所谓轻量级,我指的不是小几兆字节,它可能小 10 倍,原因是它不包含不必要的依赖项,这使得它更小更安全。是的,更多的依赖项带来更多的安全风险。我们将为 node、Golang 等提供
bullseye
和alpine
版本。 - 多阶段构建: 这是 Docker 的超能力之一,它允许我们并行运行构建步骤,还可以通过从其他步骤复制必要的文件和内容来创建最终镜像,只包含运行程序所需的项目。
- 创建二进制文件: 许多语言支持从源代码创建二进制文件,使其变小并更容易运行,因为我们不需要处理完整的源代码。此外,我们可以在任何环境中运行,而不受语言障碍的影响。
- 分解层: 我们知道 Dockerfile 中的每条指令都是一层,分解层是加快构建速度的好方法。例如,如果我们从源代码复制所有文件(连同要安装的依赖文件),然后安装依赖项,即使我们没有对依赖项进行任何更改,每次重新构建镜像时,它都会复制所有文件并安装依赖项。为了克服这个问题,我们将它们分解成几层,可以在一个步骤中复制依赖文件并安装它。在下一步中,我们可以复制所有文件。现在,当我们对代码进行更改并重新构建时,只有复制所有文件的层会被重新构建,而依赖项步骤(由于没有更改而被缓存)则保持不变。我们也可以在下面的 Dockerfile 中看到一个例子。我们以这样的方式编写 Dockerfile,即变化较少的步骤,如基础镜像、依赖项等,将采用自上而下的方法。
所以,这是我们为 Go API 创建的 Dockerfile。
FROM golang:alpine AS builder WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -o /app/main . FROM alpine:latest WORKDIR /root/ COPY --from=builder /app/main . CMD ["./main"]
在 FROM
中,我们可以看到我们使用了 golang:alpine
版本作为基础镜像,而不是使用完整的 golang 镜像,并将步骤命名为 builder
,这个名称/标签将帮助我们从一个阶段复制文件到另一个阶段。之后,我们创建了一个工作目录。然后,我们没有一起复制所有文件,而是只复制了 go.mod
和 go.sum
并安装依赖项(我在上面的分解层点中解释了原因)。
现在,一旦依赖项安装完成,我们复制剩余的文件。然后我们通过运行 go build
从源代码创建一个二进制文件,并使用 -o
输出标志将二进制文件命名为当前位置。
现在,事情变得有趣了,在最后阶段我们不需要 golang 镜像或类似的东西,我们可以使用 Alpine 基础镜像,因为我们现在有了一个二进制文件,我们可以在任何 Linux 系统上运行,而不受编程语言特定性的影响。在下一步中,我们可以从构建器步骤复制二进制文件到最终阶段并运行二进制文件。
就是这样。这就是我们如何容器化我们的应用程序,我们可以通过引入最佳实践来进一步改进 Dockerfile,比如创建用户并以非 root 身份运行等。现在我们可以使用 Dockerfile 构建镜像并运行,然后通过提供所需的凭据连接到远程或本地 MySQL 服务器,然后访问这些 API 端点。
但是,我们不会就此止步,我们将更进一步,我们还将在容器中运行 MySQL 服务器并与我们的应用程序连接。但是,需要注意的一点是,我们可以运行一个 MySQL 容器并将我们的 API 容器连接到它,但是有太多的手动工作和需要在终端中输入的长命令,而且可能会出错。为了克服这个问题,我们将使用 Docker Compose 来简化我们的生活。
让我们创建一个名为 compose.yml
的文件,并使用以下配置。
version: '3.8' services: app: build: . ports: - "8080:8080" environment: - DB_HOST=mysql - DB_USER=root - DB_PASSWORD=password - DB_NAME=my_database depends_on: mysql: condition: service_healthy mysql: image: mysql:8.0 environment: - MYSQL_ROOT_PASSWORD=password - MYSQL_DATABASE=my_database healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] timeout: 5s retries: 10
这是一个相当基本的配置,但我想提到几个关键点,如果我们看到 DB_HOST
是 mysql
,这里没有 localhost[3] 或 ip,因为在 compose 中,服务通过服务名与其他服务通信。这是 Docker Compose 提供的开箱即用的网络功能。
另一点是,在使用 Docker Compose 时经常会发生这种情况,我们有两个服务:一个应用程序和一个数据库,应用程序服务先启动,但当应用程序尝试连接时,数据库还没有准备好,导致应用程序崩溃。为了克服这个问题,我们为数据库设置了一个 healthcheck
来确认其就绪状态。然后,在我们的应用程序服务中,我们可以使用带有 condition
的 depends_on
来确保应用程序只在数据库服务健康时才启动。
现在,当我们第一次执行 Docker compose 时,我们可能会遇到一个错误,说权限被拒绝,因为它没有权限,也没有名为 my_database
的数据库,所以我们需要通过进入容器来解决这两个问题。
即使我们的应用程序已经崩溃,数据库仍然在运行。我们可以通过执行 docker ps
来检查。
现在通过执行 docker exec -it <container-id> sh
进入容器。容器 ID 可以从执行 docker ps
后的输出中复制。一旦我们进入容器,现在使用以下命令登录 mysql
:
它会要求输入密码,输入你在 compose.yml
文件中提到的密码。一旦我们登录,我们就可以创建一个数据库。创建一个与 compose 文件中指定的名称相同的数据库。在我的例子中,它是 my_database
。执行以下命令:
CREATE DATABASE my_database;
现在要给予正确的权限并刷新它,执行以下命令。
GRANT ALL PRIVILEGES ON my_database.* TO 'root'@'%'; FLUSH PRIVILEGES;
完成后,我们需要停止正在运行的 compose 服务,并通过执行 docker compose up
重新启动
这就是这篇博客的全部内容。我很高兴你还在阅读并坚持到最后——非常感谢你的支持和阅读。我有时会在 Twitter[4] 上分享 Golang 的技巧。你可以在那里与我联系。
参考链接
- 这个: https://github.com/Pradumnasaraf/Blog-Demo/tree/main/go-api-mysql
- README: https://github.com/Pradumnasaraf/Blog-Demo/tree/main/go-api-mysql#readme
- localhost: http://localhost/
- Twitter: https://x.com/pradumna_saraf