从 MacBook 到 VPS:shadcn/ui 管理后台的完整部署记录
起因
我在 MacBook 本地跑了一个基于 shadcn/ui 的项目管理仪表盘(my-admin),通过 localhost:3001 访问。现在需要把它部署到 VPS,让外网也能访问。
本地环境:
- 项目路径:
/Users/binli/dev/shadcn-admin-test/my-admin - 技术栈:Next.js 16 + React 19 + Tailwind 4 + shadcn/ui
- 包管理器:pnpm
- VPS:Ubuntu 22.04,公网 IP
162.43.92.249
整个流程分两大阶段:本地 → GitHub,GitHub → VPS → 公网访问。
第一阶段:从 MacBook 推送到 GitHub
1. 找出项目位置
先确认开发服务器跑在哪个目录:
bash
lsof -i :3001 # 得到 PID,比如 28347 lsof -p 28347 | grep cwd # 输出:/Users/binli/dev/shadcn-admin-test/my-admin
2. 创建 GitHub 仓库
在 GitHub 上新建 Private 仓库 my-admin(用户名 git938),不要勾选 README、.gitignore、license。
3. 配置 .gitignore
关键:绝对不能把 .env 推上去,否则 Jira API Token 会泄露。
bash
cd /Users/binli/dev/shadcn-admin-test/my-admin echo ".env" >> .gitignore echo ".env.local" >> .gitignore echo ".env.production" >> .gitignore echo "node_modules/" >> .gitignore echo ".next/" >> .gitignore
4. 初始化并推送
bash
git init git add . git commit -m "initial commit" git branch -M main git remote add origin https://github.com/git938/my-admin.git git push -u origin main
推送时会要求输入:
- Username:
git938 - Password:Personal Access Token(不是 GitHub 登录密码)
⚠️ 踩坑记录
1. Repository not found 错误
最常见原因:GitHub 上仓库还没创建,或者仓库名拼错了。先去 https://github.com/git938?tab=repositories 确认仓库是否存在。
2. 403 Write access to repository not granted
Token 权限不足。需要在 https://github.com/settings/tokens 新建一个 classic token,勾选 repo 权限。Fine-grained token 的话,需要明确选择该仓库并授予 Contents: Read and write。
3. zsh 报 event not found: /bin/bash
粘贴脚本时,zsh 会把 ! 当作历史展开。用 set +H 关闭,或者把脚本写入文件后再执行。
4. 重要:Token 泄露处理
如果不小心把 ghp_... 开头的 Token 贴到聊天或截图里,立即去 https://github.com/settings/tokens 删除该 Token,然后重新生成。Token 等同于密码,泄露后任何人都能读写你的仓库。
第二阶段:在 VPS 上部署
1. 安装 Node.js
bash
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash source ~/.bashrc nvm install --lts # 得到 Node v24.21.0
2. 安装 pnpm
项目用的是 pnpm(有 pnpm-lock.yaml),不能用 npm。
bash
npm install -g pnpm
3. 克隆仓库
bash
cd /root git clone https://github.com/git938/my-admin.git my-dashboard cd my-dashboard
同样输入 Username 和 Token。
4. 安装依赖并构建
bash
pnpm install pnpm build
⚠️ 构建时的 TypeScript 错误
构建时报了一堆类型错误:
text
error TS2322: Property 'asChild' does not exist on type 'ButtonProps' error TS2322: Type '"icon-sm"' is not assignable to type '"default" | "sm" | ...'
原因:shadcn/ui 升级到 Base UI 版本后,废弃了 asChild,新增了 icon-sm 尺寸,但项目里的 Button 组件还是旧版。
临时方案:跳过类型检查,让构建通过。
bash
cat > next.config.ts << 'EOF'
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
typescript: {
ignoreBuildErrors: true,
},
eslint: {
ignoreDuringBuilds: true,
},
};
export default nextConfig;
EOF
pnpm build
构建成功后会看到所有路由:
text
Route (app) ┌ ○ / ├ ○ /project-management/dashboard-2 ├ ○ /project-management/projects └ ○ /project-management/tasks
5. 用 PM2 启动
第一坑:端口冲突
VPS 上已经有其他服务(photo-gem-map、tokyo-traffic)占用了 3000 端口,启动时报:
text
Error: listen EADDRINUSE: address already in use :::3000
解决:改用 3002 端口。直接修改 package.json:
bash
sed -i 's/"start": "next start"/"start": "next start -p 3002"/' package.json
第二坑:PM2 传参失败
不要用 pm2 start pnpm -- start -- -p 3002,-- -p 3002 会被误认为是项目目录,报:
text
Invalid project directory provided, no such directory: /root/my-dashboard/-p
正确做法:把端口写死在 package.json 里,然后:
bash
pm2 delete my-dashboard pm2 start pnpm --name "my-dashboard" -- start pm2 save pm2 startup
pm2 startup 会输出一行 sudo env PATH=...,原样复制执行,让 PM2 开机自启。
验证:
bash
pm2 status # my-dashboard online curl -I http://localhost:3002 # HTTP/1.1 200 OK
第三阶段:让公网能访问
关键发现:VPS 上有两层防火墙
VPS 上跑着很多服务(Docker、k3s、Nginx、RustDesk 等),网络结构比较复杂:
第一层:ufw(系统防火墙)
bash
ufw allow 3002/tcp ufw status
第二层:VPS 服务商的安全组
这是最容易忽略的。ufw 放行了,但服务商(如 Xserver VPS)的控制台里还有一层独立的防火墙。必须去服务商管理后台,把 3002 端口也加入放行列表。
我在这一步卡了很久:VPS 内部 curl http://162.43.92.249:3002 返回 200,但 MacBook 浏览器一直 ERR_CONNECTION_TIMED_OUT。最后在 Xserver VPS 控制台的防火墙设置里加了 3002 才通。
验证
在 MacBook 上访问:
text
http://162.43.92.249:3002/my-dashboard/project-management/dashboard-2
成功打开仪表盘。
为什么不用 Nginx 反向代理?
一开始想用 fbinjapan.xvps.jp/my-dashboard/ 这种方式,但遇到了问题:
- 端口 80 被 Docker 容器占用:VPS 上有多个 Nginx 实例(宿主机一个、Docker 容器内一个),配置互相干扰。
- Next.js 的 basePath 问题:要支持
/my-dashboard/前缀,需要在next.config.ts里设basePath,然后重新构建。 - 既有服务风险:修改 Nginx 配置可能影响现有的
fbinjapan.xvps.jp静态站点和/traffic/服务。
综合考虑,直接用 3002 端口暴露是最简单、最安全的方案。
最终成果
| 环节 | 状态 |
|---|---|
| 本地开发 | ✅ localhost:3001 |
| 推送到 GitHub | ✅ github.com/git938/my-admin |
| VPS 克隆并构建 | ✅ /root/my-dashboard |
| PM2 常驻运行 | ✅ 端口 3002 |
| ufw 放行 | ✅ 3002/tcp |
| 服务商安全组放行 | ✅ 3002/tcp |
| 公网访问 | ✅ http://162.43.92.249:3002/my-dashboard/project-management/dashboard-2 |
踩坑清单
- GitHub 仓库必须先创建 — 否则
Repository not found。 - Token 要有
repo权限 — 否则403 Write access not granted。 - Token 泄露要立即删除 —
ghp_...等同于密码。 - zsh 的
!历史展开 — 脚本用文件方式执行,或set +H。 - pnpm 项目用 pnpm 装依赖 — 不要混用 npm。
- Next.js 16 的类型错误 — 临时用
ignoreBuildErrors: true跳过。 - 端口冲突 — 用
ss -tlnp查清占用情况,换端口。 - PM2 传参要写进 package.json — 不要依赖
--后面的参数。 - 两层防火墙 — ufw 和云服务商安全组都要放行。
- VPS 资源紧张 — 部署前
df -h和free -h看看,磁盘 88%、内存 3.3/5.8GB 已用,注意清理。
后续优化方向
- 加 Basic 认证 — 现在端口直接暴露,知道 URL 的人都能访问。用 Nginx 加
auth_basic更安全。 - 限制访问来源 — 用 ufw 只允许特定 IP,或走 Tailscale(
http://100.99.4.120:3002)。 - HTTPS 化 — 绑定域名 + Let’s Encrypt,让流量加密。
- 配置 Jira API Token — 在 VPS 上创建
.env.production,填入JIRA_BASE_URL、JIRA_EMAIL、JIRA_API_TOKEN,然后重新构建。 - 自动部署 — 用 GitHub Actions,push 后自动在 VPS 上 pull + build + restart。
一句话总结
从 MacBook 到公网,核心是三件事:代码推上 GitHub、VPS 克隆构建并用 PM2 常驻、两层防火墙(ufw + 服务商安全组)都放行端口。 最容易踩的坑不是代码,而是云服务商的安全组和端口冲突。
http://162.43.92.249:3002/my-dashboard/project-management/dashboard-2
