ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

前后端分离项目云服务器部署全流程:Nginx、PM2与HTTPS实践

前后端分离项目云服务器部署全流程:Nginx、PM2与HTTPS实践 最近我把自研的一个前后端分离项目完整部署到了云服务器上从域名解析、环境搭建、前端构建到后端守护进程、反向代理、HTTPS证书整个流程走了一遍。这篇文章先把我的完整“前后端部署流程”梳理出来再把我踩过的坑和排查思路一并写清楚。如果你正准备把自己的项目从本地搬到服务器或者部署过几次但总觉得哪里没弄明白这篇文章可以帮你省掉不少试错时间。我默认的场景是前端是Vue或者React这类单页应用后端是Node.jsExpress/Koa/NestJS均可数据库用MySQL或者PostgreSQL服务器是Linux以Ubuntu为例。如果你的技术栈是Java Spring Boot、Python Django这类思路也一样具体命令替换成对应生态的即可。1. 先理清整体部署思路到底要部署什么很多人一上来就搜“前后端部署流程”搜完还是一头雾水因为网上的教程要么只讲前端要么只讲后端很少有把一条完整链路讲清楚的。所以我先花点篇幅把这件事拆开。1.1 前后端分离项目部署的本质前后端分离之后所谓“部署”其实包含三个独立的东西前端产物一堆静态文件HTML、JS、CSS、图片。它们不需要“运行”只需要被Nginx这类Web服务器托管用户访问时能下载下来就行。后端服务一个持续运行的进程监听某个端口比如3000处理业务逻辑、读写数据库、返回JSON。反向代理层Nginx站在最前面把用户的请求分流——请求HTML/JS就去找前端静态文件请求/api/xxx就转发给后端进程。你可以把整件事类比成开一家餐厅前端是菜单和门面后端是后厨Nginx是那个站在门口帮你带路的服务员。用户看到的永远是门面但真正干活的是后厨。服务员要清楚什么单子往哪送这就是反向代理的职责。理解了这三层关系后面每一步你都会知道自己在干什么而不是照着命令盲敲。1.2 部署前必须确定的三张清单动手部署前我会建议你先回答这几个问题并用一张表格列清楚否则中途很容易乱项目你需要确认的内容我的建议值服务器配置、系统版本、所在区域、带宽2核4G起步Ubuntu 22.04域名是否已备案、解析是否到位提前完成备案A记录指向服务器IP运行环境Node版本、Nginx版本、数据库版本Node 18、Nginx 1.22前端构建构建命令、产物目录、环境变量文件pnpm build产物在dist/后端部署启动命令、监听端口、依赖安装方式pm2 start监听3000数据库建库语句、迁移脚本、初始数据手动或脚本导入这几项里最容易被低估的是域名备案。如果你用的是境内服务器域名不备案的话80/443端口根本没法对外提供服务到时候配好了Nginx也访问不了排查半天发现卡在备案上非常耽误时间。建议部署前至少两周把备案提交掉。1.3 我的惯用技术栈与选型理由我这次自研项目用的是前端Vue 3 后端NestJS数据库是PostgreSQL部署时选型如下Nginx做静态托管和反向代理。选它不是因为花哨而是因为它稳定、配置直观、资料多。你搜任何一句报错都能找到解决方案。PM2Node后端进程守护工具。它解决的核心问题是“后端挂了怎么办”——PM2会让进程崩溃后自动重启还能保存日志、设置开机自启。certbot自动申请和续期HTTPS证书避免手动去证书平台下载、上传证书那些琐碎操作。这套组合的好处是每一个工具都只干一件事但都干得很彻底。后面我会逐个讲它们的实际操作。2. 服务器环境准备与基础配置部署第一步是让服务器处于“待命状态”。这一步看着基础但很多人都在这上面栽跟头。我见过有朋友把Node装错版本导致前端构建报错也有朋友在安全组里忘了放行端口导致页面一直打不开。2.1 服务器初始化与登录我习惯拿到服务器后先做两件事更新系统、创建普通用户。# 先用root登录服务器 ssh root你的服务器IP # 更新系统软件源和已装软件 apt update apt upgrade -y # 创建用于部署的普通用户不建议长期用root跑服务 adduser deploy usermod -aG sudo deploy这里有个经验别用root直接部署。项目跑起来之后如果被入侵root权限意味着攻击者直接拿到整台机器。用deploy用户部署即使某个服务被攻破影响面也小很多。而且以后你维护多个项目时用普通用户操作的心理负担也小一些。登录方式我建议直接用SSH密钥不要用密码登录。在本地生成密钥对然后把公钥写到服务器的authorized_keys文件里。这样既不用记密码也安全不少。完事后可以修改/etc/ssh/sshd_config将PasswordAuthentication设为no彻底禁用密码登录。2.2 安装Node.js、Nginx与PM2我装Node从来不用apt直接装因为apt仓库里的Node版本太老容易和前端构建工具不兼容。推荐用nvm或者直接装官方二进制。这里给个用nvm的例子# 安装nvm curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash # 重新加载shell配置 source ~/.bashrc # 安装Node 18 LTS并设为默认 nvm install 18 nvm use 18 nvm alias default 18 # 验证版本 node -v npm -v为什么强调Node版本前端构建工具Vite、Webpack对Node版本有硬性要求太高太低都可能报奇怪的错。我见过Error: error:0308010C:digital envelope routines::unsupported这个报错最后排查下来就是Node版本和OpenSSL的兼容性问题换到Node 18之后就再没出现过。接下来安装Nginx和PM2apt install nginx -y systemctl enable nginx systemctl start nginx # PM2是Node生态的工具用npm全局安装 npm install -g pm2 # 设置PM2开机自启这一步非常重要 pm2 startup systemd pm2 savepm2 startup systemd这行命令很多人会忽略结果服务器重启后后端服务没起来然后一头雾水去排查。其实PM2完全支持开机自启只需要在安装后执行一次上面两条命令。2.3 目录规划与验证我习惯在服务器上建立固定的目录结构/home/deploy/ ├── frontend/ # 存放前端源码与构建脚本 ├── backend/ # 存放后端源码 ├── logs/ # 项目日志和Nginx日志统一放这里 └── backup/ # 数据库备份、构建产物备份静态文件本身会放到/var/www/项目名/因为这是Nginx默认习惯的目录。源码放在/home/deploy下面构建完把产物拷到/var/www保持源码和部署产物分离避免Nginx误读到乱七八糟的文件。环境准备做完之后验证一下浏览器直接访问http://服务器IP如果能看到Nginx默认欢迎页说明本机环境就绪了。如果看不到优先检查云控制台的安全组是否放行了80端口。3. 前端构建与部署实操前端部署的本质是“构建出静态文件然后让Nginx能读到这些文件”。这句话听起来简单实际操作中至少有五个环节值得注意。3.1 多环境配置的思路前后端分离项目里前端代码要请求后端接口但接口地址在不同环境里不一样本地开发是http://localhost:3000生产环境是https://api.你的域名.com。如果你把接口地址写死在代码里那就完蛋了——每次换环境都要改代码重新构建。正确做法是用环境变量文件区分# .env.production VITE_API_BASE_URLhttps://api.你的域名.com/apiVue或者React项目里构建工具会自动读取对应的环境变量。代码里统一通过import.meta.env.VITE_API_BASE_URLVite或process.env.REACT_APP_API_BASE_URLCreate React App来引用。这里分享一个经验不要把/api这个前缀放在两种地方。我的习惯是VITE_API_BASE_URL里带上/api所有请求都走/api/xxx然后Nginx负责把/api开头的请求转发给后端后端再统一处理业务路径。这个约定贯穿前端、Nginx、后端三层越统一越省心。3.2 依赖安装、构建与产物检查构建命令一般长这样# 进入前端源码目录 cd /home/deploy/frontend # 安装依赖我习惯用pnpm速度快 pnpm install # 构建生产产物 pnpm build构建完一定要看一眼产物目录ls -la dist/重点确认几个事index.html是否存在JS/CSS文件是否打包生成了有没有明显异常大的文件超过1MB的chunk在白屏优化时要重点处理但不影响部署如果你发现构建完的文件里接口地址还是localhost赶紧回去查.env.production文件是不是没建对。这个错我犯过不止一次本地构建覆盖了生产环境变量文件或者环境变量文件没有提交到服务器上。请记住环境变量文件不参与构建工具的处理时不会报错但接口全打歪。3.3 把构建产物发布到Web目录我采用一个很实用的做法在服务器上做一个当前版本软链接方便回滚。# 先清理旧文件再拷贝新产物 rm -rf /var/www/项目名 mkdir -p /var/www/项目名 cp -r /home/deploy/frontend/dist/* /var/www/项目名/ # 给前端源码目录赋予Nginx用户可读权限 chown -R www-data:www-data /var/www/项目名最后一个chown很多人会漏掉。Nginx默认以www-data用户运行如果目录权限不对用户访问时Nginx会返回403浏览器看到的就是“没有权限访问”。这一步还有一个隐藏坑单页应用的路由模式。Vue Router默认用history模式直接访问https://你的域名/about时Nginx会在文件系统里找/var/www/项目名/about这个文件找不到就返回404。解决办法是让Nginx在所有找不到文件的路径下都返回index.html由前端路由接管。这个配置我在第5章详细讲。4. 后端服务部署与守护后端部署比前端多一件重要的事前端是一次性构建后端是持续运行。所以你要解决的问题是怎么让一个Node进程在服务器上稳定跑着并且意外退出时能自动恢复。4.1 后端代码的准备与依赖安装先把后端源码放到服务器上cd /home/deploy/backend # 安装生产依赖NODE_ENV设成production时devDependencies不会装 NODE_ENVproduction npm install # 复制环境变量配置 cp .env.example .env # 然后编辑.env填入数据库连接串、密钥等.env文件必须手动配置因为里面有数据库密码和密钥不能提交到git仓库也不能在服务器上直接沿用示例文件。配置以下几项基本跑不掉配置项说明PORT后端监听端口建议3000DATABASE_URL数据库连接串格式类似postgres://用户:密码localhost:5432/数据库名JWT_SECRET登录令牌签名密钥务必随机生成一长串关于密钥建议用openssl rand -hex 32生成不要自己随便敲一串“123456”上去。生产环境一旦被暴力猜解后果很麻烦。4.2 PM2配置与启动后端启动我强烈建议用PM2的配置文件而不是裸启动。因为裸启动的Node进程在你关掉SSH终端时就没了而且崩溃了也没人管。写一个ecosystem.config.js放到后端目录module.exports { apps: [ { name: my-backend, script: dist/main.js, // NestJS构建后的入口文件 instances: 1, // 集群模式可以设成2但如果你有内存限制就先用1 exec_mode: fork, // fork模式最稳妥单个实例 max_memory_restart: 500M, // 内存超过500M自动重启防止内存泄漏拖垮整机 env: { NODE_ENV: production }, error_file: /home/deploy/logs/backend-error.log, out_file: /home/deploy/logs/backend-out.log, time: true // 日志里加时间戳 } ] };配置好之后启动后端cd /home/deploy/backend pm2 start ecosystem.config.js pm2 save pm2 logs my-backend # 实时看日志排错神器 pm2 status # 查看进程状态max_memory_restart这个参数建议一定要配。Node进程内存泄漏是常态不设限制的话可能把4G内存吃满整台服务器响应变慢连带着前端页面都打不开。设个阈值让PM2自动重启相当于给服务上了保险。4.3 数据库初始化与迁移后端代码里如果用了ORM或者迁移工具第一次部署时要先跑迁移否则后端一启动就报数据库表不存在。以NestJS TypeORM为例# 执行迁移脚本建立所有表结构 npm run migration:run # 如果需要初始数据比如管理员账号单独导入 npm run seed我踩过的坑是迁移脚本跑第一遍没成功报了个连接超时我直接跑第二遍结果提示“迁移已经执行过了”。这个情况是因为迁移工具会记录哪些迁移已执行一旦第一条写进记录表但实际只执行了一半后续就会报错。解决办法是进数据库把migrations表里对应的记录删掉再手动补上建表语句。建议你先拿小数据量的测试库跑通迁移再上生产库。数据库初始化完成后用命令验证一下后端进程是否正常curl http://127.0.0.1:3000/api/health如果返回JSON数据说明后端已经起来了。此时你可以用curl直接发包测几个接口比如注册、登录确认业务逻辑没大问题再进入下一层——Nginx。5. Nginx反向代理与HTTPS配置Nginx是整个部署流程里的“门面”也是我最想重点讲的一层。它同时承担三个职责托管前端静态文件、把API请求转发到后端、启用HTTPS。配置写不好前面所有步骤都白费。5.1 核心配置逐行拆解直接给一份我在生产环境实测可用的配置。假设你有一个域名example.com后端在127.0.0.1:3000server { listen 80; server_name example.com www.example.com; # 前端静态文件目录 root /var/www/项目名; index index.html; # 处理前端路由history模式 location / { try_files $uri $uri/ /index.html; } # API请求反向代理到后端 location /api/ { proxy_pass http://127.0.0.1:3000; 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; # 如果后端有WebSocket比如聊天功能去掉下面三行注释 # proxy_http_version 1.1; # proxy_set_header Upgrade $http_upgrade; # proxy_set_header Connection upgrade; } # 静态资源缓存JS/CSS/图片这些带hash的文件可以放心缓存 location ~* \.(js|css|png|jpg|jpeg|gif|svg|woff2?)$ { expires 30d; add_header Cache-Control public, immutable; } }逐行讲几个关键点try_files $uri $uri/ /index.html;这行就是解决Vue Router history模式404的。它先尝试找真实文件找不到就回退到index.html交给前端路由处理。location /api/的proxy_pass有个细节如果proxy_pass后面不带路径比如只写http://127.0.0.1:3000那么请求/api/user会原样转发成/api/user。如果写成proxy_pass http://127.0.0.1:3000/;那么/api/前缀会被剥掉后端收到的是/user。你要根据后端路由设计选一种并且后端代码里别混用两种风格。我习惯后端路由自带/api前缀所以proxy_pass不写路径保持一致。静态资源缓存设了30天但浏览器可能缓存住旧的JS文件导致前端页面更新后看不到变化所以前端构建工具生成的文件名必须带hash。Vite默认就是带hash的不用担心。配置写好后先测试再重载nginx -t systemctl reload nginxnginx -t相当于“预检查语法”如果有错误它会直接告诉你哪一行有问题这是改Nginx配置前必做的一步。直接重载的话一旦语法错误Nginx都起不来。5.2 用certbot自动配置HTTPSHTTPS一定要上。现在浏览器对HTTP站点标记“不安全”而且很多第三方登录回调、地理位置接口强制要求HTTPS。不用自己去证书平台折腾certbot一条命令就能搞定apt install certbot python3-certbot-nginx -y # 自动获取证书并修改Nginx配置 certbot --nginx -d example.com -d www.example.comcertbot会检查80端口能否访问你的域名从而验证你对域名的所有权然后自动给Nginx配置443端口的server块。证书有效期通常是90天certbot会在到期前自动续期# 先验证自动续期是否正常工作 certbot renew --dry-run只要你的Nginx配置没有手动改坏续期就是全自动的。我建议每月看一下服务器的cron日志确认续期任务在跑。certbot默认会加一条cron任务但如果你把服务器重装了这条任务就没了记得重新执行一次安装命令。5.3 进去之后顺手优化的三个细节HTTPS通了之后我一般还会做三件小事第一个强制HTTP跳转HTTPS。certbot生成的配置通常会保留80端口并重定向到443。如果你发现用户还能用HTTP访问也就是进入了不加密通道可以在80端口那个server块里加上return 301 https://$host$request_uri;强制跳转。第二个开启Gzip压缩。在http块里加上这段JS和CSS能小百分之六七十gzip on; gzip_types text/plain text/css application/javascript application/json image/svgxml;第三个给Nginx加访问日志。默认日志在/var/log/nginx/access.log建议保留因为排查用户问题、统计流量、看有没有人乱扫路径都靠它。6. 部署中踩过的坑与排查经验这个章节会写到我实操里真实的故障案例和排查思路也是我觉得最有参考价值的部分。部署和开发不一样开发时报错会直接告诉你文件和行号部署时报错往往只能看到一个504或者502问题藏在三层中间。你真正要学会的是“逐层排除”。6.1 常见故障速查表现象直接原因排查步骤浏览器访问白屏前端静态文件没有正确加载F12看Network找到加载失败的资源去/var/www比对文件是否存在页面404路由history模式没有回退到index.html检查try_files是否配置正确接口502后端没有启动或监听端口错误pm2 status看进程curl http://127.0.0.1:3000/api/health测本机接口504后端处理超时看后端日志是否有慢查询调大Nginx的proxy_read_timeout页面能开但接口打不开反向代理路径转发出错看Nginx错误日志/var/log/nginx/error.log比对proxy_pass路径规则HTTPS打不开HTTP能开443端口未放行或证书没配检查云安全组是否放行443检查certbot certificates浏览器一直访问旧版本静态资源缓存确认构建产物文件名是否带hash确认Cache-Control设置是否正确服务器重启后后端没起来PM2没有设置开机自启执行pm2 startup systemd和pm2 save这张表只能帮你定位到大概层真正的排查还是要靠日志。6.2 三个让我印象深刻的真实案例案例一接口404但所有配置看起来都没问题。后端路由是/api/userNginx的proxy_pass也正确为什么用户请求/api/user返回404最后我把请求用curl -v打了一遍发现Nginx转发给后端的URL变成了//user——原来前端代码里请求地址写的是/api//user多了一个斜杠。后端路由匹配不上。这个教训让我从此严格遵守“路径统一规范”前端、Nginx、后端三层任何人都不许自己加斜杠。案例二服务器内存被吃满后端虽然活着但响应极慢。排查发现Node进程内存已经涨到1.8G是数据库连接没释放导致的泄漏。后来我做了两件事第一在PM2里设max_memory_restart: 500M让进程超限自动重启第二给后端代码里的数据库连接池加上最大连接数和空闲超时时间。这个事后端稳定了很多。案例三新版本上线后用户还是能看到旧页面。因为静态资源缓存策略太激进我用了expires 30d但前端构建时没有给文件名加hash。结果用户浏览器的缓存里存了一个永远不会变的旧JS文件。解决办法是前端把输出文件名改成带内容hash的格式还要给index.html设置no-cache而带hash的JS/CSS则可以放心长缓存。这样既保证新版本能及时生效又能享受缓存的性能红利。6.3 部署时的备份与回滚建议部署本质上是一次“变更”任何变更都有失败的可能。所以我在每次部署前都会把当前的构建产物和数据库备份一次# 备份前端产物 cp -r /var/www/项目名 /home/deploy/backup/项目名-$(date %F) # 备份数据库 pg_dump -U 用户 -d 数据库名 /home/deploy/backup/db-$(date %F).sql有了备份就算发布后出了严重问题也能在五分钟内回滚到上一版把软链接指回旧版本目录再从数据库备份恢复数据。这个习惯帮我避免了至少两次事故希望你也能养成。7. 给下一个做部署的人的话部署这套流程走完我觉得最值得分享的不是某个命令或者某段配置而是排查时的思路。前中后端加上服务器一共四层每一层都有它自己的日志和验证方式。遇到问题不要慌先从最外层开始验证公网能不能通、浏览器Network里是什么状态码然后顺着请求链路一层一层往里看。状态码500多就看后端日志状态码400多大概率是Nginx配置或者跨域问题状态码3xx看有没有被重定向状态码200但页面不对就看前端构建和文件目录。我个人实际操作中的体会是部署没有太多玄学大部分问题都是环境细节和小习惯积累出来的。你把“统一路径风格、固定目录结构、保留日志备份、做可回滚的发布”这四件事落实到位部署就会变得非常顺滑。至少我自己现在再上新项目基本一遍过。希望这篇整理能让你少走几圈弯路去好好享受把作品发布到公网那一瞬间的快乐。
返回列表