1. 项目概述与环境准备
1.1 这套技术栈到底在做什么
先别急着敲代码,咱们花两分钟搞清楚这套组合拳的定位,后面会少走很多弯路。
Vue3 + Node.js + MongoDB 是目前个人博主、独立开发者和小型创业团队最常用的全栈组合之一。简单来说,Vue3负责浏览器端的界面渲染,Node.js负责服务端接口和业务逻辑,MongoDB负责数据存储。三个角色各司其职,而阿里的云服务器则负责让这台"三驾马车"能够7x24小时对外提供访问服务。
我见过太多人一上来就照着某个"一键部署脚本"咔咔往下走,结果前端构建产物不知道往哪放、Node进程在SSH断开之后就挂了、MongoDB没做鉴权和公网裸奔,最后部署出来的项目既不稳定又不安全。这个教程就是要把这些坑一个个提前填平。
适合谁来参考?三种人:一是刚学完前端三件套和Vue想尝试全栈的小白;二是本地开发跑得通但从未上过云服务器的程序员;三是需要快速交付Demo或内部工具、不想折腾K8s和Docker满汉全席的团队。
1.2 本机环境:Windows与macOS通用策略
部署的前半段是本地开发和联调,后半段才上阿里云。所以先把本机环境配好。
我需要先强调一个原则:版本锁定。Node.js版本太新或太旧都会带来一堆莫须有的问题,比如error installing 24.20.0: node.js v24.20.0 is not yet released or is not available这种报错,十有八九是版本管理器和下载源之间的同步没有跟上。因此,咱们不用最新版,而是选一个当前生态兼容性最好的长期支持版本,比如Node.js 20 LTS。
bash复制# Windows:直接去Node.js官网下载v20.x LTS的msi包
# macOS:建议用nvm管理,避免日后切换版本时权限问题
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
nvm install 20
nvm use 20
node -v
npm -v
安装完成后顺手把npm的镜像源切到国内,这一步能省下后面npm install的大量等待时间:
bash复制npm config set registry https://registry.npmmirror.com
MongoDB在Windows上安装比较简单,从官方或者国内镜像站下载msi安装包,安装时注意取消勾选"Install MongoDB Compass",Compass虽然是官方图形化工具,但后面我们用DBeaver统一管理就行,没必要再占一份内存。macOS用户用Homebrew更舒服:
bash复制brew tap mongodb/brew
brew install mongodb-community@7.0
brew services start mongodb-community
到这里,本机环境的基础已经搭好。有一个细节值得提:MongoDB安装失败的场景很常见,尤其在Windows上,多半是360、火绒这类安全软件拦截了Windows Service的注册。如果你也遇到mongodb安装失败,先退出安全软件,再以管理员身份运行安装程序,成功率会高出一大截。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 后端接口设计与MongoDB数据建模
2.1 用Express还是NestJS?从项目规模说起
Node.js生态里的服务端框架很多,但在Vue3 + MongoDB的组合中,最主流的选择是Express和NestJS。Express轻量、灵活、学习曲线平缓;NestJS则更像Java Spring的风格,有依赖注入、模块化、装饰器,适合业务复杂的大项目。
如果是个人项目、毕设、中小型管理后台,我强烈建议用Express。不是NestJS不好,而是它的TS装饰器、模块组织方式对新手来说理解成本太高,而且一个简单CRUD接口在NestJS里要建Controller、Service、Module三个文件,写着写着就容易陷入"结构焦虑"。反过来,Express一百行就能跑通一个完整的用户注册、登录、增删改查接口链路,先把业务跑起来比什么都重要。
举一个实际的项目目录结构,这是我在多个可运维项目中实战验证过的布局:
text复制server/
├── config/
│ └── db.js # 数据库连接配置
├── models/
│ └── userModel.js # 用户数据模型
├── routes/
│ └── userRoutes.js # 用户相关路由
├── middleware/
│ └── auth.js # JWT鉴权中间件
├── .env # 环境变量(不提交到Git)
├── .env.example # 环境变量示例(提交到Git)
├── package.json
└── server.js # 入口文件
为什么要强制把配置、模型、路由拆分成独立目录?因为项目上线之后,排查问题时如果能直接定位到"模型层的问题"或者"路由层的问题",效率完全不一样。一个把所有代码塞在一个文件里的项目,维护成本会随着业务体量指数级上升。
2.2 Mongoose建模:给文档数据立规矩
MongoDB本身是NoSQL,意味着它不强制表结构。但是如果在业务代码里不设立约束,三个月后你打开数据库,会发现同一条业务的数据字段五花八门,有的字段叫createTime,有的叫created_at,甚至有的根本没有这个字段。所以哪怕MongoDB不强制,我们也要用Mongoose这类ODM(对象文档映射工具)来给数据"立规矩"。
下面以最典型的用户模型为例:
javascript复制// models/userModel.js
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
username: {
type: String,
required: [true, '用户名不能为空'],
unique: true,
trim: true,
minlength: 3
},
email: {
type: String,
required: [true, '邮箱不能为空'],
unique: true,
lowercase: true,
match: [/^\S+@\S+\.\S+$/, '邮箱格式不正确']
},
passwordHash: {
type: String,
required: true,
select: false // 查询时默认不返回该字段,防止泄露
},
avatar: {
type: String,
default: ''
},
role: {
type: String,
enum: ['user', 'admin'],
default: 'user'
},
status: {
type: Number,
enum: [0, 1], // 0-禁用 1-正常
default: 1
}
}, {
timestamps: true // 自动维护 createdAt 和 updatedAt
});
module.exports = mongoose.model('User', userSchema);
有几个细节值得注意。select: false配合passwordHash字段使用,可以在查询时默认不返回密码哈希,从源头避免密码被意外输出到日志或接口响应里。timestamps: true自动维护的两个时间字段,在很多业务场景下都够用,没必要再手写一个createTime。
密码不能明文保存,这是底线中的底线。用bcryptjs做哈希,成本因子设为10,对个人项目来说安全性和性能平衡得比较好:
javascript复制const bcrypt = require('bcryptjs');
const saltRounds = 10;
async function hashPassword(plainPassword) {
return await bcrypt.hash(plainPassword, saltRounds);
}
async function verifyPassword(plainPassword, passwordHash) {
return await bcrypt.compare(plainPassword, passwordHash);
}
2.3 接口鉴权:JWT还是Session?
传统服务端开发习惯用Session,前端登录后拿到一个sessionId存在Cookie里。但前后端分离架构下,Vue3跑的静态页面和你Node.js接口可能不在同一个域名下,Cookie跨域处理会变得特别麻烦。所以在移动端和Web端共存的项目里,JWT(JSON Web Token)是更省心的方案。
JWT的核心理念是:服务器签发一个带签名和过期时间的Token给客户端,客户端每次请求时在Authorization头带上它,服务器只需要验证签名就可以确认用户身份,无需在服务端保存会话状态。好处是天然适合分布式,坏处是Token一旦签发在过期之前无法主动撤销,所以要把过期时间设短一点(比如2小时),配合前端刷新Token机制。
下面是一个标准JWT中间件:
javascript复制// middleware/auth.js
const jwt = require('jsonwebtoken');
module.exports = function auth(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ message: '未授权,缺少Token' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.userId = decoded.userId;
req.userRole = decoded.role;
next();
} catch (err) {
return res.status(401).json({ message: 'Token无效或已过期' });
}
};
JWT_SECRET一定要放在环境变量文件.env里,并且生产环境和开发环境用不同的密钥,绝对不能硬编码在代码里。
2.4 本地联调:先跑后端还是先跑前端?
我第一次做全栈项目时,憋着一股劲想一次性把前后端都写完再跑,结果报错的时候根本分不清是前端的问题还是后端的问题。后来我总结了一个更稳的节奏:先后端,后前端,最后整体联调。
后端启动之后,先用Postman或者Apifox把所有接口按"未授权场景"和"已授权场景"两组完整测试一遍。重点测三件事:参数校验是否返回了友好的错误信息、鉴权中间件是否拦截了没有Token的请求、数据库读写是否出现异常。接口确认没问题,再启动Vue3前端页面,进行页面级的联调。
后端入口最简单的样子如下:
javascript复制// server.js
require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const userRoutes = require('./routes/userRoutes');
const app = express();
const PORT = process.env.PORT || 3000;
// 中间件
app.use(cors());
app.use(express.json());
// 路由注册
app.use('/api/users', userRoutes);
// 根路由,方便部署后随时确认服务存活
app.get('/health', (req, res) => {
res.json({ status: 'ok', time: new Date().toISOString() });
});
// 连接数据库并启动服务
mongoose.connect(process.env.MONGODB_URI)
.then(() => {
console.log('MongoDB connected successfully');
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
})
.catch((err) => {
console.error('MongoDB connection failed:', err);
process.exit(1);
});
注意这里连接数据库之后才启动HTTP监听,这样能在日志里清楚地看到"数据库先连上,服务后启动"的顺序。如果反过来,可能前端请求一来,后端数据库还没连上,就会抛出一堆惊悚的未定义错误。
3. Vue3前端构建与API对接
3.1 为什么选Vite而不是Vue CLI
Vue3时代,官方推荐的构建工具已经从Vue CLI切换成了Vite。原因非常直观:Vite基于浏览器原生的ES Module进行开发调试,冷启动速度是秒级,热更新也是毫秒级,而Vue CLI在Webpack体系下,每次启动都要先完整打包整个项目,项目稍微大一点就是几十秒的等待。
创建一个Vue3 + Vite项目非常简单:
bash复制npm create vite@latest client -- --template vue
cd client
npm install
npm run dev
创建好之后,浏览器访问http://localhost:5173,你就能看到Vite的默认欢迎页。接下来的工作就是把默认页面清理掉,替换成自己的业务页面。
有一点需要提前说:Vite默认端口是5173,后端Express跑在3000端口。浏览器访问前端页面时,页面里的JS调用后端接口会跨域。开发阶段最简单的解决方案是在Vite配置里加一个代理:
javascript复制// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()],
server: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true
}
}
}
});
这样页面里可以直接用相对路径请求/api/users/register,Vite会把它转发到http://localhost:3000,跨域问题迎刃而解。更重要的是,开发环境和生产环境的代码不用来回改域名,部署时统一走Nginx反代就行。
3.2 组合式API(Composition API)核心用法
Vue3最具标志性的变化就是组合式API。Vue2时代我们写data()、methods、computed、watch是分开的四个选项,一个功能的代码会被拆散到不同的区域。Vue3的组合式API则是把一个业务功能相关的状态、逻辑、方法放在一起,代码聚合度更高。
以登录功能为例,核心代码长这样:
vue复制<template>
<div class="login-page">
<el-form :model="form" @submit.prevent="handleLogin">
<el-input v-model="form.username" placeholder="用户名" />
<el-input v-model="form.password" type="password" placeholder="密码" />
<el-button type="primary" :loading="loading" @click="handleLogin">
登录
</el-button>
</el-form>
</div>
</template>
<script setup>
import { reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
import { loginApi } from '@/api/user';
const router = useRouter();
const form = reactive({
username: '',
password: ''
});
const loading = ref(false);
const handleLogin = async () => {
if (!form.username || !form.password) {
ElMessage.warning('请输入用户名和密码');
return;
}
loading.value = true;
try {
const { token, userInfo } = await loginApi(form);
localStorage.setItem('token', token);
localStorage.setItem('userInfo', JSON.stringify(userInfo));
ElMessage.success('登录成功');
router.push('/dashboard');
} catch (error) {
ElMessage.error(error.response?.data?.message || '登录失败');
} finally {
loading.value = false;
}
};
</script>
<script setup>是Vue3的语法糖,它让顶层的变量和方法直接暴露给模板使用,少写了一大段return代码。reactive适合管理对象类型的响应式状态,ref适合管理基本类型。上面的代码里,form用reactive,而loading用ref,这是两个API最典型的使用场景。
3.3 封装Axios:统一处理Token和错误拦截
如果每个页面都自己写一遍axios.get、手动拼接Authorization头、再手动处理401跳转,那代码里将充满重复劳动。更好的做法是封装一个统一的请求模块:
javascript复制// src/api/request.js
import axios from 'axios';
import router from '@/router';
import { ElMessage } from 'element-plus';
const request = axios.create({
baseURL: '/api',
timeout: 10000
});
// 请求拦截:自动携带Token
request.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// 响应拦截:统一错误处理
request.interceptors.response.use(
(response) => response.data,
(error) => {
if (error.response) {
if (error.response.status === 401) {
ElMessage.error('登录已过期,请重新登录');
localStorage.removeItem('token');
router.push('/login');
} else {
ElMessage.error(error.response.data?.message || '请求失败');
}
} else {
ElMessage.error('网络异常,请检查连接');
}
return Promise.reject(error);
}
);
export default request;
这样封装之后,业务代码里只需要写接口函数:
javascript复制// src/api/user.js
import request from './request';
export const loginApi = (data) => request.post('/users/login', data);
export const registerApi = (data) => request.post('/users/register', data);
export const getUserInfoApi = () => request.get('/users/info');
这样做的好处是,以后如果Token从localStorage改成Cookie存储,只需要改请求拦截器那几行代码,所有用到接口的地方自动生效。
3.4 路由守卫与页面权限控制
Vue3项目里,绝大多数后台管理系统都要求登录后才能访问。实现这个需求最简单的办法是全局前置守卫:
javascript复制// router/index.js
import { createRouter, createWebHistory } from 'vue-router';
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/login', component: () => import('@/views/Login.vue') },
{ path: '/dashboard', component: () => import('@/views/Dashboard.vue'), meta: { requiresAuth: true } },
{ path: '/', redirect: '/dashboard' }
]
});
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token');
if (to.meta.requiresAuth && !token) {
next('/login');
} else {
next();
}
});
这里有个容易被忽略的坑:路由守卫里判断的是"有没有token",而不是"token是否有效"。也就是说如果用户手动把token改成任意的字符串,守卫也会放行。不过没关系,前面封装的Axios响应拦截器会在接口返回401时自动把用户踢回登录页,这前后两道防线配合,权限控制基本就稳了。
页面级权限之外,按钮级权限建议用自定义指令控制。比如v-permission="'admin'"这种指令,可以根据用户的role字段决定按钮是否渲染。这属于锦上添花的功能,项目早期可以不加,等业务复杂了再补。
4. 本地全链路联调与常见问题排查
4.1 从零到一跑通全链路的检查清单
在正式上阿里云之前,一定要在本地把整条链路跑通。我整理了一份个人习惯的联调检查清单,建议按顺序打勾:
- MongoDB服务已在运行,通过
mongosh或DBeaver能正常连接 - 后端服务启动后日志输出"MongoDB connected successfully"
- 用Apifox或Postman测试注册接口,数据库里能看到新增的用户记录,密码字段是bcrypt哈希值而非明文
- 登录接口返回Token,用该Token访问需要鉴权的接口,返回200;不携带Token访问同一接口,返回401
- Vue前端启动后,登录页面能正常提交表单,登录成功后跳转到Dashboard
- 页面能正确显示后端返回的数据,刷新页面后登录状态依然保持(Token存在localStorage里)
这个清单看着简单,但每一步都可能在某个细节卡住。尤其第3步,MongoDB的驱动或者Mongoose版本不对,会导致连不上数据库,报错信息往往又不太直观。
4.2 本地联调高频报错集锦
我在多个项目里收集到的高频报错,基本可以归纳为三类:
第一类是端口占用。Windows下最常见,Express的3000端口被别的程序占用了,启动时直接报EADDRINUSE。排查方法是命令行执行netstat -ano | findstr :3000,找出占用进程的PID,然后根据需要结束该进程,或者修改Express的监听端口。
第二类是跨域问题。你在浏览器上看到接口请求发出去了,但Network里显示CORS错误。开发阶段用Vite代理基本可以避免,但如果页面是用file://协议直接打开的,代理不生效,就一定要通过npm run dev启动前端再访问。
第三类是Mongoose连接问题。比如MongooseServerSelectionError,原因多半是MongoDB服务没启动,或者连接字符串写错了。MongoDB默认没有开启鉴权时连接字符串是mongodb://localhost:27017/项目名,如果开启了鉴权,则需要带上用户名密码,格式为mongodb://用户名:密码@localhost:27017/项目名?authSource=admin。我在本地调试时通常不开鉴权,但到了阿里云服务器上,这一项必须开启,后面部署章节会详细说。
4.3 一个真实的排查案例
有一次一个朋友找我帮忙调试,说注册接口一直报500。我在他电脑上看了很久,注册逻辑没问题,数据库也连接正常,最后发现是passwordHash字段在用户注册时存进去的哈希长度超过了数据库字段限制——等等,MongoDB其实是动态文档结构,没有字段长度限制。回去仔细一想,实际原因是bcryptjs在某个Node版本下出现了兼容问题,哈希函数返回了undefined,然后存进MongoDB时因为缺少required字段而报错。
这个案例的教训是:当你发现逻辑完全没问题但接口就是报错时,优先考虑依赖版本兼容性和运行环境差异,而不是反复盯着业务代码看。处理方式也很简单,把Node.js版本回退到项目锁定的LTS版本,再把bcryptjs升级到最新稳定版,问题迎刃而解。
5. 阿里云部署全流程实操
5.1 服务器选型与基础安全配置
本地一切正常之后,终于到了上云环节。阿里云的选购入口很多人第一次找不太到,登录控制台后搜索"云服务器ECS",然后进入创建实例页面。如果是个人项目、低并发场景,2核4G的ECS实例就够了,地域选离你最近的城市,带宽按量计费或者固定5Mbps左右都可以。操作系统建议选择Alibaba Cloud Linux或Ubuntu 22.04 LTS,两者对Node.js和MongoDB的兼容性都很好。
主机购买之后,第一件事就是改安全组规则。阿里云安全组默认只开放22、80、443等少数端口。开发阶段你需要手动添加入方向规则,把3000端口(Node服务)和27017端口(MongoDB)临时放行。但这里有一个安全原则:一旦部署完成,27017端口应该永远不对公网开放,MongoDB默认监听127.0.0.1,而不是0.0.0.0。Node服务也不建议走3000端口对外暴露,而是让Nginx监听80/443,再反向代理到本机的3000端口的Node服务。
5.2 服务器端Node.js安装与版本管理
服务器上装Node.js和本地稍微不同。我推荐使用nvm,因为以后切换Node版本不用重新下载安装包,而且可以避免一些系统级目录权限打架的问题。
bash复制# 以Ubuntu为例
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
source ~/.bashrc
nvm install 20
nvm alias default 20
这里要注意一个网络问题:某些环境下服务器连接GitHub可能不稳定,导致nvm安装脚本下载失败。如果遇到这种情况,可以手动把nvm的脚本下载下来,或者直接改成用apt安装NodeSource仓库里的Node:
bash复制curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs
两种方式任选其一,目的都是拿到一个可用的Node 20环境。装完之后执行node -v && npm -v确认输出正常。
5.3 MongoDB安装与鉴权配置
MongoDB在阿里云Ubuntu上的安装,官网给出的流程最长,但最稳妥。老规矩,逐个命令执行:
bash复制# 导入MongoDB官方GPG公钥
curl -fsSL https://www.mongodb.org/static/pgp/server-7.0.asc | sudo gpg -o /usr/share/keyrings/mongodb-server-7.0.gpg --dearmor
# 添加MongoDB源
echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-7.0.gpg ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/7.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-7.0.list
# 更新并安装
sudo apt-get update
sudo apt-get install -y mongodb-org
安装完成后,MongoDB默认会注册为systemd服务。执行以下命令启动并设置开机自启:
bash复制sudo systemctl start mongod
sudo systemctl enable mongod
接下来是最关键的一步:创建管理员账号并开启鉴权。先进入MongoDB Shell:
bash复制mongosh
在Shell里依次执行:
javascript复制use admin
db.createUser({
user: "admin",
pwd: "你的高强度密码",
roles: [{ role: "root", db: "admin" }]
})
然后退出,编辑MongoDB的配置文件/etc/mongod.conf,把net.bindIp改成127.0.0.1(默认值通常已经是这样,千万别改成0.0.0.0),并把security.authorization设置为enabled:
yaml复制security:
authorization: enabled
net:
port: 27017
bindIp: 127.0.0.1
保存退出后重启MongoDB:
bash复制sudo systemctl restart mongod
mongosh "mongodb://admin:你的密码@127.0.0.1:27017/admin"
能正常连上就说明鉴权生效了。这个密码要放到后端项目的.env文件里,不要在任何日志中打印出来。
5.4 后端项目上传与进程守护
代码上传服务器的方案有很多,最简单的可以装个sftp工具直接拖文件,也可以使用Git拉取仓库。个人推荐用Git,这样以后更新代码只需要git pull,还能保留版本历史。如果代码仓库是私有的,建议在服务器上配置SSH密钥,然后把公钥添加到Git平台的Deploy Keys里。
代码在服务器上放好之后,进入后端项目目录,执行:
bash复制npm ci --omit=dev
npm ci和npm install最大的区别是,它严格按照package-lock.json锁定版本安装,不会偷偷升级某个依赖,很适合部署环境。--omit=dev参数可以跳过开发依赖,让安装过程更快、更干净。
接下来创建.env文件:
bash复制# .env
PORT=3000
MONGODB_URI=mongodb://admin:你的密码@127.0.0.1:27017/项目名?authSource=admin
JWT_SECRET=随便一长串随机字符串
NODE_ENV=production
然后问题来了:如果直接node server.js,一旦你关闭SSH终端,Node进程就会被系统挂断。解决办法是使用进程守护工具,业界用得最广泛的当属pm2。
bash复制npm install -g pm2
pm2 start server.js --name my-project-server
pm2 save
pm2 startup
pm2 startup会生成一条开机自启命令,执行它即可确保服务器重启后pm2自动启动并拉起你的Node进程。执行到这一步,你的后端服务已经跑在3000端口了,可以用pm2 status查看运行状态,用pm2 logs查看日志输出。
5.5 Nginx反向代理与前端静态文件部署
前端Vue项目的部署,核心是构建出静态文件,然后交给Nginx托管。先在本地或服务器上执行:
bash复制cd client
npm ci
npm run build
构建完成后会产生一个dist目录,里面是纯静态的HTML、CSS和JS文件。把这个dist目录上传到服务器的/var/www/my-project目录即可。
安装Nginx并创建站点配置:
bash复制sudo apt-get install -y nginx
编辑/etc/nginx/sites-available/my-project:
nginx复制server {
listen 80;
server_name your-domain.com; # 换成你的域名或服务器IP
# 前端静态文件
root /var/www/my-project;
index index.html;
# 前端路由history模式下刷新404的问题
location / {
try_files $uri $uri/ /index.html;
}
# 反向代理到Node.js服务
location /api/ {
proxy_pass http://127.0.0.1:3000/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
创建软链接并重载Nginx配置:
bash复制sudo ln -s /etc/nginx/sites-available/my-project /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
这里有个容易踩的坑:proxy_pass http://127.0.0.1:3000/api/;末尾带的/api/是有讲究的。如果后端路由本身已经包含/api前缀,那么这里直接写http://127.0.0.1:3000就行;如果后端路由不带/api前缀,那么这里写http://127.0.0.1:3000/api/会将请求路径中的/api/部分替换掉。两种写法我都用过,关键是前后端保持一致,不然会出现404。
5.6 HTTPS证书配置与免费续期
HTTP裸奔在现在的互联网环境下很不安全,尤其是涉及到密码、Token等敏感信息的接口。好在阿里云提供免费的SSL证书,而且可以无限期续期,只是每三个月要手动或者自动续一次。
操作路径是:控制台搜索"数字证书管理服务",进入"SSL证书"页面,选择"免费证书",申请一张单域名证书。填写你的域名,等大约几分钟到几小时,审核通过后签发,就可以在证书列表里点击"下载",选择Nginx格式拿到证书文件。
把证书文件(.pem和.key)上传到服务器,然后修改Nginx配置:
nginx复制server {
listen 443 ssl;
server_name your-domain.com;
ssl_certificate /etc/nginx/ssl/your-domain.pem;
ssl_certificate_key /etc/nginx/ssl/your-domain.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
root /var/www/my-project;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://127.0.0.1:3000/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
server {
listen 80;
server_name your-domain.com;
return 301 https://$host$request_uri;
}
重载Nginx后,访问https://your-domain.com就能看到带小锁的地址栏了。
关于免费证书的"续期"问题,网上一搜一大把相关的讨论。本质来说,阿里云免费证书有效期是3个月,到期前需要在控制台重新申请并下载新证书,再把新证书替换到服务器上。手动操作不算复杂,但为了省心,可以把certbot也装一份,后面选择用Let's Encrypt方式做自动续期,或者写一个简单的cron脚本定时提醒自己去控制台续期。二者选一即可。
5.7 安全加固:防火墙、SSH与数据库
部署完成后,第一件事是回阿里云控制台,把安全组里之前临时开放的3000和27017端口全部删除,只保留22、80、443。MongoDB只允许本机访问,Node服务只允许通过Nginx反代访问,这是底线。
SSH登录方面,强烈建议禁用root密码登录,改用密钥登录。操作方法是把本地的公钥添加到服务器的~/.ssh/authorized_keys文件里,然后修改/etc/ssh/sshd_config:
bash复制PermitRootLogin prohibit-password
PasswordAuthentication no
改完执行sudo systemctl restart sshd。这一步做完之前,务必确认你的公钥已经能正常登录,否则你会把自己锁在门外。
还有一件小事很多人忽略:服务器上的防火墙。阿里云安全组是云层面的防线,操作系统自己的防火墙也建议开启。Ubuntu默认装了ufw,执行:
bash复制sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
这样一来,就算安全组配置出了问题,系统防火墙也能再兜底一层。
6. 数据备份策略与后期运维心得
6.1 定期备份MongoDB数据
个人项目最容易忽视的就是数据备份。服务器磁盘坏了、误删了数据、被勒索病毒加密了,任何一个事故都足以让你几个月的成果泡汤。
MongoDB自带mongodump工具,可以非常方便地导出数据库文件:
bash复制mongodump --uri="mongodb://admin:密码@127.0.0.1:27017/项目名?authSource=admin" --out=/backup/$(date +%Y%m%d)
然后配合cron定时任务,比如每天凌晨2点执行一次备份,并保留最近7天的备份文件:
bash复制sudo crontab -e
# 添加以下一行
0 2 * * * mongodump --uri="mongodb://admin:密码@127.0.0.1:27017/项目名?authSource=admin" --out=/backup/$(date +%Y%m%d) && find /backup -type d -mtime +7 -exec rm -rf {} \;
这行命令的意思是:每天2点整执行备份,并把7天前的备份目录清理掉,避免磁盘被撑爆。
6.2 日志查看与告警思路
部署完成后,日常维护最常干的事情就是看日志。pm2的日志可以用一行命令查看:
bash复制pm2 logs my-project-server
如果看到错误,要能判断是代码异常还是外部因素,比如数据库断连、接口超时、内存溢出等。Node.js内存溢出时,一般会在日志里输出JavaScript heap out of memory,这时优先检查是否在循环中缓存了不该缓存的对象,而不是急着给服务器加内存。
添加一个简单的监控告警,可以省去频繁登录服务器的麻烦。比如用阿里云自带的云监控,对CPU、内存、带宽设置报警规则,阈值可以设为CPU连续5分钟超过80%或磁盘空间低于20%。这样异常情况时会收到短信或邮件提醒,不用等用户反馈才知道系统挂了。
6.3 我踩过的几个坑
整个部署流程走下来,有几个坑我几乎每次都会遇到,值得单独拎出来说。
第一个是前端路由的history模式刷新404。Vue Router默认是createWebHistory,路径看起来是/dashboard这种没有#的样式,但在Nginx里如果没配try_files $uri $uri/ /index.html;,浏览器一刷新就会404。这个坑的原因在于:刷新时浏览器会向服务器发送对于/dashboard的HTTP请求,而Nginx找不到这个文件,自然就返回404了。解决办法就是上面Nginx配置里的那行try_files。
第二个是MongoDB没有开启鉴权导致数据库被删。有一个勒索病毒特别针对公网裸奔的MongoDB端口,黑客连上之后把数据全删了,留下一个要求打钱的文档。所以再次强调:MongoDB只绑定127.0.0.1,并且开启鉴权,这两条缺一不可。
第三个是阿里云安全组和系统防火墙的端口配置容易搞混。你在控制台开放了27017端口,系统防火墙如果没放行,照样连不上;反过来也一样。排查时先确认云安全组,再确认系统防火墙,否则容易在云控制台和服务器之间来回折腾。
6.4 更新发布流程
项目上线后必然要继续迭代。我习惯的发布流程是:本地改完代码,提交到Git,SSH登录服务器,在后端目录执行git pull,然后pm2 restart my-project-server;前端则是在本地或服务器上重新npm run build,生成的dist文件覆盖到/var/www/my-project。
如果你不想每次都在服务器上编译前端,可以直接在本机构建好dist目录,用rsync推送到服务器:
bash复制rsync -avz --delete ./dist/ root@服务器IP:/var/www/my-project/
--delete参数会删除服务器上多余的文件,保证两边目录完全一致。这个命令特别适合前端频繁改动的场景。
这套流程虽然朴素,但对个人项目来说足够稳定。等哪天业务量涨到需要多台服务器、自动扩容的时候,再去折腾Docker和Kubernetes也不迟。现在不是工具越复杂越好,而是能稳定跑起来、出了问题能半小时内修复,这才是部署的核心诉求。
我在实际操作中的体会是:整个部署流程最磨人的不是某一项技术难题,而是"本地能跑、服务器上跑不了"的落差。每次遇到这种问题,不要慌,按顺序检查环境版本、依赖安装、数据库连接、Nginx配置,基本十分钟内就能定位。把这套排查思路内化成自己的习惯,后续再接手任何部署任务,你都不会觉得心里没底。
