安装 Docker

接下来将体验手动编译并部署 Docker Image 到服务器,我以本地 OrbStack 提供的 Debian12 arm64 虚拟机为例

PLAINTEXT
nx@debian:~$ screenfetch
         _,met$$$$$gg.           nx@debian
      ,g$$$$$$$$$$$$$$$P.        OS: Debian
    ,g$$P""       """Y$$.".      Kernel: aarch64 Linux 6.5.13-orbstack-00122-g57b8027e2387
   ,$$P'              `$$$.      Uptime: 3h 56m
  ',$$P       ,ggs.     `$$b:    Packages: 275
  `d$$'     ,$P"'   .    $$$     Shell: bash 5.2.15
   $$P      d$'     ,    $$P     Disk: 442G / 1.4T (32%)
   $$:      $$.   -    ,d$$'     CPU: Apple - @ 8x 2GHz
   $$\;      Y$b._   _,d$P'      RAM: 979MiB / 5250MiB
   Y$$.    `.`"Y$$$$P"'
   `$$b      "-.__
    `Y$$
     `Y$$.
       `$$b.
         `Y$$b.
            `"Y$b._
                `""""

请注意,本教程全程使用 arm64 架构(或称 aarch64 架构),而一般情况下云服务商提供的服务器为 amd64 架构(或称 x64 或 x86_64 架构)

服务器一般仅能运行当前的架构的 Docker Image,如果你的本地架构与目标架构不相同,则需要使用 bulidx 进行交叉编译,这并不在本文的讨论过程中

或者你可以跳过实践「手动部署」部分,而记得在后文编写 GitHub Actions 时记得选择与目标匹配的架构编译

首先是在服务器安装 Docker 和 Docker Compose,这一点可以前往官方文档查看

BASH
nx@debian:~$ docker -v
Docker version 20.10.24+dfsg1, build 297e128
nx@debian:~$ docker-compose -v
Docker Compose version v2.25.0

当然,你在本地也应当安装 Docker 和 Docker Compose,一般情况下 Docker Desktop 或者 OrbStack 会是很好的选择

如果你从未听说过 Docker 与 Docker Compose,可以前往 Bilibili 或者 YouTube 学习

编译 Docker Image

接下来从项目根目录的 Dockerfile 编译 Docker Image

DOCKERFILE
FROM golang:latest as go-build-stage

ENV GOPROXY https://goproxy.cn,direct

WORKDIR /go/src/app

COPY . .

RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .

FROM scratch

WORKDIR /app

COPY --from=go-build-stage /go/src/app/main .

CMD ["./main"]
PLAINTEXT
docker build -t gin-rush-template:v1 .

编译完成后检查得到的 Image

PLAINTEXT
docker images
REPOSITORY                           TAG       IMAGE ID       CREATED              SIZE
gin-rush-template                    v1        c21723202467   About a minute ago   21.1MB

推送至 Docker Hub

现在我们要将这个 Image 传递到服务器上,这里可以上传到某些镜像仓库中,或者导出为文件再在服务器上从文件导入

这里我们直接上传到 Docker Hub(或者你可以使用一些云厂商的服务,这就需要查看对应的文档)

假设你已经注册了 Docker Hub 的账号,请在终端使用 docker login 进行登陆

在推送镜像之前,需要确保镜像被正确标记,以便符合 Docker Hub 的格式要求

一般的格式为

PLAINTEXT
<你的用户名>/<应用名>:<版本标签>

而我们目前的镜像并没有用户名,所以需要使用 docker tag 重新命名

PLAINTEXT
docker tag gin-rush-template:v1 yourusername/gin-rush-template:v1

请将 yourusername 替换为你的 Docker Hub 用户名,比如我需要执行

PLAINTEXT
docker tag gin-rush-template:v1 nxofficial/gin-rush-template:v1

现在,我们得到了一个名为 nxofficial/gin-rush-template 的镜像,它与之前的镜像指向一个相同的 ID,说明它们其实是一样的

PLAINTEXT
docker images
REPOSITORY                           TAG       IMAGE ID       CREATED          SIZE
gin-rush-template                    v1        c21723202467   14 minutes ago   21.1MB
nxofficial/gin-rush-template        v1        c21723202467   14 minutes ago   21.1MB

下面,可以使用 docker push 推送镜像(如果被拒绝请先使用 docker login 进行登陆)

PLAINTEXT
docker push nxofficial/gin-rush-template:v1
The push refers to repository [docker.io/nxofficial/gin-rush-template]
c84414e107f0: Pushed
3b8bd6ab22a4: Pushed
v1: digest: sha256:4d1b5f800ba3ec7022fc9dec77ec1e8fd9d2dc103825373ab2dec2c3b6015fb5 size: 734

现在在 Docker Hub 上已经能找到我们推送的镜像

拉取并运行镜像

下面的任务就是在服务器上拉取并运行镜像

首先找到 deploy 目录,其中的文件需要上传至服务器

DOCKERFILE
tree .
.
├── config.yaml
└── docker-compose.yaml

1 directory, 2 files

其中 congfig.yaml 是要映射进 Docker 容器的配置文件

注意如何在 Docker 网络中访问某个容器提供的服务:直接使用服务名作为域名即可

YAML
Mysql:
  Host: "mysql" # 而不是 127.0.0.1
  Port: "3306"

docker-compose.yaml 用于编排服务

DOCKERFILE
version: '2.1' # 之所以不用3是因为3砍了好用的健康检查而逼你用没人用的 Docker Swarm

networks:
  gin-rush-template-net:
    driver: bridge

services:
  app:
    image: nxofficial/gin-rush-template:v1 # 这里后面被改成了 latest,但是先使用 v1 体验一下
    container_name: gin-rush-template-app
    volumes:
        - ./config.yaml:/app/config/config.yaml # 将配置文件映射进来
    ports:
      - "8080:8080"
    depends_on:
      mysql:
        condition: service_healthy
    networks:
      - gin-rush-template-net

  mysql:
    image: mysql:8.0
    container_name: gin-rush-template-mysql
    environment:
      MYSQL_ROOT_PASSWORD: 12345678
      MYSQL_DATABASE: gin-rush-template
      TZ: Asia/Shanghai
    healthcheck:
      # MySQL 就绪检测
      test: [ "CMD", "mysqladmin" ,"ping", "-h", "localhost" ]
      interval: 5s
      retries: 10
    privileged: true
    restart: always
    networks:
      - gin-rush-template-net

之后运行即可,他会自动拉下来

BASH

$ sudo docker-compose up
WARN[0000] /Users/nx/GolandProjects/gin-rush-template/deploy/docker-compose.yaml: `version` is obsolete
[+] Running 2/0
 ✔ Container gin-rush-template-mysql  Created                                                                                                     0.0s
 ✔ Container gin-rush-template-app    Created                                                                                                     0.0s
Attaching to gin-rush-template-app, gin-rush-template-mysql
gin-rush-template-mysql  | 2024-03-22 21:21:20+08:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 8.0.36-1.el8 started.
gin-rush-template-mysql  | 2024-03-22 21:21:20+08:00 [Note] [Entrypoint]: Switching to dedicated user 'mysql'
gin-rush-template-mysql  | 2024-03-22 21:21:20+08:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 8.0.36-1.el8 started.
gin-rush-template-mysql  | '/var/lib/mysql/mysql.sock' -> '/var/run/mysqld/mysqld.sock'
gin-rush-template-mysql  | 2024-03-22T13:21:21.278083Z 0 [Warning] [MY-011068] [Server] The syntax '--skip-host-cache' is deprecated and will be removed in a future release. Please use SET GLOBAL host_cache_size=0 instead.
gin-rush-template-mysql  | 2024-03-22T13:21:21.279792Z 0 [System] [MY-010116] [Server] /usr/sbin/mysqld (mysqld 8.0.36) starting as process 1
gin-rush-template-mysql  | 2024-03-22T13:21:21.284859Z 1 [System] [MY-013576] [InnoDB] InnoDB initialization has started.
gin-rush-template-mysql  | 2024-03-22T13:21:21.367493Z 1 [System] [MY-013577] [InnoDB] InnoDB initialization has ended.
gin-rush-template-mysql  | 2024-03-22T13:21:21.494743Z 0 [Warning] [MY-010068] [Server] CA certificate ca.pem is self signed.
gin-rush-template-mysql  | 2024-03-22T13:21:21.494766Z 0 [System] [MY-013602] [Server] Channel mysql_main configured to support TLS. Encrypted connections are now supported for this channel.
gin-rush-template-mysql  | 2024-03-22T13:21:21.495550Z 0 [Warning] [MY-011810] [Server] Insecure configuration for --pid-file: Location '/var/run/mysqld' in the path is accessible to all OS users. Consider choosing a different directory.
gin-rush-template-mysql  | 2024-03-22T13:21:21.504335Z 0 [System] [MY-011323] [Server] X Plugin ready for connections. Bind-address: '::' port: 33060, socket: /var/run/mysqld/mysqlx.sock
gin-rush-template-mysql  | 2024-03-22T13:21:21.504354Z 0 [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '8.0.36'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server - GPL.
gin-rush-template-app    |
gin-rush-template-app    | 2024/03/22 13:21:26 /go/src/app/internal/global/database/mysql.go:36
gin-rush-template-app    | [0.078ms] [rows:-] SELECT DATABASE()
gin-rush-template-app    |
gin-rush-template-app    | 2024/03/22 13:21:26 /go/src/app/internal/global/database/mysql.go:36
gin-rush-template-app    | [1.725ms] [rows:1] SELECT SCHEMA_NAME from Information_schema.SCHEMATA where SCHEMA_NAME LIKE 'gin-rush-template%' ORDER BY SCHEMA_NAME='gin-rush-template' DESC,SCHEMA_NAME limit 1
gin-rush-template-app    |
gin-rush-template-app    | 2024/03/22 13:21:26 /go/src/app/internal/global/database/mysql.go:36
gin-rush-template-app    | [0.887ms] [rows:-] SELECT count(*) FROM information_schema.tables WHERE table_schema = 'gin-rush-template' AND table_name = 'user' AND table_type = 'BASE TABLE'
gin-rush-template-app    |
gin-rush-template-app    | 2024/03/22 13:21:26 /go/src/app/internal/global/database/mysql.go:36
gin-rush-template-app    | [0.053ms] [rows:-] SELECT DATABASE()
gin-rush-template-app    |
gin-rush-template-app    | 2024/03/22 13:21:26 /go/src/app/internal/global/database/mysql.go:36
gin-rush-template-app    | [0.200ms] [rows:1] SELECT SCHEMA_NAME from Information_schema.SCHEMATA where SCHEMA_NAME LIKE 'gin-rush-template%' ORDER BY SCHEMA_NAME='gin-rush-template' DESC,SCHEMA_NAME limit 1
gin-rush-template-app    |
gin-rush-template-app    | 2024/03/22 13:21:26 /go/src/app/internal/global/database/mysql.go:36
gin-rush-template-app    | [1.952ms] [rows:-] SELECT * FROM `user` LIMIT 1
gin-rush-template-app    |
gin-rush-template-app    | 2024/03/22 13:21:26 /go/src/app/internal/global/database/mysql.go:36
gin-rush-template-app    | [0.816ms] [rows:-] SELECT column_name, column_default, is_nullable = 'YES', data_type, character_maximum_length, column_type, column_key, extra, column_comment, numeric_precision, numeric_scale , datetime_precision FROM information_schema.columns WHERE table_schema = 'gin-rush-template' AND table_name = 'user' ORDER BY ORDINAL_POSITION
gin-rush-template-app    |
gin-rush-template-app    | 2024/03/22 13:21:26 /go/src/app/internal/global/database/mysql.go:36
gin-rush-template-app    | [0.070ms] [rows:-] SELECT DATABASE()
gin-rush-template-app    |
gin-rush-template-app    | 2024/03/22 13:21:26 /go/src/app/internal/global/database/mysql.go:36
gin-rush-template-app    | [0.190ms] [rows:1] SELECT SCHEMA_NAME from Information_schema.SCHEMATA where SCHEMA_NAME LIKE 'gin-rush-template%' ORDER BY SCHEMA_NAME='gin-rush-template' DESC,SCHEMA_NAME limit 1
gin-rush-template-app    |
gin-rush-template-app    | 2024/03/22 13:21:26 /go/src/app/internal/global/database/mysql.go:36
gin-rush-template-app    | [0.534ms] [rows:-] SELECT count(*) FROM information_schema.statistics WHERE table_schema = 'gin-rush-template' AND table_name = 'user' AND index_name = 'idx_user_deleted_at'
gin-rush-template-app    |
gin-rush-template-app    | 2024/03/22 13:21:26 /go/src/app/internal/global/database/mysql.go:36
gin-rush-template-app    | [0.039ms] [rows:-] SELECT DATABASE()
gin-rush-template-app    |
gin-rush-template-app    | 2024/03/22 13:21:26 /go/src/app/internal/global/database/mysql.go:36
gin-rush-template-app    | [0.149ms] [rows:1] SELECT SCHEMA_NAME from Information_schema.SCHEMATA where SCHEMA_NAME LIKE 'gin-rush-template%' ORDER BY SCHEMA_NAME='gin-rush-template' DESC,SCHEMA_NAME limit 1
gin-rush-template-app    |
gin-rush-template-app    | 2024/03/22 13:21:26 /go/src/app/internal/global/database/mysql.go:36
gin-rush-template-app    | [0.309ms] [rows:-] SELECT count(*) FROM information_schema.statistics WHERE table_schema = 'gin-rush-template' AND table_name = 'user' AND index_name = 'idx_user_email'
gin-rush-template-app    | [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
gin-rush-template-app    |  - using env:    export GIN_MODE=release
gin-rush-template-app    |  - using code:    gin.SetMode(gin.ReleaseMode)
gin-rush-template-app    |
gin-rush-template-app    | [GIN-debug] POST   /login                    --> gin-rush-template/internal/module/user.Login (3 handlers)
gin-rush-template-app    | [GIN-debug] POST   /register                 --> gin-rush-template/internal/module/user.Create (3 handlers)
gin-rush-template-app    | [GIN-debug] GET    /ping                     --> gin-rush-template/internal/module/ping.(*ModulePing).InitRouter.func1 (3 handlers)
gin-rush-template-app    | [GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
gin-rush-template-app    | Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
gin-rush-template-app    | [GIN-debug] Listening and serving HTTP on 0.0.0.0:8080
gin-rush-template-app    | 2024/03/22 13:21:26 Init Module: User
gin-rush-template-app    | 2024/03/22 13:21:26 Init Module: Ping
gin-rush-template-app    | 2024/03/22 13:21:26 InitRouter: User
gin-rush-template-app    | 2024/03/22 13:21:26 InitRouter: Ping

如果你的服务器位于国内,可能会遇到网络问题,但这不属于本文的内容

测试运行

在服务器上测试

BASH
curl 127.0.0.1:8080/ping
{"message":"pong"}

在外部测试

BASH
curl http://debian.orb.local:8080/ping
{"message":"pong"}

可见服务已经正常运行


实现自动化部署

为了实现自动化部署,我们需要有两个环节的自动化:

  1. 每次提交自动构建新镜像并推送至 Docker Hub
  2. 在 Docker Hub 每次收到新的更新时自动拉取并替换为新的 Image

我们可以先实现第二个自动化

自动拉取并替换

我们可以先更改一下代码,突出与之前的不同

DIFF
 func (p *ModulePing) InitRouter(r *gin.RouterGroup) {
     r.GET("/ping", func(c *gin.Context) {
         c.JSON(200, gin.H{
             "message": "pong",
+            "version": "v2",
         })
    })
 }

为了监听更新,我们可以使用 Watchtower ,他能自动监听镜像的更新并进行替换

DOCKERFILE
watchtower:
  image: containrrr/watchtower
  container_name: gin-rush-template-watchtower
  command: --interval 5
  volumes:
    - /var/run/docker.sock:/var/run/docker.sock # 需要将 Docker 的 sock 映射进去以控制 Docker
  restart: always
  networks:
    - gin-rush-template-net

同时,为了始终拉取最新的版本,我们可以将 app 的预期版本设置为 latest

DIFF
- image: nxofficial/gin-rush-template:v1
+ image: nxofficial/gin-rush-template:latest

然后在本地编译 Docker Image

PLAINTEXT
# 构建镜像并添加多个标签
docker build -t nxofficial/gin-rush-template:v2 -t nxofficial/gin-rush-template:latest .
# 将两个标签都推送过去
docker push nxofficial/gin-rush-template:latest
docker push nxofficial/gin-rush-template:v2

但latest 和 v2 其实是一个版本,每次重复操作就可以保留每个版本的 Docker Image 并且将 latest 指向最新版本

现在,重新启动 docker-compose

PLAINTEXT
nx@debian:/Users/nx/GolandProjects/gin-rush-template/deploy$ sudo docker-compose down
WARN[0000] /Users/nx/GolandProjects/gin-rush-template/deploy/docker-compose.yaml: `version` is obsolete
[+] Running 3/0
 ✔ Container gin-rush-template-app       Removed                                                                                                  0.0s
 ✔ Container gin-rush-template-mysql     Removed                                                                                                  0.0s
 ✔ Network deploy_gin-rush-template-net  Removed                                                                                                  0.0s
nx@debian:/Users/nx/GolandProjects/gin-rush-template/deploy$ sudo docker-compose up
WARN[0000] /Users/nx/GolandProjects/gin-rush-template/deploy/docker-compose.yaml: `version` is obsolete
[+] Running 7/7
 ✔ app 2 layers [⣿⣿]      0B/0B      Pulled                                                                                                      32.2s
   ✔ 11af50565267 Already exists                                                                                                                  0.0s
   ✔ 7eb7e9370331 Pull complete                                                                                                                  25.0s
 ✔ watchtower 3 layers [⣿⣿⣿]      0B/0B      Pulled                                                                                              31.4s
   ✔ 57241801ebfd Pull complete                                                                                                                   9.7s
   ✔ 3d4f475b92a2 Pull complete                                                                                                                   9.4s
   ✔ b6a140e9726f Pull complete                                                                                                                  21.0s
[+] Running 4/3
 ✔ Network deploy_gin-rush-template-net    Created                                                                                                0.0s
 ✔ Container gin-rush-template-watchtower  Created                                                                                                0.1s
 ✔ Container gin-rush-template-mysql       Created                                                                                                0.1s
 ✔ Container gin-rush-template-app         Created                                                                                                0.0s

看看,现在已经是 v2 了

BASH
curl 127.0.0.1:8080/ping
{"message":"pong","version":"v2"}

这时,再把版本改成 v3 并重新上传

PLAINTEXT
docker build -t nxofficial/gin-rush-template:v3 -t nxofficial/gin-rush-template:latest .
docker push nxofficial/gin-rush-template:v3
docker push nxofficial/gin-rush-template:latest

发现 Watchtower 有响应了

BASH
gin-rush-template-watchtower  | time="2024-03-22T13:57:01Z" level=info msg="Session done" Failed=0 Scanned=3 Updated=0 notify=no
gin-rush-template-watchtower  | time="2024-03-22T13:57:21Z" level=info msg="Found new nxofficial/gin-rush-template:latest image (e3283961517d)"
gin-rush-template-watchtower  | time="2024-03-22T13:57:36Z" level=info msg="Stopping /gin-rush-template-app (cbfe32e3ed41) with SIGTERM"
gin-rush-template-app exited with code 2
gin-rush-template-watchtower  | time="2024-03-22T13:57:37Z" level=info msg="Creating /gin-rush-template-app"
gin-rush-template-watchtower  | time="2024-03-22T13:57:38Z" level=info msg="Session done" Failed=0 Scanned=3 Updated=1 notify=no

测试,貌似完美

PLAINTEXT
curl 127.0.0.1:8080/ping
{"message":"pong","version":"v3"}

自动打包并上传

接下来就要实现每次自动打包并上传的功能了,这一般可以使用 GitHub Actions

在项目根目录下创建 .github/workflows/docker-publish.yaml

我选择了使用 bulidx 实现交叉编译以支持多种架构

YAML

name: Build and Push Docker Image

on:
  push:
    branches:
      - main # 指定触发事件的分支,这里是 main 分支

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    steps:
      - name: Check out the code
        uses: actions/checkout@v3

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      # 记得在 GitHub 仓库的 Secrets 中添加 DOCKER_USERNAME DOCKER_PASSWORD DOCKER_REPOSITORY 三个环境变量
      - name: Log in to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Set short SHA
        id: shortsha
        run: echo "SHORT_SHA=$(echo ${{ github.sha }} | cut -c1-8)" >> $GITHUB_ENV

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          file: ./Dockerfile
          push: true
          # 使用 commit hash 作为镜像 tag
          tags: |
            ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPOSITORY }}:latest
            ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPOSITORY }}:${{ env.SHORT_SHA }}
          platforms: linux/amd64,linux/arm64 # 为多个架构编译,如果你确定只需要其中一种,可以仅保留一种

同时请前往 GitHub 上配置你的环境变量,这样运行时就可以读取

image-20240425下午45809267

之后推送 commit,等待自动化编译完成

实际上很多时候会使用 commit hash 作为 tag,我在这里也是这么处理的

回到服务器,发现 Watchtower 的确拉取了最新的镜像

YAML
curl 127.0.0.1:8080/ping
{"message":"pong","version":"v4"}

大功告成❤️