有这样一个需求,某机构有一个像是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应用于实践了。
把一个没什么经济上回报的项目写成一个模板,之后再有类似的项目,接,而且不管回报如何,至少没啥投入了[狗头]
但,我似乎没太从这个项目中学到什么东西呢。