三条路
| PM2 | systemd | Docker | |
|---|---|---|---|
| 复杂度 | 低 | 中 | 中-高 |
| 依赖隔离 | 弱 | 弱 | 强 |
| 多实例 | 内置 cluster | 自己写 | docker compose |
| 监控 | 内置仪表板 | journalctl | docker logs |
| 适合 | 单机起步 | Linux 老派 / 不想装额外 | 现代标准 / 容器化 |
实战中常组合:Docker + systemd(容器作为 unit);或 Nginx + PM2(最简)。
PM2(最快上手)
npm install -g pm2
# 启动
pm2 start src/index.js --name myapp
# Cluster 模式
pm2 start src/index.js -i max # max = CPU 核数
# 用 ecosystem 文件(推荐)
ecosystem.config.cjs:
module.exports = {
apps: [{
name: 'myapp',
script: 'dist/index.js',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
PORT: 3000,
},
max_memory_restart: '500M',
}],
};
pm2 start ecosystem.config.cjs
# 看状态
pm2 list
pm2 logs myapp
pm2 monit # 终端实时面板
pm2 status
# 操作
pm2 restart myapp
pm2 reload myapp # 零停机重启
pm2 stop myapp
pm2 delete myapp
# 开机自启
pm2 startup # 按提示一行命令
pm2 save # 保存当前进程列表
PM2 优点:
- ✓ 内置 cluster
- ✓ 监控仪表板
- ✓ 日志聚合 / 轮转
- ✓ 零停机 reload
- ✓ 内存超阈值自动重启
PM2 缺点:
- 又多一层(不是 OS 原生)
- 跟容器化时代不太契合
systemd(OS 原生)
/etc/systemd/system/myapp.service:
[Unit]
Description=My App
After=network.target
[Service]
Type=simple
User=wadely
WorkingDirectory=/home/wadely/myapp
ExecStart=/usr/bin/node dist/index.js
Restart=on-failure
RestartSec=5
Environment=NODE_ENV=production
Environment=PORT=3000
StandardOutput=journal
StandardError=journal
# 资源限制
MemoryLimit=512M
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now myapp
sudo systemctl status myapp
sudo journalctl -u myapp -f # 跟踪日志
详见 linux/21-systemctl。
systemd 优点:
- ✓ OS 原生,没有额外层
- ✓ journal 聚合日志
- ✓ 依赖管理强(After=)
- ✓ 资源限制(MemoryLimit / CPUQuota)
Docker(现代标准)
Dockerfile:
# 多阶段构建
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json tsconfig.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-alpine
WORKDIR /app
COPY --from=build /app/package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
USER node # 非 root
EXPOSE 3000
CMD ["node", "dist/index.js"]
docker-compose.yml:
services:
app:
build: .
ports: ["3000:3000"]
environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://...
restart: unless-stopped
depends_on: [db, redis]
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: secret
volumes:
- pg-data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
volumes:
pg-data:
docker compose up -d --build
docker compose logs -f app
docker compose ps
docker compose restart app
docker compose down # 停 + 删容器(卷保留)
详见 linux/52-docker 和 ops-corp/08-docker-prod。
Nginx 反向代理(基本必装)
不论用哪个上面方案,前面都该有 Nginx:
upstream nodeapp {
server 127.0.0.1:3000;
keepalive 64;
}
server {
listen 80;
server_name myapp.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name myapp.example.com;
ssl_certificate /etc/letsencrypt/.../fullchain.pem;
ssl_certificate_key /etc/letsencrypt/.../privkey.pem;
gzip on;
gzip_types text/plain application/json text/css application/javascript;
location / {
proxy_pass http://nodeapp;
proxy_http_version 1.1;
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_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
Node 应用 app.set('trust proxy', 1) 才能拿真实 IP。
健康检查
app.get('/health', async (req, res) => {
try {
await db.query('SELECT 1');
res.json({ ok: true, time: new Date() });
} catch (err) {
res.status(503).json({ ok: false, error: err.message });
}
});
PM2 / Docker / Kubernetes 都用这个端点判断进程是否健康。
优雅退出
const server = app.listen(3000);
let shuttingDown = false;
async function shutdown(signal) {
if (shuttingDown) return;
shuttingDown = true;
console.log(`${signal} received, shutting down`);
server.close();
await db.end();
process.exit(0);
}
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
部署平台发 SIGTERM 时优雅关闭——10 秒内未结束才被 SIGKILL。
决策建议
| 场景 | 用 |
|---|---|
| 个人项目 / MVP | PM2 + Nginx |
| 公司单机服务 | systemd + Nginx |
| 多服务 / 想要隔离 | Docker Compose |
| 团队 / 多机 / K8s | Docker → Kubernetes |
坑
- 不要在 PM2 / systemd 里装 root——非特权用户跑
- 容器里没有 systemd——别在容器里 PM2 cluster 也别 systemd
- 健康检查端点要轻(别真查数据库每次)—— 否则监控自己打挂服务
- Node 进程正常死亡是好事——让 PM2/systemd/Docker 重启它
下一篇:路线总结。