今年做了一个工业数字孪生 Demo(wxbuluo-twin):前端 Next.js + R3F 做 3D 实时大屏,后端 Go + WebSocket 广播设备遥测。功能本身不复杂,真正磨人的是怎么把它稳稳地部署到生产——尤其是手里只有一台 2G 内存的小服务器,前端还想用 Vercel 白嫖。这篇文章讲清楚从架构决策到 CI/CD 落地的全过程,所有坑都是真撞过的。
一、为什么必须前后端拆开
一开始的想法很朴素:一台服务器、一个域名、前后端一起跑。但两个硬约束把这条路堵死了:
1. 2G 内存跑不动双进程。 前端 Next.js(Node 运行时)常驻就要吃掉几百 MB,加上 Go 后端、加 nginx,2G 内存频繁 swap,部署一次卡到怀疑人生。所以前端必须外迁——Vercel 的 Edge/Serverless 不占我服务器资源,刚好。
2. WebSocket 长连接穿不过 Vercel。 Vercel 的 Serverless Function 不适合长连接,而且一个域名 DNS 只能指一处:wxbuluo-twin.vercel.app 的流量全进 Vercel,/ws/ 无法回源到我服务器。数字孪生的实时遥测恰恰是 WS 长连接,必须给后端单独开个子域。
结论很清晰:前端上 Vercel,后端留服务器,WS 走独立子域 twin-api.wxbuluo.com。
二、子域与端口规划
| 层 | 地址 | 说明 |
|---|---|---|
| 前端 | wxbuluo-twin.vercel.app(规划绑 twin.wxbuluo.com) | Vercel 托管,不占服务器 |
| 后端 WS | twin-api.wxbuluo.com:443 | nginx 反代到容器内后端 |
| 后端容器 | twin-server(docker 网络内 :8080) | 不占宿主 8080(被 frps 占着) |
| 镜像 | ghcr.io/ningbnii/wxbuluo-twin-server:latest | GitHub 构建推送,服务器只拉 |
关键设计:twin-server 只在 docker_default 网络内暴露 8080,由 nginx 反代出去,不抢宿主端口。宿主 8080 已经被 frps 占用,这样两者互不干扰。
三、CI/CD 双路:同一个 main,按目录分流
整个仓库是单仓,但前端和后端走完全不同的两条部署链路,靠目录过滤分开:
前端 web/** → Vercel(GitHub webhook 自动部署)
- 项目从 Git 导入时,Vercel 在 GitHub 建了 webhook;
git push origin main自动触发构建部署。 - 本地不需要装 vercel CLI,所谓"本地授权一下"就是导入时 GitHub 给 Vercel 的 OAuth,它顺手建好了 webhook。
- 前端连后端必须设环境变量
NEXT_PUBLIC_WS_URL=wss://twin-api.wxbuluo.com/ws/(Production/Preview/Development 三档全选)。这个变量是构建期内联的,改了变量不会自动部署,必须手动 Redeploy;但 push 代码一定自动部署。
后端 server/** → GitHub Actions(.github/workflows/deploy.yml)
serverjob:go vet + 100% 覆盖率门禁 +go build ./...。deploy-serverjob:GitHub runner 上docker/build-push-action构建镜像推到ghcr.io,然后 SSH 到服务器docker pull+docker compose up -d,服务段统一纳管在服务器的/docker/docker-compose.yml。- 服务器 2G 内存绝不本地构建镜像,镜像统一在 runner 上造好,服务器只负责拉和起。
四、nginx 反代 WebSocket:slb 架构
服务器上对外 SSL 的是 nginx 容器 slb(映射宿主 0.0.0.0:80/443,挂在 docker_default 网络,和 twin-server 同网)。这点很关键:因为同网络,proxy_pass http://twin-server:8080 直接用容器名解析,不用写 IP。
nginx/twin-api.wxbuluo.com.conf(仓库里是唯一事实源)核心三段:
# 80 端口:配合 certbot 做 HTTP-01 验证
server {
listen 80;
server_name twin-api.wxbuluo.com;
location /.well-known/acme-challenge/ { root /etc/nginx/conf.d/acme; }
location / { return 301 https://$host$request_uri; }
}
# 443 端口:WS 升级头 + 长超时
server {
listen 443 ssl;
server_name twin-api.wxbuluo.com;
ssl_certificate /etc/letsencrypt/live/twin-api.wxbuluo.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/twin-api.wxbuluo.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location /ws/ {
proxy_pass http://twin-server:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
location / { return 404; } # 本子域只服务 WS
}
证书用 certbot 容器签发(certbot/certbot certonly --webroot),并在 cron 里加了续期行 renew --cert-name twin-api.wxbuluo.com——否则默认续期只管 analytics/www,90 天后会过期。
五、踩坑实录(每个都真撞过)
build-push-action的file:相对仓库根解析。file: Dockerfile.server找不到,必须写server/Dockerfile.server,和context: server不是一套相对基准。- Go 版本三处必须一致。
go.mod写go 1.27,Dockerfile 基础镜像却用golang:1.22——Go 1.21+ 默认GOTOOLCHAIN=auto,CI runner 能联网下 toolchain 门禁过,但 Docker 构建容器内拉不到go.dev/dl,go mod download直接 exit 1。修法:镜像和setup-go同步升 1.27。 docker compose up --pull参数错。新版--pull要的是always/missing/never值,不是服务名,--pull twin-server会报invalid --pull option。前面已单独pull,这里直接去掉。- nginx 反代架构认知。
web1容器只在内网不对外,新子域 conf 必须放slb的/docker/nginx/slb/conf.d/;conf 必须含acme-challengelocation 和options-ssl-nginx.conf/ssl_dhparam,否则签发被 80→443 跳转挡住、SSL 配置也不完整。 - curl 测 WS 必须
--http1.1。curl 默认走 HTTP/2,WS 升级只在 HTTP/1.1 上,否则返回400 Bad Request——这是测试工具陷阱,浏览器真实客户端不受影响。 - git push 带二进制超 postBuffer。一次提交了 7.1MB 的 GLB/wasm/jpg,
git push报 HTTP 400 / RPC failed。git config http.postBuffer 1073741824(1G)后一次过。 - Vercel 设变量走错面板。不是 "Create Environment" 弹窗(那是 Pro 的自定义环境功能),而是左侧菜单 Environment Variables,表格三列 Key/Value/Environments。
六、验证结果(实锤)
twin-server容器运行中,镜像ghcr.io/ningbnii/wxbuluo-twin-server:latestcurl <容器IP>:8080/healthz→{"status":"ok"}- 外部
wss://twin-api.wxbuluo.com/ws/(HTTP/1.1)→101 Switching Protocols,后端实时推送 assets/metrics/tick/alarm - twin-api 证书 Let's Encrypt 有效至 2026-11-28(续期 cron 已加)
- 前端 Vercel 自动部署 Ready,WS 直连 twin-api 成功
七、小结与系列预告
这套前后端分离 + CI/CD 双路的本质,就是把"重"的交给平台(Vercel 扛前端、GitHub runner 扛镜像构建),只留"轻"的在服务器(拉镜像 + 起容器 + 反代)。2G 内存的小服务器也能稳稳跑起一个带实时 WS 的数字孪生。
这个系列后面两篇会深入具体技术点:
- (二)前端 3D 实时渲染:R3F + GLB 模型加载,以及"浏览器里看不到模型纹理"的完整诊断与修复(envMap / metalness / 色彩空间 / UV repeat)。
- (三)Go + WebSocket 实时遥测后端:从 gorilla Hub 广播、CheckOrigin 同源白名单,到 GitHub Actions 推镜像自动部署的逐行拆解。
如果哪块想先看,或者想让我把某个坑展开成独立短文,直接说。