以前部署前端静态项目,我一直是本地执行:

Shell
npm run build

然后把生成的 dist 压缩、上传到宝塔,再覆盖网站目录。

项目少的时候还能接受,项目一多,这套流程就很重复:

Text
改代码
→ 本地 build
→ 打包 dist
→ 登录宝塔
→ 上传
→ 解压
→ 覆盖旧文件
→ 检查网站

这次把部署方式改成了:

Text
本地 git push
→ GitHub
→ GitHub Webhook
→ 宝塔 WebHook
→ 服务器拉取最新代码
→ npm build
→ 生成 dist
→ rsync 覆盖网站目录
→ 上线

整个过程看起来不复杂,但实际配置时踩了不少坑:

  • GitHub 已经不支持账号密码拉取代码
  • Windows 和 Linux 文件名大小写规则不一致
  • 2G 服务器执行 Vite build 直接内存溢出
  • npm cipackage-lock.json 不同步失败
  • rsync --delete 删除宝塔 .user.ini 时返回 code 23
  • 宝塔 WebHook 直接跑长时间构建容易看起来“卡住”
  • GitHub Webhook URL、SSL、宝塔面板反向代理之间容易混淆

下面按实际部署过程完整记录一遍。

一、最终部署结构

这套方案适合所有能够构建成静态目录的前端项目,例如:

  • Vue
  • Vite
  • React
  • Angular
  • 普通静态站
  • 其他最终能生成 dist / build 目录的项目

我的目录规划是:

Text
/www/git/levi-blog-admin

保存完整源码:

Text
/www/git/levi-blog-admin
├── .git
├── src
├── public
├── package.json
├── package-lock.json
├── vite.config.js
└── dist

宝塔真正对外提供静态网站的目录仍然保持原样:

Text
/www/wwwroot/admin.leviqin.top

也就是说,源码和线上文件分开:

Text
/www/git
    ↓
源码、node_modules、Git 仓库
    ↓
npm run build
    ↓
dist
    ↓
rsync
    ↓
/www/wwwroot
    ↓
Nginx 对外访问

这样不会把 .gitsrcnode_modules 全部放进网站根目录。

二、给服务器配置 GitHub SSH 权限

一开始直接在服务器执行 HTTPS clone:

Shell
git clone https://github.com/LeviQin/levi-blog-admin.git

GitHub 会提示:

Text
Username for 'https://github.com':
Password for 'https://xxx@github.com':

remote: Invalid username or token.
Password authentication is not supported for Git operations.

GitHub 已经取消了 Git 操作的账号密码认证。服务器自动部署最适合用 SSH Deploy Key。

1. 服务器生成 SSH Key

执行:

Shell
ssh-keygen -t ed25519 -C "bt-levi-blog-admin"

保存位置使用默认值:

Text
/root/.ssh/id_ed25519

部署服务器通常不需要交互输入密码,所以 passphrase 可以直接回车留空。

生成后查看公钥:

Shell
cat /root/.ssh/id_ed25519.pub

正确格式类似:

Text
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIxxxxxxxxxxxx bt-levi-blog-admin

注意:

Text
id_ed25519.pub

才是公钥。

不要上传:

Text
id_ed25519

如果看到:

Text
-----BEGIN OPENSSH PRIVATE KEY-----

说明打开的是私钥,绝对不要提交到 GitHub,也不要公开。

2. 添加 GitHub Deploy Key

进入仓库:

Text
GitHub
→ Repository
→ Settings
→ Deploy keys
→ Add deploy key

填写:

Text
Title:
Tencent Cloud BT

Key 填刚才 cat ~/.ssh/id_ed25519.pub 得到的完整内容。

服务器只需要 clone / fetch / pull,所以 Allow write access 不需要勾选。

3. 多个仓库部署时,可以改用账号级 SSH Key

如果这台服务器只部署一个仓库,继续使用 Deploy Key 就够了。

但如果同一台宝塔服务器后面还要部署多个自己账号下的 GitHub 仓库,例如:

Text
levi-blog-admin
levi-blog
其他前端项目
其他 Node 项目

每个仓库都单独添加 Deploy Key 会比较麻烦。

这种情况下,可以给服务器单独生成一把 GitHub 账号级 SSH Key

账号级 SSH Key 配置在 GitHub 个人账号中,而不是某个仓库中。

两种方式的区别可以简单理解为:

方式 配置位置 权限范围 适合场景
Deploy Key Repository → Settings → Deploy keys 单个仓库 单项目、权限隔离要求高
账号级 SSH Key GitHub → Settings → SSH and GPG keys 当前账号有权限访问的仓库 一台服务器部署多个自己的项目

如果对权限隔离要求比较高,还是推荐一仓库一把 Deploy Key。

如果服务器本身就是自己的固定部署机,并且需要维护很多个人仓库,账号级 SSH Key 会方便很多。

3.1 生成一把专门的账号级 SSH Key

为了避免和前面的 Deploy Key 混在一起,建议重新生成一把单独的 Key:

Shell
ssh-keygen -t ed25519 \
  -C "tencent-bt-github-account" \
  -f /root/.ssh/id_ed25519_github

用于自动部署时,passphrase 可以直接回车留空。

生成完成后会得到:

Text
/root/.ssh/id_ed25519_github
/root/.ssh/id_ed25519_github.pub

其中 id_ed25519_github 是私钥,不要公开;id_ed25519_github.pub 是公钥,可以添加到 GitHub。

查看公钥:

Shell
cat /root/.ssh/id_ed25519_github.pub

输出类似:

Text
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIxxxxxxxxxxxx tencent-bt-github-account

复制整行。

3.2 添加到 GitHub 个人账号

进入 GitHub:

Text
右上角头像
→ Settings
→ SSH and GPG keys
→ New SSH key

这里和 Deploy Key 最大的区别是:

Text
Deploy Key:
Repository → Settings → Deploy keys

账号级 SSH Key:
个人账号 → Settings → SSH and GPG keys

填写:

Text
Title:
Tencent Cloud BT Server

Key type:
Authentication Key

Key 粘贴刚才 id_ed25519_github.pub 的完整内容,然后保存。

3.3 让服务器固定使用这把 Key

编辑:

Shell
vi /root/.ssh/config

加入:

SSHCONFIG
Host github.com
    HostName github.com
    User git
    IdentityFile /root/.ssh/id_ed25519_github
    IdentitiesOnly yes

设置权限:

Shell
chmod 700 /root/.ssh
chmod 600 /root/.ssh/config
chmod 600 /root/.ssh/id_ed25519_github
chmod 644 /root/.ssh/id_ed25519_github.pub

这样以后:

Shell
git clone git@github.com:你的账号/仓库A.git
git clone git@github.com:你的账号/仓库B.git

都会优先使用 /root/.ssh/id_ed25519_github

3.4 测试账号级 SSH 是否生效

执行:

Shell
ssh -T git@github.com

正常会看到类似:

Text
Hi your-github-name! You've successfully authenticated,
but GitHub does not provide shell access.

如果想确认某个私有仓库是否有访问权限,不需要真的 Clone,可以直接:

Shell
git ls-remote git@github.com:你的账号/仓库名.git

如果返回 HEADrefs/heads/main 等 refs,说明这台服务器已经可以访问这个仓库。

3.5 之前的 Deploy Key 要不要删除

不需要马上删除。可以先保留原来的 Deploy Key,确认账号级 SSH Key 已经能够正常访问多个仓库。全部测试通过以后,再根据需要删除旧 Deploy Key。

需要注意的是,账号级 SSH Key 的权限范围比单仓库 Deploy Key 大。如果服务器私钥泄露,影响范围也会更大。

因此可以这样选:

Text
单仓库生产服务器
→ 更推荐 Deploy Key

自己的多项目宝塔服务器
→ 可以使用账号级 SSH Key

两种方案没有绝对优劣,主要看服务器用途和权限隔离要求。

4. 测试 SSH

服务器执行:

Shell
ssh -T git@github.com

第一次会提示:

Text
The authenticity of host 'github.com' can't be established.
Are you sure you want to continue connecting?

输入:

Text
yes

成功后会出现类似:

Text
You've successfully authenticated, but GitHub does not provide shell access.

说明服务器已经可以读取 GitHub 仓库。

三、Clone 源码到单独目录

创建统一源码目录:

Shell
mkdir -p /www/git
cd /www/git

Clone:

Shell
git clone git@github.com:LeviQin/levi-blog-admin.git

进入项目:

Shell
cd /www/git/levi-blog-admin

确认 remote:

Shell
git remote -v

应该类似:

Text
origin  git@github.com:LeviQin/levi-blog-admin.git (fetch)
origin  git@github.com:LeviQin/levi-blog-admin.git (push)

四、先在服务器把项目手动构建通

不要一上来就配置 Webhook。先确保下面这条链路手动执行没有问题:

Text
GitHub
→ 服务器
→ 安装依赖
→ build
→ dist

这个项目同时存在过:

Text
package-lock.json
yarn.lock

但服务器最终统一使用 npm。

如果决定使用 npm,建议最终只保留:

Text
package.json
package-lock.json

避免 npm 和 Yarn 的锁文件长期混用。

安装依赖:

Shell
npm ci --include=dev --no-audit --no-fund

构建:

Shell
npm run build

五、Windows 正常,Linux build 报路径不存在

这个项目在 Windows 开发时一直正常,但放到 Linux 后连续出现:

Text
Could not resolve './../views/Dashboard/Index.vue'
Text
Could not resolve './../views/WebSites/NaviList.vue'
Text
Could not resolve './../views/M/Article/ArticleList.vue'
Text
Could not load src/components/Editor/Index.vue
ENOENT: no such file or directory

原因不是 Vite,而是 Windows 默认不严格区分文件名大小写,Linux 区分。

例如:

Text
Dashboard
dashboard

Windows 上可能都能访问,Linux 下是两个不同目录。

查看 Git 真正记录的文件名

不要只看 Windows 文件管理器。

执行:

Shell
git ls-files src/views

或者服务器:

Shell
git ls-tree -r HEAD --name-only

例如代码写的是:

JavaScript
import("@/views/Dashboard/Index.vue")

但 Git 实际记录:

Text
src/views/dashboard/Index.vue

Linux 构建一定失败。

Windows 修改纯大小写必须用 git mv

直接在资源管理器把:

Text
dashboard

改成:

Text
Dashboard

Git 可能完全检测不到。

正确方式:

Shell
git mv src/views/dashboard src/views/__tmp_dashboard
git mv src/views/__tmp_dashboard src/views/Dashboard

文件名也是一样。

例如:

Text
articleList.vue

改成:

Text
ArticleList.vue

执行:

Shell
git mv src/views/M/Article/articleList.vue src/views/M/Article/__tmp_article.vue
git mv src/views/M/Article/__tmp_article.vue src/views/M/Article/ArticleList.vue

然后:

Shell
git status
git add -A
git commit -m "fix: normalize path casing"
git push

六、GitHub 已经更新,服务器为什么还是旧代码

中间还遇到过一种情况:GitHub 已经是:

JavaScript
component: () => import("@/views/Websites/NaviList.vue")

服务器还是:

JavaScript
component: () => import("@/views/WebSites/NaviList.vue")

检查:

Shell
git rev-parse HEAD
git rev-parse origin/main

发现两个 SHA 不一致。

原因是:

Shell
git fetch origin

只更新:

Text
origin/main

不会自动更新当前工作区。

部署服务器更适合直接:

Shell
git fetch origin
git reset --hard origin/main

这样服务器永远以 GitHub main 为准。

部署目录不要手工改代码,也不需要 merge。

七、2G 服务器执行 Vite build 内存溢出

路径问题修完后,项目终于进入真正构建阶段,但又出现:

Text
FATAL ERROR: Ineffective mark-compacts near heap limit
Allocation failed - JavaScript heap out of memory

这个项目构建时大约有:

Text
2700+ modules
138 chunks

同时还开启了:

  • vite-plugin-imagemin
  • vite-plugin-compression
  • Element Plus
  • ECharts
  • 自动组件导入
  • 图标自动导入

2G 服务器压力很大。

八、给 Vite 增加低内存构建模式

不要单纯把 Node 内存无限调大。

2G 服务器如果直接把 Heap 拉满,Node 可能把整台机器内存吃光,宝塔、Nginx、SSH 都会受到影响。

最终给 Node Heap 控制在:

Text
1400MB

同时关闭服务器构建时不必要的插件。

vite.config.js

先定义:

JavaScript
const isLowMemoryBuild = process.env.LOW_MEMORY_BUILD === "true";

图片压缩插件:

JavaScript
!isLowMemoryBuild &&
  viteImagemin({
    gifsicle: {
      optimizationLevel: 3,
      interlaced: false,
    },
    optipng: {
      optimizationLevel: 5,
    },
    mozjpeg: {
      quality: 80,
      progressive: true,
    },
    svgo: {
      plugins: [
        {
          name: "removeViewBox",
          active: false,
        },
        {
          name: "removeEmptyAttrs",
          active: true,
        },
      ],
    },
  })

gzip 插件也可以在服务器低内存模式关闭:

JavaScript
!isLowMemoryBuild &&
  viteCompression({
    algorithm: "gzip",
    threshold: 10240,
  })

插件数组最后加:

JavaScript
.filter(Boolean)

build 配置建议

JavaScript
build: {
  outDir: "dist",
  emptyOutDir: true,
  minify: "esbuild",
  reportCompressedSize: false,
  cssCodeSplit: true,
  sourcemap: false,
  manifest: false,
  chunkSizeWarningLimit: 1500,
  assetsDir: "static/img/",
  rollupOptions: {
    output: {
      chunkFileNames: "static/js/[name].[hash].js",
      entryFileNames: "static/js/[name].[hash].js",
      assetFileNames: "static/[ext]/[name].[hash].[ext]",
    },
  },
}

其中比较重要的是:

JavaScript
reportCompressedSize: false

服务器没必要每次构建再计算 gzip 后的尺寸。

如果 Nginx 已经配置 gzip,也没有必要每次 Vite 构建再生成 .gz 文件。

图片同样没必要每次服务器部署都重新压缩。

九、让 npm run build 自动使用低内存模式

为了不用每次执行:

Shell
LOW_MEMORY_BUILD=true NODE_OPTIONS="--max-old-space-size=1400" npm run build

安装:

Shell
npm install -D cross-env

package.json

JSON
{
  "scripts": {
    "dev": "vite",
    "build": "cross-env LOW_MEMORY_BUILD=true NODE_OPTIONS=--max-old-space-size=1400 vite build",
    "build:full": "vite build"
  }
}

以后服务器仍然只需要:

Shell
npm run build

本地如果想执行完整构建:

Shell
npm run build:full

十、cross-env 安装后 npm ci 报错

服务器执行:

Shell
npm ci

出现:

Text
npm ERR! `npm ci` can only install packages when your package.json
and package-lock.json are in sync.

Missing: cross-env@10.1.0 from lock file

原因很简单:package.json 已经增加了 cross-env,但是 package-lock.json 没有同步。

在本地执行:

Shell
npm install -D cross-env

然后把两个文件一起提交:

Shell
git add package.json package-lock.json
git commit -m "build: sync cross-env dependency"
git push origin main

服务器再:

Shell
git fetch origin
git reset --hard origin/main

npm ci --include=dev --no-audit --no-fund

就可以正常安装。

这也是为什么生产部署推荐使用:

Shell
npm ci

而不是随意 npm install

npm ci 会强制要求锁文件和依赖声明完全一致。

十一、不要每次部署都 npm ci

2G 服务器每次:

Text
删 node_modules
→ 重新 npm ci
→ build

压力很大。

实际部署时可以比较 package-lock.json 是否变化。

只有依赖变化时才执行:

Shell
npm ci

普通修改 .vue.js.css 时直接使用现有 node_modules 构建。

十二、构建完成后先检查 dist

Vite 构建失败时,emptyOutDir: true 会先清空 dist

然后 public 文件可能已经复制进去,但 Rollup 后续失败。

这时 dist 可能只剩:

Text
images/
set-logo.svg
set-logo1.svg
set-logo2.svg

却没有:

Text
index.html
static/js/
static/css/

所以自动发布前一定检查:

Shell
test -f dist/index.html

这是整个自动部署脚本里非常重要的一道保护。

如果构建失败,绝对不能继续:

Shell
rsync --delete

否则可能把线上网站清空。

十三、使用 rsync 发布 dist

手动构建成功后,先备份现有网站:

Shell
mkdir -p /www/backup

tar -czf /www/backup/admin.leviqin.top-$(date +%Y%m%d-%H%M%S).tar.gz \
/www/wwwroot/admin.leviqin.top

同步:

Shell
rsync -av --delete \
  --exclude='.well-known/' \
  --exclude='.user.ini' \
  /www/git/levi-blog-admin/dist/ \
  /www/wwwroot/admin.leviqin.top/

注意 dist/ 后面的 / 很重要,表示复制 dist 里面的内容。

最终:

Text
/www/wwwroot/admin.leviqin.top/
├── index.html
├── static
├── images
└── ...

而不是:

Text
/www/wwwroot/admin.leviqin.top/dist/

十四、rsync code 23:宝塔 .user.ini 无法删除

第一次执行:

Shell
rsync -av --delete

最后出现:

Text
rsync error: some files/attrs were not transferred
(code 23)

查看日志:

Shell
grep -nE "rsync:|failed|denied|Operation not permitted|Permission denied" \
/www/logs/levi-blog-admin-deploy.log | tail -30

找到:

Text
rsync: [generator] delete_file: unlink(.user.ini)
failed: Operation not permitted (1)

原因是宝塔对 .user.ini 增加了保护属性。

dist 中没有这个文件,rsync --delete 想删除它,结果失败。

不要删除 .user.ini,直接排除:

Shell
--exclude='.user.ini'

最终:

Shell
rsync -av --delete \
  --exclude='.well-known/' \
  --exclude='.user.ini' \
  "$PROJECT_DIR/dist/" \
  "$WEB_DIR/"

不要用:

Shell
rsync ... || true

强行吞掉错误。真正有 JS 或 CSS 文件传输失败时,也会被误判成成功。

十五、最终部署脚本

创建:

Text
/www/scripts/deploy-levi-blog-admin.sh

内容:

Shell
#!/bin/bash

(
    flock -n 9 || {
        echo "[$(date)] 已有部署任务正在运行,本次退出"
        exit 1
    }

    set -e

    export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

    PROJECT_DIR="/www/git/levi-blog-admin"
    WEB_DIR="/www/wwwroot/admin.leviqin.top"

    echo ""
    echo "========================================"
    echo "Levi Blog Admin Deploy"
    echo "Start: $(date)"
    echo "User: $(whoami)"
    echo "========================================"

    cd "$PROJECT_DIR"

    echo "[1/6] 获取最新代码"

    OLD_LOCK=$(sha256sum package-lock.json 2>/dev/null | awk '{print $1}')

    git fetch origin
    git reset --hard origin/main

    NEW_LOCK=$(sha256sum package-lock.json 2>/dev/null | awk '{print $1}')

    echo "当前版本:$(git rev-parse --short HEAD)"

    echo "[2/6] 检查依赖"

    if [ ! -d "node_modules" ] || [ "$OLD_LOCK" != "$NEW_LOCK" ]; then

        echo "依赖发生变化,执行 npm ci"

        npm ci \
            --include=dev \
            --no-audit \
            --no-fund \
            --prefer-offline
    else
        echo "依赖未变化,跳过 npm ci"
    fi

    echo "[3/6] 清理旧构建"
    rm -rf "$PROJECT_DIR/dist"

    echo "[4/6] 构建"
    npm run build

    echo "[5/6] 检查构建产物"

    if [ ! -f "$PROJECT_DIR/dist/index.html" ]; then
        echo "ERROR: dist/index.html 不存在"
        exit 1
    fi

    echo "构建成功:"
    ls -lh "$PROJECT_DIR/dist/index.html"
    du -sh "$PROJECT_DIR/dist"

    echo "[6/6] 发布"

    rsync -av --delete \
        --exclude='.well-known/' \
        --exclude='.user.ini' \
        "$PROJECT_DIR/dist/" \
        "$WEB_DIR/"

    echo "========================================"
    echo "Deploy Success"
    echo "End: $(date)"
    echo "========================================"

) 9>/tmp/levi-blog-admin-deploy.lock

加执行权限:

Shell
chmod +x /www/scripts/deploy-levi-blog-admin.sh

先手动测试:

Shell
/www/scripts/deploy-levi-blog-admin.sh

看到:

Text
Deploy Success

再继续配置 WebHook。

十六、为什么脚本里要用 flock

服务器只有 2G。

如果连续 push 两次:

Text
04:00 push
04:01 push

很可能出现两个 vite build 同时运行。

结果就是:

Text
内存耗尽
→ 宝塔失联
→ SSH 卡顿
→ OOM

所以使用:

Shell
flock

确保同一时间只存在一个部署任务。

第二次触发时:

Text
已有部署任务正在运行,本次退出

比把整台服务器拖死安全得多。

十七、宝塔 WebHook 不直接运行完整构建

最开始把完整构建脚本直接放进宝塔 WebHook。

日志停在:

Text
[3/5] 构建
[4/5] 检查构建产物

但没有继续。

对于构建时间比较长的项目,不建议让 HTTP Webhook 请求一直等待:

Text
git
→ npm
→ Vite
→ rsync

更稳定的方式是:

Text
WebHook
→ 启动后台脚本
→ 立即返回

宝塔 WebHook 中只放:

Shell
#!/bin/bash

nohup /bin/bash /www/scripts/deploy-levi-blog-admin.sh \
  >> /www/logs/levi-blog-admin-deploy.log 2>&1 &

echo "部署任务已启动,PID: $!"

准备日志目录:

Shell
mkdir -p /www/logs

这样 WebHook 请求很快就结束,真正部署过程在后台继续。

十八、查看部署日志

实时查看:

Shell
tail -f /www/logs/levi-blog-admin-deploy.log

正常会看到:

Text
Levi Blog Admin Deploy
Start: ...

[1/6] 获取最新代码
当前版本:xxxxxxx

[2/6] 检查依赖
依赖未变化,跳过 npm ci

[3/6] 清理旧构建

[4/6] 构建
vite ...
✓ xxxx modules transformed
✓ built in xx.xx s

[5/6] 检查构建产物
构建成功

[6/6] 发布

Deploy Success

退出:

Text
Ctrl + C

只会关闭 tail,不会停止部署。

查看最近日志:

Shell
tail -100 /www/logs/levi-blog-admin-deploy.log

十九、增加 WebHook 触发日志

如果 GitHub push 后没有部署,很难第一时间判断:

Text
GitHub 没有触发?
宝塔没收到?
脚本没启动?

可以给宝塔 WebHook 再增加一份非常简单的触发日志:

Shell
#!/bin/bash

echo "========================================" >> /www/logs/webhook-trigger.log
echo "Webhook triggered: $(date)" >> /www/logs/webhook-trigger.log
echo "User: $(whoami)" >> /www/logs/webhook-trigger.log

nohup /bin/bash /www/scripts/deploy-levi-blog-admin.sh \
  >> /www/logs/levi-blog-admin-deploy.log 2>&1 &

echo "PID: $!" >> /www/logs/webhook-trigger.log

echo "部署任务已启动,PID: $!"

以后:

Shell
tail -20 /www/logs/webhook-trigger.log

只要有新记录,就说明:

Text
GitHub
→ 宝塔 WebHook

至少已经成功触发。

二十、配置 GitHub Webhook

宝塔 WebHook 插件会生成一个类似:

Text
https://example.com/hook?access_key=xxxxxxxx

的地址。

GitHub:

Text
Repository
→ Settings
→ Webhooks
→ Add webhook

配置:

Text
Payload URL:
宝塔生成的完整 WebHook 地址
Text
Content type:
application/json

事件:

Text
Just the push event

启用:

Text
Active

如果没有自行实现 GitHub X-Hub-Signature-256 校验,Secret 可以留空。宝塔本身已经使用 access_key 作为触发凭证。

二十一、GitHub Webhook SSL 验证

GitHub Webhook 默认会验证 HTTPS 证书。

正式环境建议保持:

Text
Enable SSL verification

如果 GitHub Recent deliveries 出现 SSL 错误,不要长期关闭 SSL 验证。

应该检查:

Shell
openssl s_client \
  -connect baota.example.com:443 \
  -servername baota.example.com \
  </dev/null 2>/dev/null \
| openssl x509 \
  -noout \
  -subject \
  -issuer \
  -dates \
  -ext subjectAltName

Subject Alternative Name 中必须包含:

Text
DNS:baota.example.com

同时检查 Nginx 真正使用的证书:

Shell
nginx -T 2>/dev/null \
| grep -A 20 -B 5 "server_name baota.example.com"

确认:

Text
ssl_certificate
ssl_certificate_key

指向正确证书。

二十二、access_key 不要公开

宝塔 WebHook URL 中的:

Text
access_key

相当于部署触发凭证。

不要放在:

  • GitHub README
  • 博客截图
  • 公开日志
  • 前端代码
  • 公开仓库

如果不小心暴露,直接重新生成。

二十三、正式使用后的操作

配置完成以后,本地日常开发只需要:

Shell
git add .
git commit -m "feat: update"
git push origin main

剩下全部自动完成:

Text
git push
↓
GitHub main
↓
GitHub Webhook
↓
宝塔 WebHook
↓
nohup 启动后台部署
↓
git fetch origin
↓
git reset --hard origin/main
↓
检查 package-lock
↓
必要时 npm ci
↓
npm run build
↓
检查 dist/index.html
↓
rsync
↓
网站更新

二十四、排错顺序

自动部署失败后,不建议一上来就改配置。按链路从前往后排查。

1. GitHub 是否真的有新 commit

服务器:

Shell
git rev-parse HEAD
git ls-remote origin refs/heads/main

两个 SHA 不一样,说明服务器没有部署最新版本。

2. GitHub 有没有触发 Webhook

GitHub:

Text
Settings
→ Webhooks
→ Recent deliveries

常见状态:

Text
200 / 204

说明 GitHub 已经成功请求。

Text
404

Webhook URL 不正确。

Text
403

可能被安全规则或权限拦截。

Text
Timeout

GitHub 无法访问宝塔。

Text
SSL error

证书有问题。

3. 宝塔有没有收到请求

Shell
tail -20 /www/logs/webhook-trigger.log

没有新记录,说明 GitHub → 宝塔 这一段有问题。

4. 部署脚本有没有启动

Shell
ps -ef | grep deploy-levi-blog-admin | grep -v grep

5. 查看真实部署日志

Shell
tail -100 /www/logs/levi-blog-admin-deploy.log

6. Git 是否真正同步

Shell
git fetch origin

git rev-parse HEAD
git rev-parse origin/main

不一致:

Shell
git reset --hard origin/main

7. dist 是否完整

Shell
test -f dist/index.html && echo "dist 正常" || echo "dist 异常"

8. rsync 是否失败

Shell
grep -nE \
"rsync:|failed|denied|Operation not permitted|Permission denied" \
/www/logs/levi-blog-admin-deploy.log \
| tail -30

二十五、几个比较关键的经验

源码和网站目录分开

不要:

Text
/www/wwwroot/example.com
├── src
├── node_modules
├── .git
└── dist

更推荐:

Text
/www/git/project

负责源码和构建。

Text
/www/wwwroot/example.com

只放最终静态文件。

部署服务器不要改源码

服务器应该永远:

Shell
git fetch origin
git reset --hard origin/main

GitHub main 才是唯一代码源。

构建成功不等于 dist 一定完整

发布前必须:

Shell
test -f dist/index.html

rsync --delete 一定配 exclude

宝塔目录至少考虑:

Text
.user.ini
.well-known/

低配置服务器不要做没必要的构建工作

2G 服务器上:

  • 图片压缩尽量关闭
  • gzip 构建尽量交给 Nginx
  • sourcemap 关闭
  • compressed size 关闭
  • Node Heap 留出系统运行空间
  • npm ci 只在依赖变化时执行

长时间部署不要阻塞 WebHook 请求

Webhook:

Text
收到请求
→ nohup
→ 立即返回

真正构建放后台。

一定防止重复构建

Shell
flock

非常有用。

低配置服务器同时跑两个 Vite build,很容易直接把机器拖死。

结尾

以前手动上传 dist 时,部署这件事看起来很简单。

真正改成自动部署以后才会发现,它其实涉及:

Text
Git
SSH
GitHub
Linux
Node
npm
Vite
Nginx
rsync
宝塔
Webhook
SSL

每一层都可能成为问题来源。

但把链路拆开以后就容易很多:

Text
GitHub 能不能拉?
↓
服务器能不能 build?
↓
dist 对不对?
↓
rsync 能不能发布?
↓
宝塔 WebHook 能不能启动脚本?
↓
GitHub Webhook 能不能请求宝塔?

每一步单独测试通过,再把它们串起来。

最终得到的效果就是:

Shell
git push

代码提交完成后,服务器自动拉取、构建并上线。

对需要经常更新的前端项目来说,这套流程比手工上传 dist 省事很多,也更不容易因为漏删旧文件、上传错目录或者忘记构建而出问题。

阅读进度 0%