看到标题里的“ubnutu”,我先说个小事:这应该是 ubuntu 的笔误。不过这并不影响思路,下面我统一用 ubuntu 22.04 来讲。很多前端同学开发完 vue 项目,npm run build 一执行,dist 目录出来了,然后就卡住了——接下来该干嘛?有人拿 dist 直接丢给后端,有人压缩包发给运维,还有人复制粘贴到服务器结果打开全是白屏。这篇文章就把这个过程收敛成清清楚楚的三步:本地构建、安装 nginx、上传配置站点。目标只有一个:让你在自己电脑上敲完命令,浏览器里能稳定访问到线上页面。我会把每一步的命令、参数、为什么这么做全部讲透,最后再给你一份高频问题排查清单。不管你是第一次接触服务器部署的前端新人,还是想自己搞定发布流程的全栈开发者,照着这篇文章操作就能把项目干净利落地跑起来。
1. 部署思路复盘:vue 项目到底部署了什么,以及为什么是 nginx
1.1 构建产物的本质决定了部署方式
很多人第一次部署 vue 项目时会有一个误区:以为要把整个项目源码传到服务器上,然后在服务器上 npm install、npm run dev。这是把“开发模式”和“生产模式”搞混了。vue 是典型的单页应用(SPA),开发时依赖 node 环境跑 dev server,但用户真正访问的是编译压缩后的静态资源:一个 index.html、一堆 js 和 css 文件。
所以部署的本质其实非常简单:把 npm run build 生成的 dist 目录里的文件,交给一个 Web 服务器去托管,让用户通过 http 请求拿到这些静态文件。打个比方,dist 目录是一桌已经做好的菜,nginx 是替你把菜端到客人面前的服务员。你不需要在服务器上重新炒菜,只需要把菜放上餐台,并且告诉服务员“客人要点哪道菜时该往哪个窗口取”。
理解这一点之后,后面所有的操作都会变得顺理成章:构建这个动作发生在本地,上传动作负责搬运,nginx 负责对外提供服务,仅此而已。
1.2 为什么选 nginx,而不是 Apache 或其他方案
部署静态网站的可选项其实不少,我帮你把主流的几个拉出来对比一下:
| Web 服务方案 | 静态文件性能 | 反向代理配置 | 内存占用 | 学习成本 | 适用场景 |
|---|---|---|---|---|---|
| nginx | 极高,高并发下稳定 | 简洁直观 | 低,约几 MB 到几十 MB | 低 | 静态站点 + 反向代理,中小项目首选 |
| Apache | 良好,但高并发下更吃内存 | 配置冗长 | 较高 | 中 | 老牌系统,兼容模块多 |
| Node 直接托管 | 一般,取决于业务逻辑 | 需要自己写代码 | 高 | 中 | 纯 Node 服务、前后端同构 |
| 云对象存储静态托管 | 极高 | 有限,依赖平台 | 无服务器概念 | 低 | 纯静态站点、或有 CDN 需求的项目 |
nginx 在静态文件服务这个场景下几乎是碾压级的优势。它不是靠什么魔法,而是基于事件驱动的异步架构,一个进程能挂住大量并发连接,内存占用却很小。再加上配置语法走的是声明式路线,写一个 server 块就能搞定虚拟主机、反向代理、缓存、gzip 压缩这些需求,不需要像 Apache 那样维护一堆钩子模块。对个人项目和中小团队来说,nginx 就是那个性价比最高的服务员。
1.3 为什么选 ubuntu 22.04 作为服务器系统
ubuntu 22.04 是 LTS 版本,意味着有长期安全更新和维护周期,系统稳定性有保障。它的 apt 包管理器让安装软件变得非常简单,一条 apt install nginx 就能装完,不需要像编译源码那样折腾依赖。另外,ubuntu 的服务器生态非常成熟,几乎你能遇到的部署问题都有人在网上分享过排查经验。这套组合拳打下来,对于想要快速跑通部署流程的开发者来说,是最省心的起点。
2. 动手前先摸清三个关键细节:路由模式、环境变量、服务器初始状态
2.1 路由模式决定服务器要不要“兜底”
vue-router 有两种路由模式:hash 模式和 history 模式。默认开发环境下,很多脚手架会直接用 createWebHashHistory,地址栏里会出现一个 # 号。比如 http://example.com/#/home。这种模式下的路由变化其实是在改 # 后面的部分,浏览器不会真正向服务器发起新的页面请求,所以刷新页面时 nginx 根本感知不到,也就不会出现 404。这就是很多新手第一次部署没遇到问题的原因。
但生产环境大家通常会用 createWebHistory,因为地址栏干净、美观,也更符合用户认知。问题在于:当用户直接访问 http://example.com/home 时,浏览器会把这个地址当作一个真实路径发给 nginx。nginx 在磁盘上找 home 这个文件或目录,结果当然找不到,于是默认返回 404。
解决办法是在 nginx 配置里加一行 try_files,把请求统一回退到 index.html。这个行为的本质是告诉 nginx:如果磁盘上找不到对应文件,就把这个请求交给前端路由去处理,让 vue-router 自己判断应该渲染哪个页面。听起来很绕,但配起来就一行,后面第三章会给出完整配置。这个细节是整个部署过程中新手最容易踩的坑,我建议你在构建项目之前就先确认自己用的是哪种模式。
2.2 环境变量和接口地址的坑
vue 项目里通常会有 .env.development 和 .env.production 两个文件,里面定义了 VITE_API_BASE_URL 或类似的环境变量。本地开发时请求的是 http://localhost:8080/api,这是开发服务器的地址;但打包后的生产代码必须指向线上后端地址,比如 https://api.example.com。如果你在代码里硬编码了接口地址,或者 .env.production 里的值没配对,就会出现一个非常隐蔽的问题:页面能打开,界面也正常渲染,但所有请求都打到本地开发地址上,最终一片报错。
我见过不少同学把接口地址直接写死在业务代码里,每次部署不同环境就要改代码、重新打包,费时费力还容易出错。正确做法是一开始就区分环境变量,打包时脚手架会自动读取 .env.production 里的值并注入代码。还有一个小细节:打包完成后,你可以打开 dist 目录下的 js 文件,搜索一下你的接口域名,肉眼确认是否被正确替换进去了。这一步只需要几秒钟,却能帮你提前发现一半以上的部署事故。
2.3 服务器初始状态检查清单
在开始安装之前,先在服务器上确认三件事。第一,nginx 是否已经存在。有些云服务器镜像预装了 nginx,重复安装会报错或者引入版本冲突,用 nginx -v 看一下就知道。第二,80 端口是否被占用。服务器上如果有其他 Web 服务占了 80,nginx 启动时会遇到 bind() failed 的报错。用 ss -tlnp | grep :80 检查,如果有进程占用了,你得先搞清楚它是干什么的,别贸然杀掉。第三,当前用户是否有 sudo 权限。部署过程中大量命令需要 root 权限,如果你只有普通账号且没有 sudo,后面会非常被动。
这三件事花不了两分钟,但能避免后续所有操作都卡在半道上。我把检查命令写在一起,你在 ssh 登录服务器后直接复制执行即可。
nginx -v 2>/dev/null || echo "nginx 未安装" ss -tlnp | grep -E ':(80|443)' || echo "80/443 端口未占用" sudo -v && echo "当前用户有 sudo 权限"3. 三步部署实操:从本地构建到 nginx 线上发布
3.1 第一步:本地构建 vue 项目,拿到 dist 产物
部署流程的第一步其实不在服务器上,而是在你自己的电脑上完成。打开项目根目录,先确认 node 环境没问题,再安装依赖并构建。你可以在本地打开终端,依次执行以下几个命令。
node -v npm -v npm install npm run buildnode -v 这一步很多人会跳过,但它其实值得看一眼。vue 3 的脚手架一般要求 node 版本在 18 以上,如果你本地装的是老版本 node,npm install 过程中可能报错,构建出来的资源也可能存在兼容性问题。遇到版本不符时,建议先升级 node,而不是在项目里强行绕过。
npm install 执行完毕后,npm run build 会把项目编译输送到 dist 目录。构建完成后,你用 ls 看一下 dist 目录里有什么。
ls -la dist正常情况下你会看到 index.html 和一个 assets 目录,assets 里面是编译压缩后的 js、css、图片等静态资源。有些项目配置了 public 目录,里面的内容也会原样拷贝到 dist 根目录。这里有一个确认点:index.html 必须存在于 dist 的根路径,因为 nginx 默认会在这个位置寻找入口页面。如果构建产物路径不对,后面配置 nginx 时要调整 root 路径,但最简单的情况就是让 dist 目录作为网站的根目录。
构建完成以后,你可以在本地先启动一个临时静态服务器,亲眼验证一下产物能否正常访问。
npx serve dist浏览器打开终端提示的地址,如果页面正常渲染、接口正常请求,说明构建产物本身没有问题。这一步能帮你把“前端代码问题”和“服务器配置问题”隔离开,后面万一出现故障,排查范围会小很多。本地验证有问题时先别急着部署,因为部署只能把问题从一台机器搬到另一台机器,不会解决问题。
3.2 第二步:安装并启动 nginx
接下来切换到服务器操作。ubuntu 22.04 安装 nginx 非常简单,我直接把命令完整列出来。
sudo apt update sudo apt install nginx -yapt update 的作用是刷新软件源索引,避免安装到过期版本。安装完成后,nginx 会作为系统服务存在,默认已经启动。执行下面这条命令确认服务状态。
sudo systemctl status nginx如果你看到输出里带有 active (running) 的字样,说明 nginx 服务在正常运行。如果没启动,可以用 sudo systemctl start nginx 手动拉起来。我还建议顺手执行 sudo systemctl enable nginx,让 nginx 在服务器重启之后自动启动。这一步很不起眼,但能避免服务器意外重启后网站静悄悄挂掉的情况,属于典型的花一分钟省一小时的操作。
安装完成后,先了解 nginx 的几个关键目录和文件,因为后面所有操作都会围绕它们展开。主配置文件是 /etc/nginx/nginx.conf,它负责加载其他配置;站点配置文件通常放在 /etc/nginx/sites-available/ 目录下;要启用的站点则在 /etc/nginx/sites-enabled/ 目录下创建软链接。这种“available 存配置、enabled 启用配置”的结构,让你在维护多个站点时可以提前准备配置,等需要上线时再一键启用,非常灵活。
如果你此时直接在浏览器输入服务器 IP,看到的将是 nginx 默认的欢迎页面。这是 nginx 自带默认站点生效的结果。访问成功说明 nginx 已经对外提供服务,整个部署工具链是通的,接下来就可以替换默认站点,换成我们自己的 vue 项目。
3.3 第三步:上传 dist 并在 nginx 里配置站点
先把第一步构建好的 dist 目录上传到服务器。常用的方式有两种,第一种是 scp,简单直接。
scp -r dist/* 用户名@你的服务器IP:/var/www/项目名/第二种是 rsync,增量同步更高效,适合文件多、经常更新的场景。
rsync -av --delete dist/ 用户名@你的服务器IP:/var/www/项目名/我个人更推荐 rsync,因为 --delete 参数能让服务器上多余的文件自动清理掉,确保线上文件和本地构建产物完全一致。注意目录路径的选择:我习惯把站点文件放在 /var/www/ 下面,以项目名作为子目录。你也可以按自己的习惯放在 /home/用户名/sites/ 下,关键是 nginx 配置里的 root 路径要和文件实际存放的位置完全一致。
上传命令里的 /var/www/项目名/ 需要你先在服务器上创建好目录。可以执行:
sudo mkdir -p /var/www/项目名创建完成后,接下来是核心环节:编写 nginx 站点配置。用文本编辑器新建一个配置文件:
sudo vim /etc/nginx/sites-available/vue-project.conf然后把下面的配置内容写进去。这个配置是我用过的最小可用版本,每一步都有明确目的。
server { listen 80; server_name yourdomain.com; root /var/www/项目名; index index.html; location / { try_files $uri $uri/ /index.html; } 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; } }我来逐行解释这个配置的含义。listen 80 表示监听 80 端口,这是 HTTP 默认端口。server_name 填你的域名;如果没有域名,可以直接填服务器 IP,效果一样。root 指向 dist 文件上传的目录,nginx 会从这里读取静态文件。index index.html 指定目录访问时默认加载的入口文件。
location / 块是所有请求的默认归宿。try_files $uri $uri/ /index.html 这一行是 history 路由模式能否正常刷新的关键。它的逻辑是:先按请求路径找真实文件,比如访问 /favicon.ico 时找到就返回;再按目录找,比如访问 /about 时看看有没有 about 目录;如果都找不到,就回退返回 index.html,把请求交给前端路由去处理。
location /api/ 块是反向代理的配置。前端项目通常会向后端发请求,如果后端服务跑在同一台服务器的 8080 端口,那么这个配置会把所有以 /api/ 开头的请求转发到后端服务。proxy_set_header 几行是为了让后端服务能拿到真实的客户端信息,一般建议保留。
写完配置后,保存文件,然后建立软链接启用这个站点配置,并移除默认站点。
sudo ln -s /etc/nginx/sites-available/vue-project.conf /etc/nginx/sites-enabled/ sudo rm /etc/nginx/sites-enabled/default然后检查配置语法,这一步千万不能省。
sudo nginx -t如果输出中出现 syntax is ok 和 test is successful,说明配置语法没问题,可以放心重新加载。如果出现错误,nginx 会提示具体的行号,根据提示修改后重新检查。
最后重新加载 nginx,让新配置生效:
sudo systemctl reload nginx到这里,三步部署已经全部完成。你在浏览器里输入域名或服务器 IP,应该就能看到 vue 项目正常渲染。为了确认服务端确实返回了页面,可以执行 curl -I yourdomain.com,返回的 HTTP 状态码如果是 200,说明站点对外访问正常。
4. 部署后常见维护与排查实录
4.1 刷新页面 404,问题通常出在 try_files
最大概率出现的坑就是前面反复提到的 history 模式刷新 404。你在浏览器里访问首页没问题,点击内部路由跳转也没问题,但只要刷新一下当前的路由页面,比如刷新 /user/list,就会报 404 Not Found。
原因我已经解释过:浏览器把 /user/list 当作真实路径发给 nginx,nginx 在 root 目录下找不到这个名字的文件,就返回了 404。解决办法就是确认配置里有 try_files $uri $uri/ /index.html; 这一行。配置文件改完后,执行 nginx -t 检查语法,再 reload nginx,刷新问题就会消失。
这个坑我几乎每次给团队做部署培训都会遇到,而且很多人一开始根本想不到是 nginx 配置的问题,以为是 vue-router 代码写错了。如果你也遇到刷新 404,先不要改代码,回到 nginx 配置里找 try_files。
4.2 页面白屏,控制台报各种资源加载失败
白屏问题一般发生在部署初期,页面打开后一片空白,打开浏览器开发者工具,Console 里会出现一堆资源的 404 错误。常见原因有两个:一是 index.html 里引用的 js、css 路径缺失,二是构建时 base 配置没设对导致资源路径以绝对路径开头。
如果你的项目部署在域名的根目录,ajax 不需要特殊处理。但如果你把项目放在二级目录下,比如 http://example.com/admin/,就必须在 vite.config.js 里把 base 设置为相对路径或者 /admin/。路径配置不对,index.html 引用的资源路径就会指向错误的地址,nginx 自然找不到。解决方法是重新构建,或在 nginx 配置里调整 root 路径的层级,让资源请求能正确映射到 dist 目录下的 assets 文件。
另外还可能是 dist 上传不完整,比如你上传时漏掉了 assets 目录。用 du -sh /var/www/项目名 和本地 du -sh dist 对比一下体积,能快速确认文件是否传齐。
4.3 接口请求 502 或 504,问题往往不在 nginx
页面能打开但数据加载不出来,打开控制台发现 /api/ 开头的请求返回 502 Bad Gateway。502 表示 nginx 无法从后端服务获取有效响应,最常见的解释是后端服务根本没启动,或者没有监听在你在 proxy_pass 里指定的端口上。
先确认后端进程是否存活。如果后端是 node 服务,执行 ss -tlnp | grep 8080 看看端口有没有被监听。如果服务没起来,就回到后端启动步骤排查;如果端口在监听但仍返回 502,多半是后端服务跑了但没绑定在 127.0.0.1 上。有些后端框架默认只监听 IPv6 的 ::1,nginx 通过 127.0.0.1 访问就会失败。
504 Gateway Timeout 则是请求超时。如果后端接口本身耗时长,比如跑复杂报表或调用外部服务,可能在默认 60 秒内没响应。可以在 nginx 配置里增加 proxy_read_timeout 和 proxy_connect_timeout 来延长等待时间,但不能无限拉长,更根治的办法是优化后端接口的响应速度。
4.4 403 Forbidden,权限问题的一次典型排查
nginx 返回 403 通常是因为它对请求的文件没有读权限,或者目录索引被禁止。ubuntu 上 nginx 的工作进程一般以 www-data 用户运行,如果 dist 文件的所有者是 root,并且权限是 700,www-data 用户根本进不去目录,自然无法读取文件。
遇到 403,第一步看日志。/var/log/nginx/error.log 会明确告诉你 permission denied 出现在哪个路径。然后根据日志提示修复权限:
sudo chown -R www-data:www-data /var/www/项目名 sudo chmod -R 755 /var/www/项目名chown 把目录所有权交给 www-data,chmod 给目录设置 755 权限,让用户可以读取和执行。这一步操作完,再刷新一般就能正常访问。需要提醒的是,如果你用 ftp 或用 root 上传文件,上传完成后记得顺手执行一次权限修正,避免下次再踩同一个坑。
4.5 修改代码后线上页面不变,多半是缓存惹的祸
部署新版本后,用户浏览器里看到的还是旧页面,这种问题也非常常见。核心原因是浏览器和 nginx 都会对静态资源做缓存。vue 脚手架默认打包时,文件名会带上内容哈希,比如 app.a1b2c3.js。文件内容变了,哈希也会变,理论上浏览器会请求新资源,但入口 index.html 本身经常被浏览器缓存,导致页面还在引用旧文件的路径。
排查时,你可以用 curl 看一下服务器返回的 index.html 内容是否已经更新:
curl -s http://yourdomain.com/ | grep -o 'app\.[a-z0-9]*\.js'如果返回的确实是新文件名,说明服务器没问题,是浏览器缓存。这时可以强制刷新试一下。如果想让发布更顺滑,可以做两件事:一是在 nginx 配置里给 index.html 设置 no-cache 响应头,让浏览器每次重新验证这个文件;二是给带哈希的 js、css 设置较长的 expires 时间,充分利用缓存减少加载时间。这两条配置在后续执行中我也会附上,属于进阶优化,但很值得提前配置好。
4.6 配置文件常见语法错误自查
新手手写 nginx 配置时经常会漏分号、多括号。nginx -t 就是帮你找出这些问题的工具。输出提示 unexpected end of file 时,大多数人都是少了闭合大括号;提示 unknown directive 时,一般是上一行末尾漏了分号。我把两个高频错误点列出来,你可以对照自查:
- server 块、location 块以
{开始,必须以}结束,每一个{都需要对应一个}。 - 每行指令结尾必须带分号,否则 nginx 会把下一行指令名当作参数解析,直接报错。
- listen、server_name、root、index、try_files 这些指令都是分号结尾的。
改完配置后,我建议养成“三步检查”习惯:先 nginx -t 检查语法,再 systemctl reload nginx 加载配置,最后 curl 一下域名确认返回 200。三步全过,才算真的发布成功。
5. 从一次部署到长期省心:脚本、自检清单与我的体会
5.1 把重复流程固化成部署脚本
三次部署之后你就会发现,整个流程里最耗时的是输入重复命令、担心漏步骤。这时候最好的朋友就是脚本。把构建和发布流程写成一个本地执行的 shell 脚本,每次只需要跑一条命令就能完成发布。
一个最小可用的 deploy.sh 大概长这样:
#!/bin/bash set -e echo "开始构建..." npm run build echo "上传 dist 到服务器..." rsync -av --delete dist/ 用户名@你的服务器IP:/var/www/项目名/ echo "重新加载 nginx 配置..." ssh 用户名@你的服务器IP "sudo nginx -t && sudo systemctl reload nginx" echo "发布完成,当前可用版本已上线"set -e 的作用是脚本中任意命令失败就立即终止,避免明明构建失败了却继续上传旧版本。加了这行之后,你不需要在终端前一直盯着输出,构建失败会有明确提示,不会出现“版本没更新但你还不知道”的尴尬。
脚本执行前别忘了给权限:chmod +x deploy.sh。之后每次发布只需要 ./deploy.sh 一条命令。如果你想要更稳妥,还可以在脚本里加一个远程 curl 检查,确认线上版本号已经更新。这一套下来,发布动作就从手动流水线变成了自动化指令。
5.2 我自己的上线前自检清单
经验贴里最值钱的部分往往是那些“提醒自己别犯错”的清单。我习惯在每次部署前按顺序过一遍下面这几项,全过之后再宣布上线:
| 检查项 | 检查方法或标准 |
|---|---|
| 构建是否成功 | 本地执行 npm run build 退出码为 0,dist 目录存在 |
| 环境变量是否生效 | grep 接口域名 检查 dist 内 js 文件,确认没有本地地址残留 |
| 文件是否完整上传 | rsync 日志无异常,服务器 dist 体积与本地基本一致 |
| nginx 配置语法 | nginx -t 返回 syntax is ok |
| nginx 是否重新加载 | systemctl reload nginx 执行成功 |
| 线上首页是否 200 | curl -I http://域名 返回 200 |
| 页面刷新是否正常 | 手动刷新一个 history 路由页面,确认不 404 |
| 接口是否正常返回 | 浏览器里确认接口请求状态码和返回数据 |
这个清单不是给别人看的,是给自己兜底的。哪怕只是改了一行文案,我也不会跳过全部流程,因为我踩过太多次“改动很小所以不检查,结果环境变量被覆盖导致发布事故”的坑。把检查成本降到最低的才是好流程,脚本负责执行,清单负责兜底,两个配合,心里踏实。
5.3 我个人在实际操作中的体会是
部署这条路,刚开始会觉得难,又是服务器又是 nginx 又是端口权限,脑子里一团浆糊。但拆成“构建、装服务、传文件配站点”这三步以后,每一步都变得可以被验证:构建看 dist 目录里的 index.html 是否存在,装服务看 systemctl status 是否为 running,上传配站点看 curl 是否 200。用可验证的结果去驱动操作,理解自然就上来了。
最后再分享一个小技巧:遇到任何部署问题,先去看 nginx 的错误日志,路径在 /var/log/nginx/error.log。这里记录着 90% 以上问题的根因,比你盲目改配置高效得多。你可以在部署期间的终端里执行 tail -f /var/log/nginx/error.log 盯着日志,一边操作一边观察,很多问题会当场原形毕露。先跑通最简单的三步,再慢慢往里面加 HTTPS、CDN、自动化部署这些进阶能力,你会发现整个部署体系其实没有想象中那么复杂。