阅读视图

发现新文章,点击刷新页面。

后端代码部署到服务器,服务器配置数据库,pm2进程管理发布(四)

前置系列文章

从零开始:在阿里云 Ubuntu 服务器部署 Node+Express 接口(一)

阿里云域名解析 + Nginx 反向代理 + HTTPS 全流程:从 IP 访问到加密域名的完整配置(二)

Node+Express+MySQL 实现注册功能(三)

将代码和数据库发布到阿里云轻量服务器(Ubuntu)的核心流程是:本地准备 → 服务器环境配置 → 代码部署(含安全传输配置文件) → 数据库迁移 → 启动服务。以下是循序渐进的详细步骤,重点解决 .env.production 不提交到 Git 的问题:

一、本地准备工作(确保代码可部署)

1. 检查本地代码规范
  • 确认 .gitignore 配置:确保已忽略敏感文件,避免提交到 Git:
# 项目根目录的 .gitignore 文件必须包含以下内容
.env.development
.env.production
.env.test
node_modules/
2. 导出本地数据库结构(用于服务器初始化)

在mysql workBench中到出本地数据库mydb,只需要导出数据结构,会有一个sql文件导到本地,可以自己命名,我的叫my_db.sql

二、服务器环境准备(阿里云 Ubuntu)

1. 登录服务器

通过终端登录阿里云轻量服务器(替换为你的服务器公网 IP):

ssh root@你的服务器公网IP # 例如:ssh root@120.78.xxx.xxx

输入服务器登录密码

2. 安装必要软件

如果服务器未安装以下软件,依次执行:

# 更新系统
sudo apt update && sudo apt upgrade -y

# 安装 Node.js(推荐 v16+)
curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash -
sudo apt install -y nodejs

# 安装 MySQL 服务器(生产环境数据库)
sudo apt install -y mysql-server

# 安装 PM2(Node 服务进程管理工具)
sudo npm install pm2 -g

# 安装 Nginx(反向代理、静态资源服务)
sudo apt install -y nginx
3. 配置服务器 MySQL(生产环境数据库)
步骤一:初始化 MySQL 并创建生产数据库
# 登录MySQL(Ubuntu初始无密码,直接回车)
sudo mysql -u root -p

# 设置root密码(自定义强密码,如Server@Mysql2024)
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的root密码';
FLUSH PRIVILEGES;

# 创建生产数据库(与项目.env.production一致,mydb_prod是数据库名)
CREATE DATABASE mydb_prod CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

# 创建数据库专用用户(避免root直接操作)
CREATE USER 'prod_user'@'%' IDENTIFIED BY 'Prod@2024';

# 授予用户数据库权限
GRANT ALL PRIVILEGES ON mydb_prod.* TO 'prod_user'@'%';

# 刷新权限
FLUSH PRIVILEGES;

# 退出MySQL
EXIT;

.env.production如下

# 环境标识
NODE_ENV=production

# 数据库配置(服务器数据库信息,变量名与开发环境完全一致)
NODE_ENV=production
DB_HOST=localhost  # 服务器数据库在本地,填localhost
DB_PORT=3306
DB_USER=prod_user  # 步骤3创建的专用用户
DB_PASSWORD=Prod@2024  # 专用用户的密码
DB_NAME=mydb_prod  # 生产数据库名
API_PORT=3000  # 后端服务端口

因为这个文件不能提交到git,所以要在服务器这个项目的根目录创建这个文件,并把上面的内容写进去就行。 我的是放在/root/projects/node-api-test下

image.png

步骤2: 导入本地表结构到服务器数据库
# 1. 在 Mac 终端另开窗口,上传本地 my_db.sql 到服务器的 /tmp 目录
scp /本地路径/my_db.sql root@你的服务器IP:/tmp/
# 例如:scp ~/project/my_db.sql root@120.78.xxx.xxx:/tmp/

# 2. 回到服务器终端,导入表结构到 mydb_prod 数据库
mysql -u root -p mydb_prod < /tmp/my_db.sql
# 输入服务器 MySQL 的 root 密码(步骤1中设置的),完成后表结构导入成功

服务器生产数据库中创建 users 表(表结构迁移)

在生产环境部署中,需将本地开发的 users 表结构迁移到服务器的 mydb_prod 数据库中,确保后端接口能正常读写用户数据。

服务器导入 users 表结构到 mydb_prod

登录服务器终端,将表结构导入生产数据库:

# 服务器终端执行(输入prod_user的密码Prod@2024) 
mysql -u prod_user -p mydb_prod < /tmp/my_db.sql
(4)验证表是否创建成功

登录服务器 MySQL,检查 users 表是否存在:

# 服务器终端登录MySQL
mysql -u prod_user -p mydb_prod

# 查看数据库中的表
SHOW TABLES;  # 应显示 users 表

# 查看表结构(可选)
DESCRIBE users;  # 显示 users 表的字段、类型等结构信息

# 退出MySQL
EXIT;

补充说明

  • 若本地 users 表有初始必要数据(如管理员账号),可去掉 --no-data 参数,导出包含数据的表结构:mysqldump -u 本地用户名 -p 本地数据库名 users > users_with_data.sql,再按相同步骤导入。
  • 表结构迁移是生产部署的关键步骤,确保服务器数据库表结构与本地开发环境一致,否则会出现 “表不存在”“字段缺失” 等接口错误。

发布代码

登录服务器,cd /root/xxx/xxx 到自己的项目根目录,执行git pull ,然后执行pm2 restart node-api-test

这时候会报错,提示环境变量是undefined,process.env.NODE_ENV 打印出的永远是undefined,所以下面这段代码,发布到服务器上就挂了

const env = process.env.NODE_ENV || 'development'
const envPath = path.resolve(__dirname, `../.env.${env}`)
console.log('envPath', envPath) 
// 会打印出undefined

原因分析

  1. 当你在服务器命令行直接输入 pm2 start app.js 时,Linux shell 并没有设置 NODE_ENV,代码里 process.env.NODE_ENV 就会变成 undefined,然后你的代码兜底逻辑将其设为 development,于是去读开发环境的配置,导致报错。

  2. app.js 方式启动: 执行 pm2 start app.js 时,PM2 只是简单地执行 node app.js。除非你在操作系统层面(如 /etc/profile)设置了全局 NODE_ENV,否则它就是空的。

解决方案:

使用 ecosystem.config.cjs(最推荐,工业标准

项目根目录创建ecosystem.config.cjs

// ecosystem.config.cjs
module.exports = {
  apps: [
    {
      name: 'node-api-test',
      script: './app.js',
 
      instances: 1,
      autorestart: true,
      watch: false,
      env: {
        NODE_ENV: 'development',
      },
      env_production: {
        NODE_ENV: 'production',
      },
    },
  ],
}

启动命令
以后在服务器上启动或重启,统一使用以下命令,不要再用 pm2 start app.js:

 #   首次启动(生产):
 pm2 start ecosystem.config.cjs --env production
 #    代码更新后重启 平滑重载(零停机):
 pm2 reload ecosystem.config.cjs --env production
 #   完全重启:
 pm2 restart ecosystem.config.cjs --env production

最终测试

image.png

Node+Express+MySQL 后端生产环境部署,实现注册功能(三)

一、部署前准备

  • 本地环境:MacOS(开发端)
  • 服务器环境:阿里云 Ubuntu 22.04 轻量应用服务器
  • 技术栈:Node.js + Express + MySQL
  • 核心目标:将本地开发完成的 Express 后端项目部署到阿里云,实现公网访问接口

二、服务器环境配置(最终生效配置)

1. 登录服务器

通过 Mac 终端 SSH 连接服务器:

ssh root@你的服务器公网IP # 例如:ssh root@47.101.129.155

输入服务器登录密码即可进入。

2. 安装核心依赖软件
# 更新系统包
sudo apt update && sudo apt upgrade -y

# 安装Node.js(v16+)
curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash -
sudo apt install -y nodejs

# 安装MySQL服务器
sudo apt install -y mysql-server

# 安装PM2(Node服务进程管理)
sudo npm install pm2 -g

# 安装Nginx(反向代理)
sudo apt install -y nginx

创建本地数据库

  1. 确保数据库已创建:用 MySQL Bench 连接本地 MySQL(root/admin123/3306),创建数据库 mydb(字符集 utf8mb4,排序规则 utf8mb4_unicode_ci)。

三、创建用户表(存储注册信息)

在 MySQL Bench 中,对 mydb 数据库执行以下 SQL,创建 users 表(用于存储注册用户):

USE mydb; -- 切换到 mydb 数据库

CREATE TABLE users (
  id INT AUTO_INCREMENT PRIMARY KEY, -- 自增主键
  email VARCHAR(100) NOT NULL UNIQUE, -- 邮箱(唯一,避免重复注册)
  password VARCHAR(255) NOT NULL, -- 加密后的密码(bcrypt 加密后长度固定60)
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP, -- 注册时间
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -- 更新时间
);

四 新建项目

创建文件夹node-api-test

  1. 安装依赖:需要 mysql2(数据库连接)、bcrypt(密码加密,不能明文存储)、express-validator(参数校验),dotenv 判断环境变量

npm init -y

npm install mysql2 bcrypt express-validator deotnev

2. 多环境配置文件(区分测试 / 正式)

在项目根目录创建 多个 .env 文件,分别对应不同环境:

project/
├── .env.development  # 开发环境(本地调试)
├── .env.production   # 生产环境(正式服务器)
├── .env.test         # 测试环境(可选,测试服务器)
└── .gitignore        # 忽略 .env* 文件,避免提交到 Git

文件内容示例

NODE_ENV=development  # 标识环境
DB_HOST=localhost     # 本地数据库地址
DB_USER=root          # 本地数据库账号
DB_PASSWORD=admin123  # 本地数据库密码
DB_NAME=mydb          # 本地数据库名
API_PORT=3000         # 开发环境端口

.env.production(生产环境):

NODE_ENV=production
DB_HOST=10.0.0.1      # 服务器数据库地址(内网 IP)
DB_USER=prod_user     # 服务器数据库账号(非 root,更安全)
DB_PASSWORD=Prod@123  # 服务器数据库密码(复杂密码)
DB_NAME=mydb_prod     # 生产环境数据库名(可与开发环境不同)
API_PORT=3000         # 生产环境端口
3. 在代码中加载对应环境的配置

db/mysql.js,根据 NODE_ENV 自动加载对应的 .env 文件:

// db/mysql.js(ESM 版)
import mysql from 'mysql2/promise';
import dotenv from 'dotenv';
import path from 'path';
import { fileURLToPath } from 'url';

// 解决 ESM 中 __dirname 问题
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// 1. 确定当前环境(默认 development)
const env = process.env.NODE_ENV || 'development';

// 2. 加载对应环境的 .env 文件(如 .env.development 或 .env.production)
const envPath = path.resolve(__dirname, `../.env.${env}`);
dotenv.config({ path: envPath });  // 加载指定路径的 .env 文件

// 3. 从 process.env 中读取配置(环境变量全部是字符串类型)
const dbConfig = {
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME,
  port: Number(process.env.DB_PORT) || 3306,  // 转换为数字
  connectionLimit: 10,
};

// 创建连接池
const pool = mysql.createPool(dbConfig);

// 测试连接时打印当前环境
export async function testDbConnection() {
  try {
    await pool.getConnection();
    console.log(`✅ 数据库连接成功(环境:${env},数据库:${dbConfig.database}`);
  } catch (err) {
    console.error(`❌ 数据库连接失败(环境:${env}):`, err.message);
    throw err;
  }
}

export { pool };

app.js如下:


import express from 'express'
import bodyParser from 'body-parser'
import userRouter from './routes/user.js'
import { testDbConnection } from './db/mysql.js'
import HttpError from './utils/HttpError.js' // 导入自定义错误类

const app = express()
// 从环境变量读取端口(对应.env中的API_PORT)
const port = process.env.API_PORT || 3000

// 解析JSON请求(必须,否则无法获取req.body)
app.use(bodyParser.json())

// 挂载用户模块路由
app.use('/api/user', userRouter)

// 全局错误处理中间件(必须放在所有路由和中间件之后)
app.use((err, req, res, next) => {
  // 1. 处理自定义HttpError
  if (err instanceof HttpError) {
    return res.status(err.statusCode).json({
      status: err.statusCode, // 业务错误状态码(如400)
      message: err.message, // 错误提示信息
      errors: err.errors, // 详细错误列表(如参数校验错误)
    })
  }

  // 2. 处理系统错误(如数据库连接失败、代码bug等)
  console.error('系统错误堆栈:', err.stack) // 打印堆栈,方便后端调试
  res.status(500).json({
    status: 500,
    message:
      process.env.NODE_ENV === 'production'
        ? '服务器内部错误,请稍后重试' // 生产环境隐藏具体错误
        : `系统错误:${err.message}`, // 开发环境显示具体错误(便于调试)
    errors: [],
  })
})

// 启动服务(端口来自环境变量)
app.listen(port, () => {
  console.log(
    `服务启动成功(环境:${process.env.NODE_ENV}):http://localhost:${port}`
  )
  testDbConnection() // 启动时验证数据库连接
})

注册接口编写

在根目录下新建routes/user.js

import express from 'express'
import { body, validationResult } from 'express-validator'
import bcrypt from 'bcrypt'
import { pool } from '../db/mysql.js'
import HttpError from '../utils/HttpError.js'

const router = express.Router()

// 注册接口:POST /api/user/register
router.post(
  '/register',
  // 参数校验(字段名与前端传入、数据库字段一致)
  [
    body('email').isEmail().withMessage('邮箱格式错误'), // 对应数据库email字段
    body('password').isLength({ min: 6 }).withMessage('密码至少6位'), // 对应password字段
    body('nickname')
      .optional()
      .isLength({ max: 50 })
      .withMessage('昵称最多50字'), // 对应nickname字段
  ],
  async (req, res, next) => {
    try {
      // 校验参数
      const errors = validationResult(req)
      if (!errors.isEmpty()) {
        throw new HttpError(400, '参数校验失败', errors.array())
      }

      // 解构前端传入的参数(字段名与数据库字段一致)
      const { email, password, nickname } = req.body

      // 1. 检查邮箱是否已注册(SQL中使用email字段,与数据库一致)
      const [existingUsers] = await pool.query(
        'SELECT id FROM users WHERE email = ?', // WHERE条件用email字段
        [email]
      )
      if (existingUsers.length > 0) {
        throw new HttpError(400, '该邮箱已被注册')
      }

      // 2. 密码加密
      //   const hashedPassword = await bcrypt.hash(password, 10)

      // 3. 插入数据库(字段名与数据库表完全一致)
      const [result] = await pool.query(
        'INSERT INTO users (email, password, nickname) VALUES (?, ?, ?)', // 字段顺序:email, password, nickname
        [email, password, nickname || null] // 对应字段的值
      )

      // 4. 返回结果(包含数据库自动生成的id和字段)
      res.status(200).json({
        code: 200,
        message: '注册成功',
        data: {
          userId: result.insertId, // 数据库自增id
          email: email, // 与数据库email字段一致
          nickname: nickname || null, // 与数据库nickname字段一致
          createdAt: new Date().toISOString(),
        },
      })
    } catch (err) {
      next(err)
    }
  }
)

// 获取所有用户
router.get('/allUsers', async (req, res, next) => {
  try {
    const [users] = await pool.query('SELECT * FROM users')
    res.status(200).json({
      code: 200,
      message: '获取成功',
      data: users,
    })
  } catch (err) {
    next(err)
  }
})

export default router

本地postman测试

image.png

数据库查看这条数据

image.png

下一篇学习如何把代码发布到服务器,通过域名来访问接口,实现注册,顺便把前端页面也发布上去

❌