把一个项目部署上线,并不容易...
项目地址
准备服务器、装运行环境、启动后端、配置 Nginx、处理域名备案,最后把手动上传改成 GitHub Actions 自动部署。
网站是怎么运行的
项目的分工如下:
| 部分 | 技术与职责 |
|---|---|
| 前端 | React、TypeScript、Vite,构建后生成静态文件 |
| 后端 | Node.js、Express、TypeScript,提供 API |
| 实时通信 | Socket.IO,支持聊天和画板同步 |
| 数据库 | PostgreSQL 16,通过 Prisma 访问 |
| 网站入口 | Nginx,提供静态文件、转发 API 和 Socket.IO |
| 进程管理 | systemd,管理后端启动、异常重启和开机启动 |
| HTTPS | Let’s Encrypt 证书,通过 Certbot 续期 |
| 自动发布 | GitHub Actions,前后端各有一个工作流 |
请求大致这样流转:
1 | 浏览器 |
前端构建后交给 Nginx 提供服务,不需要在生产服务器上一直运行 Vite 开发服务器。API 和 Socket.IO 通过同一个网站域名访问,构建时设置 VITE_API_BASE_URL=/。
准备服务器环境
刚连上服务器时,先检查身份、系统和资源情况
1 | whoami |
当时默认登录的是 admin。安装系统软件等管理操作需要切换身份:
1 | sudo -i |
管理服务器时使用 root,并不意味着应用进程应该一直由 root 运行。后面我给后端使用了 campuswall 账号,自动发布又单独创建了 campusdeploy。
检查软件时,我也遇到一个误区:镜像里存在 /www/server/nodejs 目录,不代表 Node 已安装好。那个目录当时只有配置相关内容。
真正需要检查的是命令和进程:
1 | for tool in node npm nginx psql git; do |
这一步还可以避免在宝塔安装的服务之外,又启动另一套占用相同端口的服务。
域名和备案
对于中国内地服务器上的网站,公开提供服务前需要按适用要求完成 ICP 备案;域名、主体、网站内容和接入信息都需要结合自己的情况确认。阿里云备案说明如果申请的是个人网站备案,那么在相关说明中不能出现“在线”“校园”等字眼,否则会被认为是责任主题不匹配有社交性质/侵权等。会被封禁的TT
在正式开放前,我先让 Nginx 监听 127.0.0.1:80,通过 SSH 隧道在自己电脑上预览。以下是在 Windows CMD 中执行的示例,前提是这个 SSH 账号已经能正常登录:
1 | ssh -N -L 127.0.0.1:8080:127.0.0.1:80 -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 admin@192.0.2.10 |
保持窗口开启,在本机浏览器访问 http://127.0.0.1:8080,请求会通过 SSH 转发给服务器上的 Nginx。这个地址只用于当前电脑的私人预览。
Nginx 和 HTTPS(我也不是很懂,由 codex 总结)
1. 软件包被 exclude 过滤
安装 Nginx 时,终端曾提示:
1 | All matches were filtered out by exclude filtering for argument: nginx |
这个提示把排查方向指向了 DNF 的排除规则。换一个软件源之前,应先检查 /etc/dnf/dnf.conf 和相关仓库配置中的 exclude 或 excludepkgs,并确认面板是否已经管理了另一套 Nginx。
不要把某台服务器的安装命令当成所有系统都适用的命令,尤其是带有预装面板的镜像。
2. DNS 解析正确,不代表端口已经能访问
域名解析到服务器后,在 Windows CMD 中检查:
1 | nslookup campus-wall.me |
当时 DNS 已经返回正确的 IP,HTTP 却连接失败。需要继续分层检查:云防火墙、系统防火墙、Nginx 是否启动,以及它究竟监听 127.0.0.1:80 还是公网可达的地址。
1 | ss -lntp '( sport = :80 )' |
如果 Nginx 只监听回环地址,之前通过 SSH 隧道能打开页面,也不能说明公网已经能访问。
3. 返回 200,却返回了错误的内容
配置证书验证时,我请求的是:
1 | /.well-known/acme-challenge/check.txt |
服务器返回了 HTTP/1.1 200 OK,正文却是 React 页面的 HTML,里面还有 id="root" 和前端 JS 文件名。
原因是请求进入了 SPA 的首页回退逻辑:找不到指定文件时,Nginx 返回 index.html。所以这次不能只看状态码,还要看响应正文是不是预期的验证文本。
证书验证路径需要单独处理。下面是放在相应 server 块里的配置片段:
1 | location ^~ /.well-known/acme-challenge/ { |
它把不存在的验证文件明确返回为 404。
Let’s Encrypt 的 HTTP-01 验证会通过公网的 80 端口访问验证路径。服务器本机能读取验证文件以后,还需要从外部网络确认同一路径可达。HTTP-01 验证说明
4. nginx -t 通过了,新配置却没有生效
替换配置后,nginx -t 显示成功,systemctl reload nginx 也没有直接报出明显错误,但验证路径仍然返回旧首页。
检查错误日志和监听端口以后,才看到:
1 | bind() to 0.0.0.0:80 failed (98: Address already in use) |
1 | tail -n 50 /var/log/nginx/error.log |
那时旧进程还在监听 127.0.0.1:80,新配置要监听 0.0.0.0:80。这次监听地址调整在重载时发生了绑定冲突,旧配置继续提供服务。
Nginx 官方文档也说明:重载时如果无法应用新配置,主进程会继续使用旧配置。Nginx 重载行为
确认占用端口的是这套服务、配置也正确以后,这次通过重启释放旧监听,再使用新配置:
1 | nginx -t && systemctl restart nginx |
用 GitHub Actions 实现自动部署
| 前端 | 后端 |
|---|---|
npm ci 安装锁定依赖 |
npm ci --include=dev 安装依赖 |
| TypeScript 检查和 Vite 生产构建 | Prisma schema 校验、客户端生成和 TypeScript 检查 |
打包 dist |
在临时 PostgreSQL 16 上执行迁移 |
| 上传并切换静态文件版本 | 测试健康接口、数据库 API 和部署回退流程 |
| 验证 Nginx 返回新版本首页 | 只打包 Git 已跟踪的源码并上传 |
搭建的工作流的主要目录如下(codex 搭的,我也没仔细研究):
1 | /opt/campus-wall/ |
后端服务的工作目录变为 /opt/campus-wall/deploy/backend/current,Nginx 的前端根目录变为 /var/www/campus-wall-deploy/current。
前端还会带上上一版使用的静态资源,尽量让已经打开旧页面的浏览器能继续加载文件。不过这不意味着所有历史版本的标签页都能无限期兼容。
服务器侧准备完成后,先从自己电脑验证专用账号的 SSH 登录和 sudo 权限:
1 | ssh -i "%USERPROFILE%\.ssh\campus-wall-actions" -o IdentitiesOnly=yes -o BatchMode=yes campusdeploy@192.0.2.10 "id; sudo -n -l" |
部署账号通过专用密钥登录,sudo 只授权固定的后端重启和数据库备份命令。
然后在两个 GitHub 仓库中分别创建 production 环境,限制部署分支为 main,配置三个 Environment secrets:
| 名称 | 内容 |
|---|---|
DEPLOY_HOST |
服务器地址 |
DEPLOY_SSH_KEY |
部署专用私钥的完整内容 |
DEPLOY_KNOWN_HOSTS |
从可信服务器终端取得的 SSH 主机公钥记录 |
.pub 公钥放到服务器授权列表里,私钥放到 GitHub Secrets;服务器主机公钥则用于确认连接的是预期的服务器。这三者用途不同。
最后在仓库的 Settings → Secrets and variables → Actions → Variables 中创建:
1 | Name: ENABLE_AUTO_DEPLOY |
这个开关要创建成 Repository variable,不能只放在 production 环境变量里,因为工作流在进入部署任务前就会判断它。GitHub 的 Secrets 文档 和 Variables 文档 分别说明了这些配置入口。
更新项目需要单独维护的内容
有几类内容仍然需要单独维护:
| 修改内容 | 处理方式 |
|---|---|
生产 .env |
修改服务器共享配置,然后重启后端 |
| Nginx、systemd、证书和部署脚本 | 单独安装或更新服务器配置,并验证 |
现在生产环境变量维护的位置是:
1 | /opt/campus-wall/deploy/shared/.env |
修改后重启后端:
1 | sudo systemctl restart campus-wall-backend |
如果需要排查发布结果,下面这些命令会经常用到:
1 | readlink -f /opt/campus-wall/deploy/backend/current |
想暂停后续自动发布,可以把对应仓库的 ENABLE_AUTO_DEPLOY 改成 false。已经开始的发布任务不会因此自动停止,维护前还要确认当前任务已经结束。
完整的工作流、服务配置和脚本放在 后端 deployment 目录,首次接入步骤整理在 AUTO-DEPLOY.md。这些脚本针对本文的账号、目录和单机环境编写,换项目时需要先调整。
这次最有用的收获,是开始知道一个报错应该去哪一层找证据。命令找不到先看 PATH,外部访问失败看监听和网络,响应内容不对看 Nginx 路由,权限不足沿着父目录向上检查。下一次再遇到部署问题,可以从这些具体的检查开始。





