有这样一个需求,某机构有一个像是Demo一样的一个Web应用,符合他们自己的需求,同时,他们想把这套应用推给其他同行业的机构用,但是他们还不想就给其他机构随便用,他们想管理。

这个Demo不具备登录功能,而且需要各个机构之间的数据不互通。

聪明的你一定看出来了,把这个Demo在不同机构的内网中部署很多遍就好啦~

但是他们想管理,不能直接分发程序。

所以登录是必须要有的,所以必须要加一个登录功能。

又因为还需要数据隔离,所以我提出这样子的架构:

技术选型

使用如下技术开发本次系统:

  • Golang 后端,我最——拿手的语言

  • Gorm ORM库

  • Gin Http路由库

  • React 前端框架

  • Antd UI框架

  • PostgreSQL 小大象数据库

在Golang上有 github.com/docker/docker 这个库,所以与Docker通信就很简单了。

这个库底层应该是调用了Docker cli,所以只要保证cli能用就可以。

Docker部署规划

开始写之前,先做规划,根据前面的架构图,系统需要这些镜像:

  • 管理系统镜像 (包含管理系统后端和编译好的前端)

  • 服务实例镜像

  • PostgreSQL数据库

这些镜像在部署的时候会使用同一个Docker bridge网络,考虑到服务实例可能很多(上百),需要用16位子网而不是默认的24位子网。

到目前为止,咱已经是会用CI/CD的人了,自然也要用上CI/CD,具体行为是,在沙盒里面构建镜像,通过SSH连接服务器部署。

其中,管理系统和数据库写在docker-compose中,服务实例镜像仅传输过去,不部署,让管理系统去部署。

具体相关脚本:

构建

#!/bin/bash

docker compose -f docker-compose.yml build

docker build core -t core-service:latest

导出

#!/bin/bash
set -e

echo "📦 从 docker-compose.yml 打包镜像..."
images=$(docker compose config | grep 'image:' | awk '{print $2}' | sort | uniq)
# 附加的不在docker-compose中的镜像
images="$images core-service:latest"

echo "镜像列表:"
echo "$images"

mkdir -p ./images-output

for image in $images; do
    sanitized=$(echo "$image" | tr '/:' '_')   # 替换特殊字符
    echo "🔹 保存 $image 到 ./images-output/${sanitized}.tar.gz"
    docker save "$image" | gzip > "./images-output/${sanitized}.tar.gz"
done

echo "✅ 所有镜像已经导出到 ./images-output/"

导入

#!/bin/bash
for f in ./images-output/*.tar.gz; do
    echo "🔄 加载 $f..."
    gunzip -c "$f" | docker load
done

echo "✅ 所有镜像加载完成!"

上传文件到服务器appleboy/scp-action@v1.0.0 ,在服务器上执行脚本就用appleboy/ssh-action@v1.2.2

于是有这样子的部署脚本:

name: 部署生产服务器
run-name: 正在部署生产服务器 🚀
on:
  push:
    tags:
      - "v*"
branches:
  - main

jobs:
  build-and-deploy:
    runs-on: ubuntu-20.04
    steps:
      - name: 📥 迁出代码
        uses: actions/checkout@v4
        with:
          fetch-depth: 1
          ref: ${{ env.CI_COMMIT_TAG }}

      - name: 🏗️ 构建前端
        run: |
          export PATH=/usr/local/bin:$PATH
          npm install -g n
          n 20
          cd web
          npm install -g yarn
          yarn
          yarn build
          cd ..
          cp -r web/dist ./go-server/static

      - name: 🐳 构建镜像
        uses: nick-fields/retry@v3
        with:
          timeout_minutes: 20
          max_attempts: 5
          command: |
            chmod +x scripts/build-compose-images.sh
            scripts/build-compose-images.sh

      - name: 📦 导出镜像
        run: |
          chmod +x scripts/save-compose-images.sh
          scripts/save-compose-images.sh
          ls -lah images-output

      - name: 📤 发送镜像到服务器
        uses: appleboy/scp-action@v1.0.0
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          port: ${{ secrets.SSH_PORT }}
          source: images-output,scripts/load-compose-images.sh
          target: /tmp/mims

      - name: 📥 在服务器上加载镜像
        uses: appleboy/ssh-action@v1.2.2
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          port: ${{ secrets.SSH_PORT }}
          script: |
            cd /tmp/mims
            ls -lah .
            ls -lah images-output
            chmod +x scripts/load-compose-images.sh
            scripts/load-compose-images.sh
            rm -rf /tmp/mims

      - name: 📤 发送部署脚本到服务器
        uses: appleboy/scp-action@v1.0.0
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          port: ${{ secrets.SSH_PORT }}
          source: docker-compose.yml
          target: /apps/mims

      - name: 🔧 在服务器上部署
        uses: appleboy/ssh-action@v1.2.2
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          port: ${{ secrets.SSH_PORT }}
          script: |
            cd /apps/mims
            docker compose down || true
            if [ ! -f application.yml ]; then
                touch application.yml
            fi
            docker compose up -d --force-recreate

项目可复用性

虽然这是针对于某机构需求写的系统,但是这个需求感觉是经常有的,所以在项目开发的过程中就给项目分了两个分支。

  • main分支 大部分开发代码提交和开发工作在此进行

  • 甲方定制分支 时常合并main上的代码,包括了甲方提供的服务实例代码、Logo、平台名等

没什么营养的部分开发

像登录鉴权、审批、增删改查什么的就交给万能的AI啦。

把前端和后端项目放在同一个文件夹下的子目录,然后让AI写全栈。

路由

甲方提供的服务实例本身是一个既有前端又有后端的Web项目,它的默认路由就在 / 上,但肯定不是/**下的所有路径都被用了,所以我将/_manage 作为管理系统的Base Path。

路由规则:

  • / 如果鉴权成功反向代理到服务实例,失败就重定向到/_manage

  • /** 如果鉴权成功反向代理到服务实例,失败拒绝

  • /_manage/api/** 管理系统的后端API,由API处理器进一步处理

  • /_manage/** 管理系统的前端页面,总是成功的,如果访问的文件未找到,返回index(Web单页APP)

鉴权

使用JWT鉴权,并且能够支持服务实例拿JWT获取用户信息。

用户分三级:

  • 超级管理员,不属于任何机构,不能访问服务实例。

  • 机构管理员,管理机构内的用户,为了避免用户退出服务实例的服务(因为设计怕用户找不到管理入口)

  • 普通用户

Docker实例管理

假定管理系统是跑在Docker里面的。(因为实例管理基于Docker,既然Docker都有了,管理系统在Docker中部署,何乐而不为呢?)

容器配置

管理系统需要能和Docker守护进程通信,但位于Docker内部的程序肯定是不能和守护进程通信的,所以需要这些操作:

映射docker.sock

需要将宿主机上的/var/run/docker.sock 通过卷映射,映射到容器内的相同地方。

安装docker-cli

golang的docker sdk 底层是通过调用docker-cli命令完成的。

创建docker-cli配置

如果没有配置,docker-cli会报错,所以需要给它创建一个空配置,使用mkdir -p /root/.docker && echo '{}' > /root/.docker/config.json

初始化

外部需要提供服务实例镜像的名字,在启动时,做这些行为:

  • 根据镜像名获取端口信息。

  • 获取当前主机名,根据主机名获取当前Docker子网名称

启动实例

  • 将所有镜像用到的卷按照一定的规则,给一个宿主机上的目录用以持久化。

  • 取镜像暴漏的第一个TCP端口,记录下来,用于反向代理。

  • 设置网络为管理系统所在的网络。

总结

这个项目可以证明,我已经学会并将Docker应用于实践了。

把一个没什么经济上回报的项目写成一个模板,之后再有类似的项目,接,而且不管回报如何,至少没啥投入了[狗头]

但,我似乎没太从这个项目中学到什么东西呢。

我能想到的,最大的成功就是无愧于自己的心。