项目地址

校园墙在线体验

前端仓库 · 后端仓库

准备服务器、装运行环境、启动后端、配置 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
2
3
4
5
6
7
8
9
10
浏览器
│ HTTPS
▼
Nginx
├─ 页面、JS、CSS ──────────→ 前端构建产物
├─ /api/ ─────────────────→ Node.js 后端
└─ /socket.io/ ────────────→ Socket.IO
│
▼
PostgreSQL

前端构建后交给 Nginx 提供服务,不需要在生产服务器上一直运行 Vite 开发服务器。API 和 Socket.IO 通过同一个网站域名访问,构建时设置 VITE_API_BASE_URL=/。

准备服务器环境

刚连上服务器时,先检查身份、系统和资源情况

1
2
3
4
5
whoami
cat /etc/os-release
uname -m
free -h
df -h /

当时默认登录的是 admin。安装系统软件等管理操作需要切换身份:

1
sudo -i

管理服务器时使用 root,并不意味着应用进程应该一直由 root 运行。后面我给后端使用了 campuswall 账号,自动发布又单独创建了 campusdeploy。

检查软件时,我也遇到一个误区:镜像里存在 /www/server/nodejs 目录,不代表 Node 已安装好。那个目录当时只有配置相关内容。

真正需要检查的是命令和进程:

1
2
3
4
5
6
for tool in node npm nginx psql git; do
printf '\n%s: ' "$tool"
command -v "$tool" || true
done

ss -lntp

这一步还可以避免在宝塔安装的服务之外,又启动另一套占用相同端口的服务。

域名和备案

对于中国内地服务器上的网站,公开提供服务前需要按适用要求完成 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
2
nslookup campus-wall.me
curl.exe -i --max-time 10 http://campus-wall.me/.well-known/acme-challenge/check.txt

当时 DNS 已经返回正确的 IP,HTTP 却连接失败。需要继续分层检查:云防火墙、系统防火墙、Nginx 是否启动,以及它究竟监听 127.0.0.1:80 还是公网可达的地址。

1
2
ss -lntp '( sport = :80 )'
systemctl status nginx --no-pager

如果 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
2
3
4
5
location ^~ /.well-known/acme-challenge/ {
root /var/www/letsencrypt;
default_type text/plain;
try_files $uri =404;
}

它把不存在的验证文件明确返回为 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
2
3
tail -n 50 /var/log/nginx/error.log
ss -lntp '( sport = :80 )'
systemctl cat nginx --no-pager

那时旧进程还在监听 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/opt/campus-wall/
├── backend/ # 最初手动部署的后端,保留
└── deploy/
├── backend/
│ ├── current -> releases/某个版本
│ └── releases/
├── incoming/ # 上传包暂存
└── shared/
└── .env # 生产环境配置

/var/www/
├── campus-wall/ # 最初手动部署的前端,保留
└── campus-wall-deploy/
├── current -> releases/某个版本
└── releases/

/var/backups/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
2
Name:  ENABLE_AUTO_DEPLOY
Value: true

这个开关要创建成 Repository variable,不能只放在 production 环境变量里,因为工作流在进入部署任务前就会判断它。GitHub 的 Secrets 文档 和 Variables 文档 分别说明了这些配置入口。

更新项目需要单独维护的内容

有几类内容仍然需要单独维护:

修改内容 处理方式
生产 .env 修改服务器共享配置,然后重启后端
Nginx、systemd、证书和部署脚本 单独安装或更新服务器配置,并验证

现在生产环境变量维护的位置是:

1
/opt/campus-wall/deploy/shared/.env

修改后重启后端:

1
sudo systemctl restart campus-wall-backend

如果需要排查发布结果,下面这些命令会经常用到:

1
2
3
4
5
6
readlink -f /opt/campus-wall/deploy/backend/current
readlink -f /var/www/campus-wall-deploy/current
systemctl status campus-wall-backend --no-pager
journalctl -u campus-wall-backend -n 80 --no-pager
curl -fsS http://127.0.0.1:3001/api/health
df -h /

想暂停后续自动发布,可以把对应仓库的 ENABLE_AUTO_DEPLOY 改成 false。已经开始的发布任务不会因此自动停止,维护前还要确认当前任务已经结束。

完整的工作流、服务配置和脚本放在 后端 deployment 目录,首次接入步骤整理在 AUTO-DEPLOY.md。这些脚本针对本文的账号、目录和单机环境编写,换项目时需要先调整。

这次最有用的收获,是开始知道一个报错应该去哪一层找证据。命令找不到先看 PATH,外部访问失败看监听和网络,响应内容不对看 Nginx 路由,权限不足沿着父目录向上检查。下一次再遇到部署问题,可以从这些具体的检查开始。