GitHub Actions
安装 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"}
可见服务已经正常运行
实现自动化部署
为了实现自动化部署,我们需要有两个环节的自动化:
- 每次提交自动构建新镜像并推送至 Docker Hub
- 在 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 上配置你的环境变量,这样运行时就可以读取

之后推送 commit,等待自动化编译完成
实际上很多时候会使用 commit hash 作为 tag,我在这里也是这么处理的
回到服务器,发现 Watchtower 的确拉取了最新的镜像
YAML
curl 127.0.0.1:8080/ping
{"message":"pong","version":"v4"}
大功告成❤️