从 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/ 这种方式,但遇到了问题:

  1. 端口 80 被 Docker 容器占用:VPS 上有多个 Nginx 实例(宿主机一个、Docker 容器内一个),配置互相干扰。
  2. Next.js 的 basePath 问题:要支持 /my-dashboard/ 前缀,需要在 next.config.ts 里设 basePath,然后重新构建。
  3. 既有服务风险:修改 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

踩坑清单

  1. GitHub 仓库必须先创建 — 否则 Repository not found。
  2. Token 要有 repo 权限 — 否则 403 Write access not granted。
  3. Token 泄露要立即删除 — ghp_... 等同于密码。
  4. zsh 的 ! 历史展开 — 脚本用文件方式执行,或 set +H。
  5. pnpm 项目用 pnpm 装依赖 — 不要混用 npm。
  6. Next.js 16 的类型错误 — 临时用 ignoreBuildErrors: true 跳过。
  7. 端口冲突 — 用 ss -tlnp 查清占用情况,换端口。
  8. PM2 传参要写进 package.json — 不要依赖 -- 后面的参数。
  9. 两层防火墙 — ufw 和云服务商安全组都要放行。
  10. VPS 资源紧张 — 部署前 df -h 和 free -h 看看,磁盘 88%、内存 3.3/5.8GB 已用,注意清理。

后续优化方向

  1. 加 Basic 认证 — 现在端口直接暴露,知道 URL 的人都能访问。用 Nginx 加 auth_basic 更安全。
  2. 限制访问来源 — 用 ufw 只允许特定 IP,或走 Tailscale(http://100.99.4.120:3002)。
  3. HTTPS 化 — 绑定域名 + Let’s Encrypt,让流量加密。
  4. 配置 Jira API Token — 在 VPS 上创建 .env.production,填入 JIRA_BASE_URL、JIRA_EMAIL、JIRA_API_TOKEN,然后重新构建。
  5. 自动部署 — 用 GitHub Actions,push 后自动在 VPS 上 pull + build + restart。

一句话总结

从 MacBook 到公网,核心是三件事:代码推上 GitHub、VPS 克隆构建并用 PM2 常驻、两层防火墙(ufw + 服务商安全组)都放行端口。 最容易踩的坑不是代码,而是云服务商的安全组和端口冲突。

http://162.43.92.249:3002/my-dashboard/project-management/dashboard-2

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注