三条路

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-dockerops-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 重启它

下一篇:路线总结。