本地npm run dev跑得再顺,一到服务器上照样会翻车——购物车能加不能删、商品详情页刷新就 404、接口一会儿 502 一会儿跨域报错。Vue 电商项目这类带路由、带接口、带一堆图片资源的前端工程,部署从来不是把dist文件夹拖上去就完事,它牵扯构建产物的路径规则、history 路由的 fallback、开发代理和生产环境的边界、静态资源缓存策略,还有目录权限和进程内存这些平时压根碰不到的东西。下面这套打法是我自己反复用过的:本地 Git 提交,推送到云主机上的裸仓库,钩子自动检出到站点目录,Nginx 直接对外服务。流程熟了一次 30 分钟能走完,第一次做预留一两个小时也正常,跑通之后每次更新就一条git push。文中标注常见报错表现、排查顺序和修复方法,基本覆盖 90% 的新手场景。
1. 用 Git 推送和云主机拉取的原因
1.1 文件拖拽、FTP、CI 三条路各自在哪一步卡住
先说为什么不用最常见的做法。用可视化工具把dist目录拖到服务器的做法,问题不在“慢”,而在不可追溯:改了哪个版本、谁传的、几点传的,全靠脑子记。电商项目经常要热修一个价格显示或者优惠券逻辑,传错一版包上去,用户看到的是昨天数据,回头连回滚的目标都找不到。FTP 稍好一点,至少能选文件覆盖,但几十个带 hash 的 chunk 文件手动挑选,漏一个就是白屏。
CI 流水线当然是最规范的,但它要求你先有仓库、有 Runner、有构建缓存,配置成本放在第一次部署上并不划算。对于个人项目、小团队 demo、临时验收环境,最短路径反而是 Git 直连:提交即部署,回滚就是git reset再推一次。
Git 直连能成立的关键在于一件事——Git 本身就是一个分发工具,你把源码推到云主机,云主机上有完整的构建能力,剩下的只是“检出到哪个目录”这个问题。
1.2 Git 直连方式的适用范围
说清楚它的边界比吹它有用。这套方案适合单机、单实例、更新频率不高、没有多位开发者同时发布的项目。一旦出现这些信号,就该升级:
- 两台以上云主机需要同时更新,靠手动拉会不一致;
- 需要灰度、需要先跑测试再上线;
- 每次构建耗时超过 5 分钟,推送后长时间占着会话;
- 有人在服务器上直接改过文件,
git pull开始冲突。
我一般判断标准很土:如果部署过程需要我在服务器上敲超过三条命令,就说明该上自动化的钩子了;如果钩子之外还需要人工确认,说明该上真正的流水线了。这篇讲的钩子方案正好卡在中间这个甜点上。
1.3 30 分钟的时间分配
把 30 分钟拆开看,你会知道该在哪一步提前准备:
| 环节 | 熟练耗时 | 首次耗时 | 最容易卡住的地方 |
|---|---|---|---|
| 云主机开好、安全组放行 | 3 分钟 | 10 分钟 | 只开了 22 没开 80 |
| 装 Node + Nginx | 5 分钟 | 15 分钟 | 系统源自带 Node 版本太低 |
| 本地构建产物验证 | 5 分钟 | 20 分钟 | 路径、环境变量写错 |
| Nginx 配置与 reload | 5 分钟 | 20 分钟 | try_files 缺失导致刷新 404 |
| 裸仓库 + 钩子 | 8 分钟 | 25 分钟 | 钩子没有执行权限 |
| 联调接口 | 4 分钟 | 30 分钟 | 代理只在 dev 生效 |
首次慢不是能力问题,是这些坑没踩过。表里右列那几项,后面第 6 节会一个个拆开讲。
2. 云主机侧要准备的四个前置条件
2.1 规格与系统选择的实际影响
Vue 项目在云主机上要做的事只有两件:装依赖、打包。打包是内存密集型操作,npm run build峰值内存能吃到 800MB 到 1.5GB,取决于项目里引了多少第三方库。电商项目往往还要引 UI 组件库、图表库、地图 SDK,内存峰值只会更高。
1 核 1G 的入门配置在装依赖阶段就可能被系统 OOM 杀掉进程,报错表现为Killed或者npm ERR! signal SIGKILL,很多人误以为是包的问题。我的经验底线是2 核 2G 起步,1G 内存的机器必须配 swap,否则构建成功率靠运气。系统方面选一个长期支持的 Linux 发行版即可,本文命令以 Ubuntu 22.04 LTS 为准,其他发行版把apt换成对应的包管理器。
2.2 安全组与端口的放行顺序
云主机的端口能不能通,是两层控制:云平台侧的安全组策略 + 系统侧的防火墙。新手最常见的失败是只改了其中一个,然后对着浏览器转圈怀疑人生。
正确的顺序是:先在控制台安全组里放行22(管理)、80(HTTP),需要 HTTPS 再放行443;系统侧的ufw或firewalld保持默认关闭或者同步放行。不要图省事直接放开全部端口,前端站点对外只需要 80 和 443。
注意:改完安全组立刻用
curl -I http://你的公网IP在本地测一下连通性,别等 Nginx 配完了才发现是网络层不通,那时候你会去怀疑配置文件,方向全错。
2.3 SSH 密钥登录的实现方式
用密码登录云主机做频繁部署,每次都要输密码,钩子里也没法自动化。正确的做法是本地生成一对密钥,把公钥追加到云主机的~/.ssh/authorized_keys:
# 本地执行,一路回车即可,不需要设置密码短语 ssh-keygen -t ed25519 -C "deploy-key" # 把公钥内容推上去(首次仍需密码) ssh-copy-id -i ~/.ssh/id_ed25519.pub deploy@你的公网IP推上去之后先别急着关掉密码登录,新开一个终端验证ssh deploy@你的公网IP能免密进来,确认无误再去/etc/ssh/sshd_config里设PasswordAuthentication no。这个顺序很重要,配错了密钥又关了密码,只能去控制台走 VNC 救援。
部署用的账号我建议单独建一个,比如deploy,不要直接用 root。原因很实际:Nginx 站点目录、Git 仓库目录的属主需要一致,用 root 建的文件后面 Nginx worker 读不到,就是 403。
2.4 Node 与 Nginx 的安装方式
系统源自带的 Node 版本通常旧到没法跑现代 Vue 项目的构建链。用版本管理器装,版本可切换、升级不动系统包:
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - sudo apt install -y nodejs nginx git node -v && npm -v && nginx -v选 18.x 还是 20.x,看项目package.json里的engines和锁定文件版本。上线机器和本地开发机的 Node 大版本必须一致,这一条能省掉后面一大半“本地好使线上不行”的玄学问题,因为不同大版本对依赖的解析、ESM 处理、构建产物 hash 都可能不同。
Nginx 装完默认会起一个欢迎页占着 80 端口,后面配站点时要先把/etc/nginx/sites-enabled/default干掉或者直接覆盖,不然你还会看到那个蓝色欢迎页,然后怀疑 reload 没生效。
3. Vue 电商项目的可部署化改造
3.1 dist 目录的内容与验证方式
npm run build的产物结构是有规律的,理解它才知道出问题该看哪:
dist/ ├── index.html ├── assets/ │ ├── index-a1b2c3d4.js │ ├── vendor-e5f6g7h8.js │ ├── index-i9j0k1l2.css │ └── logo-m3n4o5p6.png └── favicon.ico文件名里那串 hash 是内容指纹,内容不变 hash 不变,这是缓存策略的基础。关键点:index.html里引用资源的路径前缀,决定了你部署到子目录还是根目录。如果站点部署在根路径,base保持/即可;如果挂在https://xxx.com/shop/这种子路径下,base必须改成/shop/,否则所有 JS/CSS 都会 404,页面白屏。
验证产物最省事的办法是在本地先模拟一次线上环境:
npm run build npx serve -s dist -l 5000-s参数会把找不到的路径回退到index.html,正好模拟 Nginx 的try_files。浏览器打开localhost:5000,点几个商品详情页再刷新,如果本地这步就 404,那问题在项目配置,跟你服务器没关系。
3.2 开发代理与生产接口地址的区别
这是电商项目最容易翻车的一处。开发时我们在vite.config.js里配了 proxy,/api的请求被转发到本地 mock 服务或者测试后端,跨域问题被代理顺手解决了。很多人在上线时把所有接口地址硬编码成http://localhost:8080,一上线全部失败。
正确的做法是把接口前缀抽成环境变量,让开发和生产的取值分离:
# .env.development VITE_API_BASE_URL=/api # .env.production VITE_API_BASE_URL=/api两个环境都写/api,看似一样,实际含义完全不同:开发时/api由 Vite 的 proxy 拦截转发;生产时/api是一个同源相对路径,由 Nginx 转发给后端。同源这两个字是重点,同源就不存在跨域,也就不需要后端配一堆 CORS 头。
axios 里这样取:
import axios from 'axios' const request = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 10000 }) request.interceptors.response.use( res => res.data, err => { if (err.response?.status === 401) { // 电商项目里登录态失效很常见,统一跳登录 window.location.href = '/login' } return Promise.reject(err) } )注意:环境变量名必须以
VITE_开头才会被打进客户端包,写成API_BASE_URL是拿不到的,构建不会被报错,运行时是undefined,表现为所有请求打到当前域名根路径下,接口全 404。
3.3 history 路由模式与 base 路径设置
电商项目的路由结构通常是多层的:/首页、/category/:id分类、/product/:id详情、/cart购物车、/order/confirm结算。这种结构用 hash 模式(带#)虽然不会 404,但地址丑,而且对分享、统计不友好,实际项目基本都开 history 模式。
history 模式的代价是:用户在/product/123上按 F5,浏览器真的会向服务器请求/product/123这个路径,而服务器上并没有这个文件或目录,自然 404。这不是 Vue 的问题,是 HTTP 服务的工作方式,解决办法只有一个——让 Nginx 把所有找不到的路径都交给index.html,由前端路由去解析。这部分配置在第 4.2 节展开。
3.4 打包体积与首屏加载的取舍
电商项目天然重:商品图、轮播、SKU 选择、购物车计算、地址选择,加上 UI 库动辄打包出 3MB 以上。云主机带宽通常只有 1M~5M,一个 3MB 的包在弱网下首屏能等十几秒。
三个我能立刻做的动作:
- 手动分包,把
vue/vue-router/pinia和 UI 库拆成独立 chunk,配合长期缓存,改业务代码时用户不需要重新下载这些; - UI 库按需引入,别
import ElementPlus from 'element-plus'全量进; - 图片走 CDN 或对象存储,不要把几百张商品图塞进
dist/assets。
分包的配置写在vite.config.js里:
import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import path from 'path' export default defineConfig({ base: '/', plugins: [vue()], resolve: { alias: { '@': path.resolve(__dirname, 'src') } }, build: { outDir: 'dist', assetsDir: 'assets', sourcemap: false, chunkSizeWarningLimit: 800, rollupOptions: { output: { manualChunks: { vendor: ['vue', 'vue-router', 'pinia'], ui: ['element-plus'] } } } }, server: { proxy: { '/api': { target: 'http://127.0.0.1:8080', changeOrigin: true, rewrite: p => p.replace(/^\/api/, '') } } } })sourcemap: false是我个人偏好,线上不需要 map 文件,打包更快、体积更小、源码不外泄;如果团队需要线上排错,可以单独留一份 map 不放公网。
4. 云主机上的 Nginx 配置
4.1 server 块结构与 root 指向
Nginx 的站点配置我习惯单独放一个文件在/etc/nginx/sites-available/下,再软链到sites-enabled,这样多站点管理和回滚都清晰。核心结构如下:
server { listen 80; server_name shop.example.com; root /var/www/shop/dist; index index.html; access_log /var/log/nginx/shop.access.log; error_log /var/log/nginx/shop.error.log; location / { try_files $uri $uri/ /index.html; } location /assets/ { expires 30d; add_header Cache-Control "public, immutable"; access_log off; } location = /index.html { add_header Cache-Control "no-cache, no-store, must-revalidate"; } location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_connect_timeout 10s; proxy_read_timeout 60s; } }root指向的是dist目录本身,不是项目根目录。这一条写错了,表现是访问首页返回 403 或者目录列表,因为 Nginx 在项目根目录下找index.html,那里只有package.json和src。
4.2 刷新时 404 的成因与 try_files 作用
try_files $uri $uri/ /index.html;这行的执行逻辑是:先找有没有同名文件,没有就找有没有同名目录,都没有就内部重定向到index.html。前端路由接管后,/product/123会被 Vue Router 解析出商品详情组件。
有几种写法会导致这条失效:
- 把
try_files写在location /里但外层又配了别的try_files覆盖; - 用了
location ~ \.html$的正则匹配,把index.html的请求劫持走了; index.html的root和location的root不一致。
排查方式很直接,看 error_log 里报的是open() "/var/www/shop/product/123" failed(说明 try_files 没生效)还是directory index of ... is forbidden(说明路径指到了目录)。
4.3 缓存策略中 index.html 的特殊处理
缓存这块有个反直觉的点:最不能被缓存的恰恰是index.html。
原因在于 hash 文件名的机制——index.html里记录着当前版本引用的是index-a1b2c3d4.js。如果index.html被浏览器缓存了 7 天,你发版后用户访问的还是旧 HTML,它去请求那个旧的 hash 文件,而服务器上已经删了,结果就是白屏或者一堆 404。反过来,assets/下带 hash 的文件可以缓存一年,因为内容一变文件名就变,绝不会冲突。
所以配置上必须区分:assets/长缓存 +immutable,index.html强制no-cache。上面那段配置就是按这个思路写的。改完之后用curl -I http://你的域名/index.html看Cache-Control响应头,确认生效。
4.4 反向代理接口时的关键 header
proxy_pass后面那个斜杠很容易被忽略。proxy_pass http://127.0.0.1:8080/;带了尾斜杠,请求/api/product/list会被转发成http://127.0.0.1:8080/product/list,/api前缀被吃掉;不带尾斜杠则是http://127.0.0.1:8080/api/product/list,前缀保留。后端路由是按哪个写的,就得配哪种,这在联调时经常是 404 的来源。
几个 header 的作用:Host让后端能做基于域名的处理;X-Real-IP和X-Forwarded-For让后端日志和限流能拿到真实客户端 IP,否则全是127.0.0.1;X-Forwarded-Proto告诉后端原始请求是 http 还是 https,生成回调地址时用得上。
超时也要设。电商接口里商品列表、订单查询可能偏慢,proxy_read_timeout默认 60s 还行,但proxy_connect_timeout默认也是 60s,后端挂了的话用户要等一分钟才看到错误页,改成 10s 体验好得多。
5. Git 部署链路的实现方式
5.1 裸仓库与钩子的分工
裸仓库(bare repository)是没有工作区的仓库,它只存.git里的东西,不能cd进去看文件。它的作用是充当“中转站”:本地git push到这里,触发的钩子再把代码检出到真正的站点目录。
这种结构的好处是推送和构建解耦。站点目录里的东西可以被构建覆盖、可以删了重来,而仓库本身永远是干净的版本记录。回滚的时候,直接在本地git reset --hard 上一版本再强推,钩子自动重新构建,服务器上不需要任何手工操作。
服务器上建仓库:
sudo mkdir -p /srv/repos /var/www/shop sudo git init --bare /srv/repos/shop.git sudo chown -R deploy:deploy /srv/repos/shop.git /var/www/shop--bare决定了它是裸仓库,目录名习惯以.git结尾。属主改成deploy是为了后面 SSH 推送时不因权限被拒。
5.2 post-receive 钩子的编写要点
钩子放在/srv/repos/shop.git/hooks/post-receive,推送完成后自动执行:
#!/bin/bash set -e TARGET=/var/www/shop GIT_DIR=/srv/repos/shop.git echo "===== 开始部署 $(date '+%F %T') =====" # 强制检出到站点目录,覆盖本地改动 git --work-tree=$TARGET --git-dir=$GIT_DIR checkout -f cd $TARGET # 用锁定文件安装,保证与本地依赖版本一致 npm ci # 生产构建 npm run build echo "===== 部署完成 $(date '+%F %T') ====="几个关键细节,每一个都对应一个真实的报错:
set -e必须有,否则npm ci失败了脚本还会继续往下跑,最后你看到的是“部署完成”但页面是旧版;checkout -f里的-f必须有,不加的话第二次部署会因为本地文件已存在而拒绝覆盖,报would be overwritten by checkout;--work-tree和--git-dir两个参数必须成对出现,少一个就会在当前目录乱检出;- 脚本必须
chmod +x,否则 Git 静默跳过,你 push 成功了却什么都没发生,这是最气人的一种。
sudo chmod +x /srv/repos/shop.git/hooks/post-receive sudo chown deploy:deploy /srv/repos/shop.git/hooks/post-receive5.3 部署流程与私有仓库的处理
如果你不想建裸仓库,还有个更简单的方案:云主机上直接git clone私有仓库,每次部署git pull && npm run build。适合个人项目,省掉钩子脚本。区别是这需要云主机有拉取权限,得配 deploy key(只读部署密钥),在代码托管平台的仓库设置里添加公钥。
两种方案的取舍:
| 对比项 | 裸仓库 + 钩子 | clone + pull |
|---|---|---|
| 推送即部署 | 是 | 否,需登录服务器 |
| 配置复杂度 | 中,要写钩子 | 低,一条命令 |
| 服务器需要有仓库读权限 | 否 | 是 |
| 多人协作 | 天然支持 | 需要每人都有登录权限 |
| 回滚操作 | 本地 reset 后强推 | 登录服务器 reset |
我自己的项目基本都用钩子方案,主要是懒得每次登录。团队项目如果有多个开发,钩子方案的推送即部署会带来一个隐患——谁推谁上线,没有评审环节,这点必须提前说清楚,要么加保护分支,要么老老实实用流水线。
5.4 首次部署与后续更新的命令序列
本地关联远端并首次推送:
git remote add deploy ssh://deploy@你的公网IP/srv/repos/shop.git git add -A git commit -m "feat: 首次部署" git push deploy main推送之后,控制台会实时打印钩子的输出,npm ci和npm run build的日志都在里面,构建失败能立刻看到。这是钩子方案比“登服务器看日志”舒服的地方。
后续更新就是三件套:
git add -A && git commit -m "fix: 修复购物车数量同步" git push deploy main推送完刷新页面。如果页面没变化,先看钩子日志有没有跑完,再看浏览器 Network 里index.html的响应是200还是304。第 6.6 节专门讲这个。
6. 八个高频报错的定位与修复
6.1 白屏但 Network 全 200
现象:页面一片白,控制台没有红色报错,Network 里index.html、JS、CSS 都是 200。
这基本可以断定是base配错了。JS 加载成功但执行时找不到挂载点或者路由匹配失败。打开index.html源码看<script src="...">的路径,如果是/assets/index-xxx.js而你实际部署在/shop/子目录,那就是 404 但被服务端 fallback 到index.html返回了 200(HTML 内容),浏览器把 HTML 当 JS 解析,报Unexpected token '<'。
修复:vite.config.js里base改成实际子路径,重新构建。
6.2 首页能开,详情页刷新就 404
这是本篇文章出现频率最高的问题,也是最容易被误判为“后端问题”的。判断依据很简单:站内点击跳转正常,只有按 F5 或者直接粘贴 URL 才 404 —— 说明前端路由工作正常,是服务端找不到对应路径。
修复就是第 4.2 节的try_files。如果已经配了还不行,检查是否有location ~ \.html$之类的正则块抢在前面,正则location的优先级高于普通前缀匹配,会绕过你的 fallback。
6.3 接口 502 与跨域错误的区分
502 的含义是 Nginx 连不上上游。三个方向查:
- 后端服务真的没起,
curl http://127.0.0.1:8080/health在本机测; proxy_pass的端口写错,或者后端只监听了127.0.0.1而 Nginx 在容器里跑;- 后端启动慢,Nginx 先起来了,改
proxy_next_upstream或者加重试。
跨域报错则是另一种表现:控制台明确写blocked by CORS policy。如果生产环境还见到这个,说明你的请求根本没走 Nginx 代理,而是打了绝对地址,比如axios的baseURL被硬编码成了https://api.xxx.com。回到 3.2 节,把地址改成同源的/api。
6.4 Permission denied (publickey) 的三种成因
推送时报这个,从来不是“密钥不对”这么笼统:
| 表现 | 原因 | 修复 |
|---|---|---|
| 一直要密码,输完仍拒绝 | 公钥没追加到目标用户 | 检查~/.ssh/authorized_keys的属主和权限 600 |
提示bad permissions | .ssh目录 755 或私钥 644 | chmod 700 ~/.ssh && chmod 600 ~/.ssh/id_* |
| 只有某个仓库推不了 | 远端 URL 里的用户名错了 | ssh://deploy@IP/...里的用户名要和建仓库的用户一致 |
还有一种是git push成功但钩子里的npm命令找不到,报npm: command not found。原因是钩子执行时的 PATH 和你登录 shell 的 PATH 不同。修法是在钩子开头加一行export PATH=$PATH:/usr/local/bin:/usr/bin,或者直接用node的绝对路径。
6.5 构建进程被系统杀掉
现象:钩子日志走到npm run build突然中断,没有明确错误,或者只有Killed。
这就是内存不够。确认方式看系统日志:
dmesg -T | grep -i "out of memory" free -h修复两条路:加 swap,或者放宽 Node 的堆上限。
# 加 2G swap sudo fallocate -l 2G /swapfile sudo chmod 600 /swapfile sudo mkswap /swapfile sudo swapon /swapfile echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab # 或者放宽 Node 堆内存上限 export NODE_OPTIONS="--max-old-space-size=1536"swap 会让构建变慢(磁盘 IO),但至少能跑完。长期看还是升配置更省心。
6.6 代码已更新但页面仍显示旧内容
这个坑我踩过不止一次。排查顺序:
- 钩子日志里
npm run build是否真的跑完了,输出里有没有新的 hash 文件名; ls -l /var/www/shop/dist/assets/ | head看新 hash 文件在不在;- 浏览器
curl -I看index.html的Cache-Control,如果还是max-age=3600说明配置没 reload; - 浏览器强制刷新(Ctrl+Shift+R)或者开无痕窗口。
如果第 1 步日志显示构建完成但第 2 步看不到新文件,那多半是root指错了目录,Nginx 服务的是另一个路径下的旧dist。这种问题在同时配过多个站点、sites-enabled里有两个软链时特别容易发生,nginx -T | grep root一把把所有 root 都列出来对比。
6.7 403 Forbidden 的三层原因
403 不是“没权限访问网站”,而是“有权限访问目录,但目录里没有可用的索引文件,且不允许列目录”。三层原因:
root指向的目录里没有index.html(构建产物不在那,或者outDir改过名字);- 目录属主不是 Nginx worker 用户,读不了,同时
error_log里有Permission denied; index指令里的文件名和实际产物不匹配。
修法是先把root指向的目录内容和预期产物比对,再namei -l /var/www/shop/dist/index.html逐级看权限。电商项目里如果dist是钩子用 root 建的,而 Nginx worker 是www-data,那 755 目录 + 644 文件本来能读,但一旦有人chmod 700,立刻 403。
6.8 仓库里塞了 node_modules 的连锁问题
新手常常git add -A一把梭,把node_modules和dist都提交了。后果不只是仓库变大:
- 推送慢,钩子里的
checkout要处理几万个文件,30 秒都跑不完; npm ci会因为本地已有node_modules而行为异常;dist里的旧构建产物被检出,和新建的混在一起,出现“删不掉的旧 hash 文件”;- 换机器部署时因为 esbuild 之类的二进制依赖和平台不匹配,直接报错。
处理方式是把.gitignore补全,然后把已经提交的内容从索引里移除(不删本地文件):
cat > .gitignore <<'EOF' node_modules/ dist/ .DS_Store *.log .env.local .idea/ .vscode/* !.vscode/extensions.json EOF git rm -r --cached node_modules dist git commit -m "chore: 移除构建产物和依赖目录".env.local一定要进 ignore,里面常常有测试环境的接口地址或者密钥。至于.env.production要不要提交,取决于团队约定——它一般只有公开的接口前缀,不包含敏感信息,提交反而方便追溯。
7. 实际部署中值得分享的经验
几个跑顺之后才会注意到的点,写下来省得你重复踩。
Nginx 配置改完先nginx -t再 reload。直接systemctl reload nginx而不测配置,一旦语法错,reload 会失败但旧进程还在跑,你看到的现象是“改了没生效”,然后开始怀疑缓存,方向全错。
钩子日志留一份。post-receive的输出只在推送那个会话里可见,推完就没了。我习惯在脚本末尾加一行>> /var/log/deploy.log 2>&1,出问题时翻日志比重新推一次快得多。
发版后做三步验证。首页打开、随便点进一个商品详情、在那个详情页按 F5。这三步能覆盖 95% 的部署事故,比“好像能打开”靠谱。
别在服务器上直接改代码。钩子里的checkout -f会把你改的东西直接覆盖掉,而且不会有任何提示。要热修就本地改、提交、推,哪怕只是改一个文案。
回滚别慌。本地git log找到上一个正常版本,git reset --hard <commit>,然后git push -f deploy main,钩子会重新构建那一版。整个过程一分钟内,比在服务器上找备份文件强太多。
最后提一个小技巧:在index.html里塞一个版本号注释,比如构建时用插件注入<!-- build: 20240612-1530 -->,然后在浏览器控制台document.documentElement.outerHTML.match(/build: (\S+)/)一眼确认线上跑的是不是最新版本。这个手段土,但在“我说我推了、你说没生效”的扯皮场合,比什么都管用。