上周帮一位做后台管理面板的朋友把项目从本地挪到公网,打包、上传、改配置、重启,整个链路跑通其实不到四十分钟。但中间他卡了三次:一次是路由刷新出来一片白,一次是接口请求打在 Nginx 上直接回了 405,还有一次是浏览器死活吃到上一版的旧资源。这三件事几乎覆盖了 Vue 项目部署到云服务器时最典型的坑——它们跟服务器本身关系不大,问题都出在构建产物、路径规则和缓存策略这几处细节上。
这篇东西想聊的就是这条完整的链路:一个本地能跑的 Vue 工程,怎么稳稳当当地放到一台公网服务器上,让别人打开域名就能用。它不依赖某个特定的云平台,也不假设你已经会写 Nginx 配置。前端想自己把作品挂上线的、后端顺手兼着运维的、做课程设计要交个公网地址的,都能从里面挑到能直接抄的部分。我会把每一步为什么这么做的逻辑讲清楚,也会把踩过的坑和现场判断的顺序摊开说,因为部署这件事最耗时间的从来不是写配置,而是出问题之后不知道先看哪儿。
1. 先把部署这件事想清楚:静态产物和常驻服务的分水岭
1.1 你手里的项目到底属于哪一类
很多人一上来就问"服务器要装什么环境",这个问题其实问错了顺序。先要判断的是:你的 Vue 项目构建完之后,产物是什么形态。绝大多数用脚手架生成的管理后台、活动页、官网、工具站,构建结果就是一个文件夹,里面是index.html加一堆带哈希的 JS 和 CSS 文件。浏览器把这几样东西下载下来,JS 在浏览器里接管路由和渲染,服务器在整个过程中只负责"把文件递出去"。这类东西叫单页应用,它不需要 Node 常驻进程,不需要数据库连接,理论上用一个最朴素的静态文件服务器就能扛住。
另一类是做了服务端渲染的项目,构建产物里除了前端文件,还有一个需要在服务器上跑起来的 Node 服务,用户的每个请求都会先打到这个进程上,由它拼好 HTML 再返回。这类项目部署复杂度完全是另一个量级:要考虑进程守护、内存占用、构建产物和运行时的 Node 版本一致性、以及服务挂掉之后怎么自动拉起来。
判断方法很简单,看package.json里的构建脚本和产物目录。构建完只有dist里一堆静态文件,那就是第一类;如果还要求你node server.js之类才能跑,就是第二类。这篇主要讲第一类,因为它是绝大多数人的实际场景,也是"轻松"两个字真正能成立的地方。第二类我会在后面的选型对比里提一下差异点,但不会展开。
还有一个中间态值得单独说:项目里带了后端接口。这时候服务器上确实需要一个后端服务,但那和 Vue 的部署是两件事,它们之间的关系是"前端文件由一个服务递出去,接口请求由另一个服务接住",中间用反向代理连起来。把这两件事在心里拆开,后面的配置就不会乱。
1.2 一次部署到底经过哪些环节
把链路拆开看,从你本地敲下构建命令到用户在手机上看页面,中间有这么几段:本地构建产出静态文件;文件从本地传到服务器上的某个目录;服务器上有一个长期运行的服务(通常是 Nginx)把这个目录对外暴露;域名解析把这个服务器的地址和一个好记的名字绑起来;如果有 HTTPS,中间还有一层证书协商。
这条链上每一段都可能成为故障点,而且故障表现高度相似——用户那边看到的都是"打不开"。所以排查的时候必须有一个固定的顺序,从后往前或者从前往后都行,但不能跳。我习惯的顺序是:先确认服务器上文件是不是真的更新了,再看 Nginx 配置有没有生效,再看端口和防火墙通不通,最后才看浏览器那边。这个顺序的依据是"越靠近数据源的地方越容易出错,也越容易被忽略"。
另外要建立一个认知:部署不是一次性的动作,而是一个可以重复执行的过程。第一次手工传文件没问题,但第三次、第五次还手工传,早晚会传错版本或者漏传文件。所以从第一天起,就该把目录结构和上传方式设计成"随时可以再来一次"的样子,这一点在第 5 节会具体展开。
1.3 三种发布方式的取舍
发布方式的选择没有绝对优劣,取决于项目几个人维护、多久发一次版。下面这张表是我自己用过之后的实际感受。
| 方式 | 适用场景 | 上手成本 | 主要风险 |
|---|---|---|---|
| 手工打包 + 手动上传 | 一次性项目、演示、个人练手 | 极低 | 容易传错目录、漏文件、忘记改配置 |
| Git 钩子自动构建发布 | 小团队、单人维护多个项目 | 中等 | 钩子脚本出错时排查麻烦,服务器要装构建环境 |
| 流水线自动构建发布 | 多人协作、发版频繁 | 较高 | 需要维护构建环境和凭据,配置漂移 |
我个人的建议是:第一次部署用手工方式,因为你必须亲手走一遍才能知道每一步发生了什么;从第二次开始就换成脚本或钩子。直接从零上流水线不是不行,但一旦出问题,你面对的是一个自己不熟悉的黑盒,而那时候你最需要的恰恰是"知道每一步本该产出什么"。
2. 服务器侧的准备:从一台空机器到能接住静态站
2.1 系统选择和最小权限的落地
系统版本不用纠结太久,主流的两个大系随便挑一个自己顺手的就行,关键是选长期支持版本,别用已经停止维护的老版本,否则后面装软件会碰到各种依赖问题。买机器的时候还有个容易忽略的点:配置不用一步到位,静态站的资源消耗极低,两个核两个 G 的入门配置承接几千日活完全够用,真正吃资源的是构建过程和后端服务。磁盘倒是建议别选最小的,构建产物、日志、多个历史版本叠起来,空间消耗比想象中快。
接下来是账号。拿到机器之后第一件事不该是装软件,而是把登录方式收拾干净。用最高权限账号直接操作是个很坏的习惯,正确做法是新建一个普通账号,给它配置密钥登录,然后让这个账号通过提权命令去执行需要特权的操作。这样做的好处是:日常操作不会因为一个手滑的命令把系统目录改坏,而且日志里能看清是谁做了什么。
具体动作是:用密钥登录,在服务器上创建普通用户,把本地公钥写进这个用户的授权文件,改好文件权限(这个文件的权限一旦放开,密钥登录就会失效,这是最常见的第一次配置失败原因),然后在提权配置里给这个用户放行。改完之后先别退出当前会话,另开一个终端验证新账号能正常登录、能正常提权,确认无误再关闭旧会话。这个"先验证再断开"的习惯能救你很多次。
提示:改任何和登录相关的配置时,永远保持一个已登录的会话不关闭,直到新方式验证通过。这是运维里最便宜的一条保险。
2.2 安装 Web 服务与目录规划
Web 服务这块,静态托管用 Nginx 是行业默认答案,理由很实在:配置直观、静态文件性能好、反向代理成熟、文档和案例多到不需要动脑。安装就是包管理器一条命令的事,装完用系统服务管理命令确认它已经在跑,然后用服务器自己的地址访问一下默认页,能看到欢迎页就说明端口通了。
目录规划是我特别想强调的一步,因为它决定了后面回滚和排查的舒适度。我的习惯是这样:
/var/www/项目名/ ├── releases/ │ ├── 20240611-1030/ │ ├── 20240612-1545/ │ └── 20240615-0920/ └── current -> releases/20240615-0920每次发布都新建一个带时间戳的目录,把构建产物整个放进去,然后用current这个软链接指向最新的一版。Nginx 配置里只写current的路径,这样切换版本就是一条改软链接的命令,秒级生效,而且回滚就是把它指回上一个目录,不需要重新传文件。这个结构看起来多了一层,但它把"发布"和"回滚"变成了同一个动作的两个方向,实战价值极高。
目录权限也要顺手定好:目录给读和执行权限,文件给读权限,属主设成 Web 服务进程能读到的用户。这里有个细节,父级目录如果没有执行权限,Web 服务进程是进不去的,表现就是不管怎么配都是 403,很多人卡在这里。
2.3 只开该开的端口
服务器买回来默认可能开了一堆端口,或者安全策略是全放行。正确做法是在控制台的安全组里只保留必要的:远程登录端口(建议改掉默认的,能挡掉大量无差别扫描)、80 和 443。其他一律关掉。
系统层面的防火墙也要同步配置,两层都开是有意义的——控制台那层挡在机器外面,系统那层挡在进程之间。配置的时候先放行远程登录端口再启用防火墙,顺序反了会把自己锁在外面。
注意:任何时候调整防火墙规则,都先确认远程登录端口在白名单里。这个坑每个人都踩过一次,代价是去控制台重置。
2.4 服务器上到底要不要装 Node
这是新手最容易纠结的问题。我的答案很干脆:如果只是托管构建产物,服务器上不要装 Node,也不要把node_modules传上去。原因有三个:服务器上的 Node 版本和本地不一致会产生难以复现的构建差异;依赖目录动辄几百兆,传上去纯属浪费;服务器上跑构建会吃掉大量内存,小配置机器很容易在构建阶段直接卡死。
正确做法是本地构建、只传产物。本地环境是自己可控的,构建失败了立刻能看到完整报错。只有在用 Git 钩子做自动发布时才需要在服务器上装构建环境,那时候也要注意限制构建进程的内存,否则一个稍大的项目就能把机器拖垮。
反过来说,如果你的项目确实需要服务端渲染,那 Node 就必须装,而且要装和本地完全一致的版本,用版本管理工具锁住,同时配好进程守护,让它在崩溃后自动重启。这部分复杂度是静态托管的好几倍,选型时要认真权衡是不是真的需要。
3. 构建产物才是真正被部署的东西
3.1 打包前必须确认的几项配置
很多"部署完白屏"的问题,根因在构建配置而不是服务器。最典型的是资源基础路径。默认配置下,构建产物里的资源引用是绝对路径,从根目录开始找。如果你的站点是部署在域名的子路径下,比如https://example.com/admin/,那资源就会去找https://example.com/assets/xxx.js,直接 404,页面白屏。
解决办法是在构建配置里把基础路径设成子路径,或者在构建命令里加参数覆盖。反过来,如果你的站点就在根路径,那就保持默认的斜杠,不要画蛇添足。
另一个要确认的是产物输出目录。默认是dist,但有人改过配置,上传时按默认目录找,结果传了个空目录上去,Nginx 返回 403 或者目录列表,看着像配置问题其实是文件根本没上去。养成习惯:构建完先在本地dist目录里用静态服务器跑一遍,确认能正常访问,再上传。本地都跑不通的东西,传上去只会更麻烦。
还有路由模式。用哈希模式的话,地址里带井号,刷新不会出问题,因为井号后面的内容不会发给服务器。用历史模式的话,地址看起来很干净,但刷新时服务器会收到一个它不认识的路径,如果没做兜底就会 404。这个问题的解法在 4.2 节说。
3.2 环境变量和多环境打包
前端项目的环境变量有个必须记住的边界:打进产物里的变量是公开的,任何人打开浏览器开发者工具都能看到。所以密钥、令牌、私有的接口凭据,一律不能放进去。这条不是建议,是底线。
实际做法是准备多份环境配置文件,构建时按模式加载。变量命名要有统一前缀,避免和系统环境变量冲突。配置的读取时机是构建时而不是运行时,也就是说,产物一旦构建完成,里面的接口地址就固定了,想改必须重新构建。这一点经常被误解,有人以为改服务器上的某个文件就能切换环境,结果怎么改都不生效。
| 环境 | 接口地址 | 构建命令 | 用途 |
|---|---|---|---|
| 开发 | 本地代理地址 | 开发服务器 | 本地调试,走代理避开跨域 |
| 预发 | 预发域名 | 带模式的构建 | 上线前验证,用真实数据但独立库 |
| 生产 | 正式域名 | 默认构建 | 对外发布 |
预发环境这一步很多人省掉了,代价是每次上线都像开盲盒。加一个预发环境成本极低,但能把大部分配置错误挡在正式发布之前。
3.3 产物瘦身:分包、压缩、指纹
产物大小直接影响首屏体验,也影响上传时间。几个立竿见影的动作:
第一是分包。把第三方依赖和业务代码拆开,第三方依赖变动频率低,可以长期缓存。配置里把体积大的库单独拆成一个块,浏览器就能在版本更新时只重新下载业务代码那一小部分。
第二是预压缩。Web 服务可以在响应时实时压缩,但那是每次请求都要算一遍,浪费 CPU。更好的做法是构建时就把文件压好,生成.gz文件放在旁边,服务配置里打开静态压缩支持,直接读现成的压缩文件。实测下来首屏加载能快一截,服务器负载也低。
第三是文件名带内容指纹。构建工具默认就会给资源文件名加哈希,这个哈希是基于文件内容算的,内容变了哈希才变。这是后面做长期缓存的前提:只有文件名跟着内容变,才敢给资源设置一年的缓存时间。
配置片段大概长这样:
// vite 配置示例 export default defineConfig({ base: '/', // 部署在根路径就用 / build: { outDir: 'dist', assetsDir: 'assets', sourcemap: false, // 生产环境关掉,减小体积也避免泄露源码结构 rollupOptions: { output: { manualChunks: { vendor: ['vue', 'vue-router', 'pinia'] // 稳定依赖单独成块 } } } } })sourcemap这一项值得单独说:开着方便线上排查,但也意味着源码结构对外可见。折中方案是生成但不公开部署,出问题时手动拿到本地做映射,而不是让它跟着产物一起上线。
4. Nginx 配置:绝大多数部署问题都在这几十行里
4.1 一份最小可用配置的逐行拆解
先给一份能直接用的配置,然后逐段讲为什么。
server { listen 80; server_name example.com; root /var/www/myapp/current; index index.html; location / { try_files $uri $uri/ /index.html; } location /assets/ { expires 1y; add_header Cache-Control "public, immutable"; } 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; } }root指向软链接目录,这样切换版本不用改配置。index指定默认文件。location /里的兜底规则是单页应用的关键,含义是:先找同名文件,再找同名目录,都找不到就返回index.html。这样不管用户访问的是/user/123这种前端路由,服务器都会把入口页面递出去,由前端路由自己解析路径。
location /assets/这一段给带指纹的静态资源设置了一年缓存,并且加了不可变标记告诉浏览器"这个地址的内容永远不会变,不用回来问了"。这是性能的关键,但前提是文件名真的带指纹。
= /index.html用的是精确匹配,优先级最高,专门给入口文件设置不缓存。这一条是整个缓存策略的命门:入口文件不能被缓存,否则用户会一直拿到旧版本引用的旧资源地址。
反向代理那一段把/api/开头的请求转到本机的后端服务。几个请求头的作用分别是:传递原始域名让后端能生成正确的链接,传递真实客户端地址方便后端做限流和日志,传递协议类型让后端知道用户走的是不是加密连接。
4.2 刷新就 404 的根因和修法
这个现象太常见了。用户在首页点来点去一切正常,一按刷新就白屏或者服务器返回 404。原因在于:前端路由在浏览器里跳转时,根本没有向服务器发请求;但刷新会真的向服务器请求当前地址,而服务器上并不存在/user/123这样一个文件或目录。
修法就是上面那条兜底规则。但这里有个容易被忽略的例外:如果兜底规则写得太宽,接口路径也会被兜到index.html上,表现是接口请求返回一段 HTML,前端解析失败,报一个莫名其妙的错。所以反向代理的location一定要放在兜底规则之前,或者用更精确的匹配把它保护起来。
另外还有一种情况:兜底规则写了但没生效,刷新还是 404。这时候要先确认配置有没有被重新加载,再看是不是有另一个配置文件把这段覆盖了。Nginx 的配置是包含式的,主配置里引入了一个目录下的所有文件,如果多个文件里都定义了同一个server_name,实际生效的是先被加载的那个,后加载的会被忽略并在日志里给个提示。这个提示非常容易被忽略,但它是"我明明改了配置为什么不生效"的高频原因。
4.3 缓存策略:既要快,又不能吃到旧版本
缓存这件事的核心矛盾是:用户想要快,你想要新。解法是把资源分成两类,分别对待。
第一类是带内容指纹的资源,比如app.a1b2c3.js。这类文件的地址随内容变化,内容一变地址就变,所以可以放心大胆地缓存一年。用户第二次访问时直接从本地读,一个字节都不用下载。
第二类是不带指纹的文件,主要就是index.html。这个文件每次都必须回服务器确认,因为它里面引用的是最新的资源地址。如果它也缓存了,用户就会一直加载旧版本,你更新了十次他都看不到,最后来一句"你们这网站是不是坏了"。
这个组合的效果是:用户第二次访问时,只请求一个几 KB 的入口文件,然后从本地缓存里拿所有资源。既快,又能拿到最新版本。
实际落地还有两个坑。一个是中间层缓存:如果前面还有一层内容分发网络,那边也要配置成对入口文件不缓存或者短缓存,否则你服务器上更新了,用户拿到的还是中间层的老文件。另一个是缓存刷新:发布之后如果发现某些用户还是旧版本,先让他们强刷一次验证,别急着怀疑配置。如果强刷能好,就是缓存策略问题;强刷还是不行,那才去看文件到底传没传上去。
提示:判断是不是缓存问题有一个很快的办法——看资源文件的哈希和服务器上
current目录里的文件名是否一致。不一致就是缓存,一致就是别的问题。
4.4 反向代理里那两个拧螺丝的细节
反向代理的配置看起来简单,实际有两个细节能让接口全部失灵。
第一个是proxy_pass末尾的斜杠。写了斜杠,请求路径里匹配的那一段会被去掉再转发;不写斜杠,匹配的那一段会原样带过去。举个例子:location /api/配proxy_pass http://127.0.0.1:8080/,用户请求/api/user/list,后端收到的是/user/list;如果末尾没有斜杠,后端收到的是/api/user/list。这两种结果对后端路由的影响是天差地别的,接口全 404 或者全 405,先回来看看这个斜杠。
第二个是请求头的传递。不传Host的时候,后端拿到的是127.0.0.1:8080,有些框架会根据这个生成带域名的链接或者做跨域校验,结果就是接口莫名其妙报错或者跳转到错误地址。不传真实客户端地址的时候,后端日志里全是本机地址,做限流和风控完全失效。
配置改完记得先做语法检查再重载。语法检查不通过就重载,老进程会继续跑,你以为改生效了其实没有,这种"改了没反应"的困惑十有八九是这里。
5. 把文件送上去:几种上传方式的实操对比
5.1 增量同步比全量覆盖靠谱
手工传文件最容易犯的错是"只传了改动的文件",结果删掉的旧文件还留在服务器上,新旧混杂,出现问题极难定位。正确做法是用支持镜像同步的工具,每次都让服务器目录和本地产物完全一致,多出来的删掉。
rsync -avz --delete \ --exclude='.git' \ ./dist/ user@server:/var/www/myapp/releases/20240615-0920/几个参数的含义:-a保留权限和时间戳,-v显示详细过程,-z传输时压缩,--delete删除目标端多余的文件。这条命令跑完,服务器上的目录就和本地产物一模一样了,不用担心残留。
传完之后执行软链接切换:
ln -sfn /var/www/myapp/releases/20240615-0920 /var/www/myapp/current-sfn这三个参数组合是有讲究的:s建软链接,f覆盖已有的,n把已存在的软链接当成普通文件处理而不是进入它指向的目录。少了n的话,命令会在旧版本目录里建一个指向新版本的软链接,结果就是完全错乱。这个坑我踩过,表现是访问站点出现奇怪的嵌套路径。
5.2 用 Git 钩子把小团队的发版自动化
项目稳定之后,可以在服务器上建一个裸仓库,配置一个接收后钩子,在钩子里执行拉取、构建、切换目录这几步。开发者本地推送代码,服务器自动发布,体验很顺。
代价是服务器上要装完整的构建环境,而且构建过程会消耗内存。小配置机器上跑构建,容易在压缩阶段被系统直接杀掉进程,表现是钩子执行到一半没了,代码推上去了但站点没更新。解决办法有两个:加交换空间,或者把构建挪到别处,服务器只接收产物。我一般选后者,把复杂度留在本地。
钩子脚本里还有一点很重要:构建失败要立刻中止,不能继续切换软链接。否则会把一个半成品的产物发布出去,比不发布还糟糕。脚本里每一步都要判断返回值,任何一步失败就退出并保留旧的current。
5.3 流水线:稳定之后才值得投入
多人协作、发版频繁的团队,最终都会走到流水线这一步。基本形态是:代码合并到主分支触发构建,构建产物打包成压缩包,通过安全的方式传到服务器,解压到新版本目录,切换软链接,最后做个健康检查。
这里面最值得投入精力的是两点:一是构建环境的一致性,用容器化的方式固定构建环境,避免"我本地能跑"的问题;二是发布失败的处理,健康检查不通过就自动切回旧版本,而不是让用户对着一个坏站点等人工介入。
流水线不是越复杂越好。我见过一些团队,为了一个每天只发一两次的静态站,搭了十几个环节的流水线,结果是每次出问题都要花比手工部署更长的时间去排查流水线本身。工具的目的是减少重复劳动,不是增加新的不确定性。
6. 上线之后:问题排查的固定路径
6.1 常见问题速查
| 现象 | 最可能的原因 | 快速验证方式 |
|---|---|---|
| 全白,控制台一堆 404 | 基础路径配错,资源指向了错误目录 | 看 404 的地址前缀,和构建配置比对 |
| 首页正常,刷新或直接访问子路由 404 | 缺少单页应用兜底规则 | 手写一个子路由地址直接访问 |
| 接口 404 或 405 | 反向代理路径斜杠问题,或匹配顺序被兜底规则抢走 | 看后端日志里收到的实际路径 |
| 改了代码但页面没变 | 入口文件被缓存,或文件根本没上传 | 强刷验证,比对服务器文件哈希 |
| 403 禁止访问 | 目录权限或父级目录缺执行权限 | 用 Web 服务进程的身份去读文件 |
| 站点偶发打不开 | 服务进程崩溃或内存被打满 | 看系统日志和服务状态 |
6.2 从浏览器到日志的定位顺序
出现问题别慌,按这个顺序走,一般三轮内就能定位。
第一轮,打开浏览器开发者工具的网络面板,看失败的请求是哪一类:是入口文件没拿到,还是资源文件 404,还是接口报错。资源 404 的地址会直接告诉你服务器在找什么路径,和实际文件路径一比就知道是基础路径配错还是文件没传。
第二轮,在服务器上直接看文件。用命令行确认current软链接指向哪里,目标目录里有没有index.html,资源文件名和浏览器请求的是否一致。这一步能区分"文件没上去"和"文件上去了但服务没读到"。
第三轮,看服务日志。访问日志能告诉你请求到底有没有打到服务上,状态码是多少;错误日志能告诉你权限问题、配置语法问题、后端连不上等等。日志路径一般在系统日志目录下,配置里也可以单独指定。
这个顺序的底层逻辑是:先确认用户看到的现象,再确认数据源的状态,最后看中间层的处理。反过来先去看配置,很容易在一堆正确的配置里找一个不存在的问题。
6.3 加一层加密和它的续期问题
现在浏览器对非加密站点的提示越来越明显,正式对外的站点基本都要上 HTTPS。配置本身不复杂:拿到证书文件和私钥文件,在服务配置里指定路径,监听加密端口,然后把明文端口的请求重定向过去。
server { listen 443 ssl; server_name example.com; ssl_certificate /path/to/fullchain.pem; ssl_certificate_key /path/to/privkey.pem; # 其余 location 配置同上 } server { listen 80; server_name example.com; return 301 https://$host$request_uri; }真正容易被忽略的是续期。免费证书通常只有三个月有效期,到期不续站点直接打不开,而且报错信息(证书过期)在浏览器里表现得像"站点不安全",很多用户会直接关掉页面。解法是用自动签发工具配一个定时任务,在到期前自动续期,并在续期之后自动重载服务配置。
定时任务的日志要留着,而且要定期看一眼。我见过自动续期失败几个月没人发现,最后证书过期站点挂掉的情况——自动化并不等于不用管,它只是把"每天检查"换成了"每周看一次日志"。
注意:重载配置和重启服务是两回事。重载是平滑的,不断连接;重启会短暂中断。证书续期后用重载就够了,别用重启。
7. 交付之后:让它能长期稳定地活下去
7.1 版本回滚要能在三十秒内完成
前面说的目录结构在这里体现价值。回滚的动作只有一条:把current软链接指回上一个版本目录。
ls -1 /var/www/myapp/releases/ # 看有哪些历史版本 ln -sfn /var/www/myapp/releases/上一版 /var/www/myapp/current不需要重新上传,不需要重新构建,秒级生效。但有个前提:上一次发布之后如果改过环境变量或者接口地址,回滚时要注意产物的接口地址是构建时固定的,回滚到旧版本可能指向旧的接口。这种情况下要确认旧接口还在不在。
历史版本保留几个合适?我一般留最近五个,够覆盖绝大多数"发布之后发现有问题"的场景。更多的版本除了占磁盘没有别的用处,定期清理比留着更省心。清理的时候注意别把current指向的那个删了。
7.2 日志、磁盘和一点点监控
静态站的运维负担很轻,但有三件事要定期看。
第一件是磁盘。日志会一直增长,不切割的话几个月就能吃掉几个 G。配置一个按天轮转、保留固定份数的策略就够,不需要复杂的方案。
第二件是访问日志里的异常状态码。如果有大量 404 集中在某几个路径,往往说明站内有死链,或者是搜索引擎在爬一些不存在的地址;如果有大量 5xx,说明后端有问题。定期扫一眼能提前发现不少事。
第三件是服务本身的存活。最简单的做法是配一个定时任务,定期请求一下站点地址,失败就发个通知。不用上复杂的监控系统,一个几十行的脚本就够用。我自己的经验是,站点挂掉之后往往是用户先发现的,这个体验很差,而加一个最基础的存活检查成本几乎为零。
最后分享一个我在实际部署里养成的小习惯:每次发布之后,不要只看首页能不能打开,而是从浏览器无痕窗口完整走一遍关键路径——登录、列表、详情、提交。无痕窗口没有缓存干扰,能真实反映新用户看到的样子。这个动作花不了两分钟,但它拦住过我好几次差点发出去的版本问题,比事后排查便宜太多了。