我之前其实写过一些 cicd 的教程 ,但是写的太烂了,这篇算重构
直入正题,我会用我的一个 gin-rush-template 来做为 demo 演示
你可以模仿下面的过程为自己的项目实现自动化部署
我的 demo 的其实写的很烂的,但是你只需要知道它有三个特点,无需关注其他细节:
会从 config/config.yaml
读取配置文件
需要连接 MySQL 依赖
能提供一个 /ping
接口
我们先做到让它在本地跑起来,首先 clone 下来,然后跟随下面的操作
1 2 3 4 5 6 7 8 # 复制一份配置文件 cp config/config.example.yaml config/config.yaml # 运行 MySQL 依赖 docker-compose -f docker-compose-env.yml up -d # 运行 Go 程序 go run main.go
如果没有报错,并且 curl 能正常提供服务,那么就没有问题了
1 2 curl http://127.0.0.1:8080/ping {"message":"pong"}
或者你可以在浏览器中手动访问 http://127.0.0.1:8080/ping
体验手动部署
安装 Docker
接下来将体验手动编译并部署 Docker Image 到服务器,我以本地 OrbStack 提供的 Debian12 arm64 虚拟机为例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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,这一点可以前往官方文档 查看
1 2 3 4 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 FROM golang:latest as go-build-stageENV GOPROXY https://goproxy.cn,directWORKDIR /go/src/app COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main . FROM scratchWORKDIR /app COPY --from=go-build-stage /go/src/app/main . CMD ["./main" ]
1 docker build -t gin-rush-template:v1 .
编译完成后检查得到的 Image
1 2 3 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 的格式要求
一般的格式为
而我们目前的镜像并没有用户名,所以需要使用 docker tag
重新命名
1 docker tag gin-rush-template:v1 yourusername/gin-rush-template:v1
请将 yourusername
替换为你的 Docker Hub 用户名,比如我需要执行
1 docker tag gin-rush-template:v1 nxofficial/gin-rush-template:v1
现在,我们得到了一个名为 nxofficial/gin-rush-template
的镜像,它与之前的镜像指向一个相同的 ID,说明它们其实是一样的
1 2 3 4 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
进行登陆)
1 2 3 4 5 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 上已经能找到我们推送的镜像
你可以看见,这个镜像是 arm64 架构
拉取并运行镜像
下面的任务就是在服务器上拉取并运行镜像
首先找到 deploy
目录,其中的文件需要上传至服务器
1 2 3 4 5 6 tree . . ├── config.yaml └── docker-compose.yaml 1 directory, 2 files
其中 congfig.yaml
是要映射进 Docker 容器的配置文件
注意如何在 Docker 网络中访问某个容器提供的服务:直接使用服务名作为域名即可
1 2 3 Mysql: Host: "mysql" Port: "3306"
而 docker-compose.yaml
用于编排服务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 version: '2.1' networks: gin-rush-template-net: driver: bridge services: app: image: nxofficial/gin-rush-template: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 : test: [ "CMD" , "mysqladmin" ,"ping" , "-h" , "localhost" ] interval: 5 s retries: 10 privileged: true restart: always networks: - gin-rush-template-net
之后运行即可,他会自动拉下来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 $ 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 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
如果你的服务器位于国内,可能会遇到网络问题,但这不属于本文的内容
测试运行
在服务器上测试
1 2 curl 127.0.0.1:8080/ping {"message" :"pong" }
在外部测试
1 2 curl http://debian.orb.local:8080/ping {"message" :"pong" }
可见服务已经正常运行
实现自动化部署
为了实现自动化部署,我们需要有两个环节的自动化:
每次提交自动构建新镜像并推送至 Docker Hub
在 Docker Hub 每次收到新的更新时自动拉取并替换为新的 Image
我们可以先实现第二个自动化
自动拉取并替换
我们可以先更改一下代码,突出与之前的不同
1 2 3 4 5 6 7 8 func (p *ModulePing) InitRouter(r *gin.RouterGroup) { r.GET("/ping", func(c *gin.Context) { c.JSON(200, gin.H{ "message": "pong", + "version": "v2", }) }) }
为了监听更新,我们可以使用 Watchtower ,他能自动监听镜像的更新并进行替换
1 2 3 4 5 6 7 8 9 watchtower: image: containrrr/watchtower container_name: gin-rush-template-watchtower command: --interval 5 volumes: - /var/run /docker.sock:/var/run/docker.sock restart: always networks: - gin-rush-template-net
同时,为了始终拉取最新的版本,我们可以将 app 的预期版本设置为 latest
1 2 - image: nxofficial/gin-rush-template:v1 + image: nxofficial/gin-rush-template:latest
然后在本地编译 Docker Image
1 2 3 4 5 # 构建镜像并添加多个标签 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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 了
1 2 curl 127.0.0.1:8080/ping {"message" :"pong" ,"version" :"v2" }
这时,再把版本改成 v3 并重新上传
1 2 3 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 有响应了
1 2 3 4 5 6 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
测试,貌似完美
1 2 curl 127.0.0.1:8080/ping {"message":"pong","version":"v3"}
自动打包并上传
接下来就要实现每次自动打包并上传的功能了,这一般可以使用 GitHub Actions
在项目根目录下创建 .github/workflows/docker-publish.yaml
我选择了使用 bulidx 实现交叉编译以支持多种架构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 name: Build and Push Docker Image on: push: branches: - 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 - 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 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 的确拉取了最新的镜像
1 2 curl 127.0 .0 .1 :8080/ping {"message" :"pong" ,"version" :"v4" }
大功告成❤️