第9章 9.3 部署:Docker + PM2 + Nginx
📌 上一章我们学会了用 Jest + Supertest 给 Node.js 应用写测试,相当于给你的房子装修好以后做了一次全面「验房」。但验房通过不代表能住进去——你还得把房子「建好」并「通上水电」。这一章我们就来解决这个问题:怎么让你的代码从「能跑」变成「能上线」。
你有没有遇到过这种情况:本地写得美滋滋,代码换个电脑就炸了;或者终于部署到服务器,结果 SSH 断开服务也跟着崩了。这些问题的根源在于——你没有把「运行环境」和「代码本身」打包在一起。
这一章学完,你就能:
- 把应用和依赖打包成一个「一键启动」的黑盒子
- 让服务在服务器上稳定运行,不受 SSH 断开影响
- 用域名访问你的应用,而不是 http://你的IP:3000
🎯 开场 3 分钟:为什么要学这个?
场景来了:你花了一周写了一个「待办清单 API」,想在服务器上跑起来让小伙伴访问。你可能会这样做:
- 把代码上传到服务器
scp -r myapp root@你的服务器:/opt/ - 服务器上
npm install node server.js启动- 心满意足关掉终端,发现服务也一起「休息」了
或者你遇到更崩溃的:
- 服务器是 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"]
类比:就像装修房子,第一阶段找施工队(构建依赖),第二阶段业主入住(只保留运行需要的文件)。施工队的工具箱不用搬进去。

什么是 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。

把它们串起来:完整部署流程
用户请求
↓
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 框架,一个「企业级全家桶」,一个「轻量级快枪手」,到底选哪个?

评论(0)