第9章 9.3 部署:Docker + PM2 + Nginx

📌 上一章我们学会了用 Jest + Supertest 给 Node.js 应用写测试,相当于给你的房子装修好以后做了一次全面「验房」。但验房通过不代表能住进去——你还得把房子「建好」并「通上水电」。这一章我们就来解决这个问题:怎么让你的代码从「能跑」变成「能上线」。

你有没有遇到过这种情况:本地写得美滋滋,代码换个电脑就炸了;或者终于部署到服务器,结果 SSH 断开服务也跟着崩了。这些问题的根源在于——你没有把「运行环境」和「代码本身」打包在一起

这一章学完,你就能:
- 把应用和依赖打包成一个「一键启动」的黑盒子
- 让服务在服务器上稳定运行,不受 SSH 断开影响
- 用域名访问你的应用,而不是 http://你的IP:3000


🎯 开场 3 分钟:为什么要学这个?

场景来了:你花了一周写了一个「待办清单 API」,想在服务器上跑起来让小伙伴访问。你可能会这样做:

  1. 把代码上传到服务器 scp -r myapp root@你的服务器:/opt/
  2. 服务器上 npm install
  3. node server.js 启动
  4. 心满意足关掉终端,发现服务也一起「休息」了

或者你遇到更崩溃的:

  • 服务器是 Ubuntu 20.04,你的 Mac 是 M1芯片,依赖库编译报错
  • 同事想跑你的项目,装了3小时依赖还没搞定
  • 服务器重启后,你的外卖都吃完了服务还没起来

学完这章你能解决
- 环境不一致问题(Docker 打包)
- 进程挂掉没人管问题(PM2 看门)
- 域名访问 + 负载均衡问题(Nginx 反向代理)


🧱 基础 25 分钟:核心概念

什么是 Docker?把它想象成「外卖打包」

你去餐厅点了一份酸菜鱼,店家会给你:
- 打包盒(容器)
- 一次性餐具(独立环境)
- 密封包装(隔离)

Docker 就是这样一套「外卖打包」机制。它把你的代码 + 运行环境 + 依赖库全部打包成一个镜像,在任何装了 Docker 的机器上都能原样跑起来。

生活类比:就好比你把「火锅底料 + 食材 + 卡式炉」全打包成一个礼盒,寄到朋友家他插上电就能吃,不用管他厨房里有没有天然气。

为什么用:解决「在我电脑能跑」这个世纪难题。

第一个 Dockerfile:给你的 Node.js 应用「打包」

# 指定基础镜像(运行环境)
FROM node:18-alpine

# 设置工作目录
WORKDIR /app

# 复制依赖文件(先复制这些,能利用 Docker 缓存)
COPY package*.json ./

# 安装依赖
RUN npm install

# 复制源代码
COPY . .

# 暴露端口
EXPOSE 3000

# 启动命令
CMD ["node", "server.js"]

这四句在干嘛
- FROM node:18-alpine:买一个带灶台的厨房(Node.js 18 版本,基于轻量 Alpine 系统)
- COPY package*.json ./:先把「食材清单」放进去
- RUN npm install:自动把所有「食材」备齐
- CMD ["node", "server.js"]:客人来了就开火(启动服务)

多阶段构建:让镜像更小更快

上面那个镜像大概 1GB,实际项目可能更大。有没有办法更小?有!用多阶段构建

# 第一阶段:构建
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install && npm run build

# 第二阶段:运行(用更小的基础镜像)
FROM node:18-alpine AS runner
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/server.js"]

类比:就像装修房子,第一阶段找施工队(构建依赖),第二阶段业主入住(只保留运行需要的文件)。施工队的工具箱不用搬进去。

配图1 - 配图1

什么是 PM2?把它想象成「24 小时贴身管家」

你本地跑 node server.js,一旦终端关了进程就死了。PM2 就是来解决这个问题的——它是一个进程管理器,让你的服务「后台跑着、断开 SSH 也不影响」。

生活类比:PM2 就像请了一个管家,你出门了它帮你看家,你断网了它依然在打扫。

核心命令

# 启动服务
pm2 start server.js

# 查看运行状态
pm2 list

# 查看日志
pm2 logs

# 重启服务
pm2 restart server

# 停止服务
pm2 stop server

cluster 模式:一个 Node.js 实例只能利用一个 CPU 核心,PM2 可以帮你起多个实例,充分利用服务器性能:

pm2 start server.js -i 4  # 启动 4 个实例形成集群

什么是 Nginx?把它想象成「酒店前台」

你的服务器上跑了 Node.js 服务(端口 3000)、Python API(端口 8000)、还有一个静态文件服务(端口 8080)。用户不可能记这么多端口——nginx 就是那个「统一前台」:

  • 用户访问 www.example.com → 前台(nginx)→ 分发到对应服务
  • 自动处理 HTTPS、静态文件缓存、负载均衡

最简单的 Nginx 配置

server {
listen 80;
server_name www.example.com;

location / {
    proxy_pass http://localhost:3000;  # 把请求转发给 Node.js
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
}
}

location / 是什么意思:就是「所有以 / 开头的路径」,比如 /api/users/products,nginx 都会转发到 localhost:3000

配图2 - 配图2

把它们串起来:完整部署流程

用户请求
↓
Nginx(端口 80/443,统一入口)
↓
PM2(管理 Node.js 进程,保持在线)
↓
Docker 容器(运行你的代码,环境一致)

🔥 实战 35 分钟:3 个递进小项目

项目 1:Dockerize 一个简单的 Express 服务(5 分钟)

目标:把一个最简 Express 服务打包成 Docker 镜像并运行。

1. 创建项目结构

mkdir my-express-app && cd my-express-app
npm init -y
npm install express

2. 创建 server.js

const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;

app.get('/', (req, res) => {
res.send('你好,Docker 部署成功!');
});

app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});

app.listen(PORT, () => {
console.log(`服务运行在端口 ${PORT}`);
});

3. 创建 Dockerfile

FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000

CMD ["node", "server.js"]

4. 构建并运行

# 构建镜像(-t 给镜像起个名字,. 表示当前目录)
docker build -t my-express-app .

# 运行容器(-d 后台运行,-p 端口映射 8080:3000)
docker run -d -p 8080:3000 --name myapp my-express-app

# 测试
curl http://localhost:8080/
# 输出:你好,Docker 部署成功!

一句话解释-p 8080:3000 意思是「把宿主机的 8080 端口映射到容器内的 3000 端口」,这样你访问 localhost:8080 就等于访问容器里的 localhost:3000


项目 2:PM2 守护 + 多实例部署(15 分钟)

目标:用 PM2 启动服务,并开启集群模式提升性能。

1. 确保项目 1 的服务在跑,先停掉容器

docker stop myapp && docker rm myapp

2. 全局安装 PM2

npm install -g pm2

3. 用 PM2 启动服务(集群模式,4 个实例)

pm2 start server.js -i 4 --name "express-cluster"

4. 查看状态

pm2 list

你会看到类似这样的输出:

┌─────┬──────────────┬─────────────┬──────────┬──────────┬──────────────┐
│ id  │ name         │ mode       │ status   │ restart  │ uptime       │
├─────┼──────────────┼─────────────┼──────────┼──────────┼──────────────┤
│ 0   │ express-cluster │ cluster   │ online   │ 0        │ 10s          │
│ 1   │ express-cluster │ cluster   │ online   │ 0        │ 10s          │
│ 2   │ express-cluster │ cluster   │ online   │ 0        │ 10s          │
│ 3   │ express-cluster │ cluster   │ online   │ 0        │ 10s          │
└─────┴──────────────┴─────────────┴──────────┴──────────┴──────────────┘

5. 测试多实例负载均衡

curl http://localhost:3000/health
# 连续请求几次,观察 timestamp 是否来自不同实例

6. PM2 开机自启配置

pm2 startup   # 复制输出的命令并执行
pm2 save      # 保存当前进程列表

一句话解释pm2 save 会把当前运行的进程「快照」保存下来,下次服务器重启,PM2 会自动按这个快照恢复服务。


项目 3:Nginx 反向代理 + HTTPS 基础配置(15 分钟)

目标:用 Nginx 接收用户请求,转发到 PM2 管理的 Node.js 服务。

1. 安装 Nginx

# Ubuntu/Debian
sudo apt update && sudo apt install nginx -y

# Mac(Homebrew)
brew install nginx

2. 配置 Nginx

sudo nano /etc/nginx/sites-available/default

写入以下配置:

server {
listen 80;
server_name localhost;

location / {
    proxy_pass http://localhost:3000;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection 'upgrade';
    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_cache_bypass $http_upgrade;
}
}

3. 重启 Nginx

sudo nginx -t        # 检查配置是否正确
sudo systemctl reload nginx   # Ubuntu
# 或 Mac:brew services restart nginx

4. 测试

curl http://localhost/
# 输出:你好,Docker 部署成功!

实际效果:现在访问 http://你的服务器IP/ 就能看到 Node.js 服务返回的内容,端口被「隐藏」在 Nginx 后面了。

进阶:配置 HTTPS(Let's Encrypt 免费证书)

# 安装 Certbot
sudo apt install certbot python3-certbot-nginx -y

# 获取证书(需要域名已解析到服务器)
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

Certbot 会自动修改 Nginx 配置,添加 HTTPS 支持。


💪 进阶 20 分钟:常见坑 + 性能小贴士

坑 1:Docker 镜像体积太大

错误做法:用 node:18 基础镜像,不做任何优化,镜像 1GB+。

正确做法
- 用 node:18-alpine(Alpine 是轻量级 Linux 发行版)
- 使用多阶段构建,只复制必要文件
- 添加 .dockerignore 文件排除不需要的内容

# .dockerignore 内容
node_modules
.git
*.log
.env

坑 2:PM2 集群模式下 Session 不共享

错误做法:把用户登录信息存在内存变量里,多实例下用户频繁掉线。

正确做法:使用 Redis 等外部存储管理 Session。

const session = require('express-session');
const RedisStore = require('connect-redis')(session);

app.use(session({
store: new RedisStore({ client: redisClient }),
secret: 'your-secret-key',
resave: false,
saveUninitialized: false
}));

坑 3:Nginx 配置 proxy_pass 端口写错

错误示例:服务跑在 3000 端口,配置里写 8000,访问永远 502。

正确做法:先 pm2 list 确认端口,再用 nginx -t 验证配置。

坑 4:容器内进程权限问题

错误做法:Dockerfile 用 RUN chmod -R 777 /app,安全风险大。

正确做法:创建专用用户。

RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
USER nodejs

坑 5:重启后服务没起来

错误做法:只 pm2 start,以为搞定了,但服务器重启后服务蒸发了。

正确做法:完整配置开机自启。

pm2 startup   # 生成初始化脚本
pm2 save      # 保存当前状态

性能小贴士:PM2 内存限制

单个 Node.js 进程默认内存无上限,写 bug 可能把服务器吃满。

pm2 start server.js --max-memory-restart 500M

当进程内存超过 500MB 时,PM2 会自动重启它。

调试技巧:PM2 日志实时查看

# 查看所有日志
pm2 logs

# 查看特定进程的实时日志
pm2 logs express-cluster --lines 100 --nostream

✏️ 练习题 + 作业题

练习题(10 分钟)

练习 1(2 分钟):换个端口跑容器
- 输入:把项目 1 的容器端口映射从 8080:3000 改成 9000:3000
- 预期输出:curl http://localhost:9000/ 返回「你好,Docker 部署成功!」
- 提示:只需要改 docker run 命令的那一行

练习 2(2 分钟):给 Express 加个路由
- 输入:在 server.js 里加一个 GET /time 路由,返回当前时间
- 预期输出:访问 http://localhost:9000/time 返回类似 {"time":"2024-01-15T10:30:00.000Z"}
- 提示:用 new Date().toISOString() 获取时间

练习 3(2 分钟):PM2 改成 2 个实例
- 输入:用 -i 2 重新启动项目 2 的集群
- 预期输出:pm2 list 显示 2 个实例
- 提示:先 pm2 delete express-cluster 删除旧的,再重新 start

练习 4(2 分钟):Nginx 转发到另一个端口
- 输入:修改 Nginx 配置,把请求转发到 9000 端口(而不是 3000)
- 预期输出:重启 Nginx 后访问 localhost 仍能正常返回
- 提示:改 proxy_pass http://localhost:9000;,然后 sudo nginx -t && sudo nginx -s reload

练习 5(2 分钟):分析 502 错误
- 输入:假设访问 localhost 返回 502 Bad Gateway,列出可能的原因
- 预期输出:至少列出 3 个可能原因
- 提示:502 说明 Nginx 拿到了错误的响应,可能是后端没启动、端口写错、或者进程挂了


作业题(30 分钟 - 2 小时)

作业:部署一个「天气查询 API」

需求描述:用 Express 写一个简单的天气查询接口(模拟数据),用 Docker 打包,PM2 管理,Nginx 代理。

功能点
1. GET /weather?city=北京 返回 { "city": "北京", "temp": "25°C", "weather": "晴" }
2. 城市不存在时返回 { "error": "城市不存在" }
3. 默认城市为「北京」

加分项
1. 用 .env 文件管理默认端口
2. PM2 集群模式运行
3. 配置 HTTPS(用自签名证书也行)

验收标准
- Docker 容器能成功构建并运行
- 访问 http://localhost/weather?city=上海 返回正确的 JSON
- PM2 列表显示进程状态为「online」
- 代码有适当注释

提交方式:评论区贴关键代码或 GitHub 仓库链接。


📚 总结 + 资源

本文学了 3 个核心点
1. Docker 把环境和代码打包,解决「在我电脑能跑」问题
2. PM2 让服务 24 小时在线,还能多实例负载均衡
3. Nginx 作为统一入口,隐藏端口、提供 HTTPS

延伸学习资源
- Docker 官方文档(最权威,但有点干)
- 《Docker 容器与容器云》(国人写的,比较接地气)
- PM2 官方指南

互动钩子:你在部署时遇到过什么奇葩问题?是依赖装不上、还是端口被占、还是半夜收到服务器报警?评论区聊聊,老粉优先回复!


📌 下章剧透:学会了部署基础,下一章我们来玩点「高端」的——Nest.js 和 Fastify 这两个现代 Node.js 框架,一个「企业级全家桶」,一个「轻量级快枪手」,到底选哪个?

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。