Tools: Latest: pm2 Process Manager: Keep Your Node.js Bots Running Forever

Tools: Latest: pm2 Process Manager: Keep Your Node.js Bots Running Forever

What You'll Need

Table of Contents

Why PM2 Matters for Bot Automation

Installation & Initial Setup

Running Your First Process

Clustering & Load Balancing

Monitoring & Logs in Real-Time

Auto-Restart on Server Reboot

Integration with n8n Webhooks I learned this the hard way. Three years ago, I deployed a Node.js bot to a VPSβ€”no process manager. It crashed at 2 AM on a Sunday, sat dead for six hours, and tanked the entire automation pipeline. That was the day I discovered PM2. If you're running Node.js applications in productionβ€”whether it's a webhook listener for n8n automation, a Telegram bot, a Discord integration, or a custom API serverβ€”you need process management. PM2 is the industry standard because it: When you're building automation workflows that depend on external services, uptime isn't optional. Unlike managed platforms, a VPS doesn't come with built-in process monitoring. PM2 fills that gap for under $5/month in hosting costs (plus zero subscription fees). SSH into your server and install PM2 globally: Verify the installation: You should see version 5.x or higher. Next, create a directory for your bot project: Install Express and Axios as dependencies (we'll use these for webhook handling): Now create your first .env file to store configuration: Let me show you a real example. This is a simple Express server that listens for webhooks and triggers actions. Create app.js: Now start this with PM2: You'll see output like: πŸ’‘ Fast-Track Your Project: Don't want to configure this yourself? I build custom n8n pipelines and bots. Message me with code SYS3-DEVTO. Here's where PM2 gets powerful. Instead of running a single process, you can spawn one worker per CPU core. This means if your server has 4 cores, PM2 will automatically load-balance across 4 instances of your app. Create an ecosystem.config.js file in your project root: Stop the current process and restart using this config: Now you'll see multiple instances (one per core): This is key for production. If one instance crashes, PM2 auto-restarts it while the others keep handling requests. When you're comparing automation platformsβ€”whether it's n8n vs Make vs Zapierβ€”self-hosted solutions like n8n running on your own VPS require this kind of reliability layer. Watch all your processes in a dashboard: This shows CPU, memory, requests per minute, and restart counts. Press q to exit. View logs for a specific app: Or tail the last 100 lines: For a broader view of what's happening, save all logs to files. The ecosystem.config.js I provided earlier already does this. Check your logs: You can also rotate logs automatically. Install the PM2 logrotate module: Configure it to keep logs under 10MB: This keeps your disk cleanβ€”critical when you're running high-volume webhook receivers on a budget VPS like Hetzner or Contabo. If your VPS restarts (whether planned or due to a crash), PM2 needs to resurrect your processes. We do this by creating a startup script: This command outputs a line you need to run. It looks like: Copy and run that exact line. Then save your current process list: Now test it. Reboot your server: Wait 30 seconds, SSH back in, and check: Your processes should be running. If they're not, check the systemd service: This is where it gets practical. When you're running a self-hosted n8n instance, you often need a separate Node.js service to handle incoming webhooks and trigger workflows. PM2 keeps that service alive. Here's a real workflow: your PM2-managed bot listens on port 3001, receives events, and hits your n8n webhook to trigger an automation. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Command

Copy

$ -weight: 600;">sudo -weight: 500;">npm -weight: 500;">install -g pm2 -weight: 600;">sudo -weight: 500;">npm -weight: 500;">install -g pm2 -weight: 600;">sudo -weight: 500;">npm -weight: 500;">install -g pm2 pm2 --version pm2 --version pm2 --version mkdir -p ~/projects/automation-bot cd ~/projects/automation-bot -weight: 500;">npm init -y mkdir -p ~/projects/automation-bot cd ~/projects/automation-bot -weight: 500;">npm init -y mkdir -p ~/projects/automation-bot cd ~/projects/automation-bot -weight: 500;">npm init -y -weight: 500;">npm -weight: 500;">install express axios dotenv -weight: 500;">npm -weight: 500;">install express axios dotenv -weight: 500;">npm -weight: 500;">install express axios dotenv cat > .env << 'EOF' PORT=3001 NODE_ENV=production N8N_WEBHOOK_URL=https://your-n8n-instance.com/webhook/automation LOG_LEVEL=info EOF cat > .env << 'EOF' PORT=3001 NODE_ENV=production N8N_WEBHOOK_URL=https://your-n8n-instance.com/webhook/automation LOG_LEVEL=info EOF cat > .env << 'EOF' PORT=3001 NODE_ENV=production N8N_WEBHOOK_URL=https://your-n8n-instance.com/webhook/automation LOG_LEVEL=info EOF const express = require('express'); const axios = require('axios'); require('dotenv').config(); const app = express(); const PORT = process.env.PORT || 3001; app.use(express.json()); // Health check endpoint app.get('/health', (req, res) => { res.-weight: 500;">status(200).json({ -weight: 500;">status: 'ok', timestamp: new Date().toISOString() }); }); // Webhook endpoint app.post('/webhook/event', async (req, res) => { try { const { event_type, data } = req.body; console.log(`[${new Date().toISOString()}] Received event: ${event_type}`); // Send to n8n webhook const response = await axios.post(process.env.N8N_WEBHOOK_URL, { event_type, data, received_at: new Date().toISOString(), server_hostname: require('os').hostname() }); res.-weight: 500;">status(200).json({ success: true, message: 'Event processed', n8n_response: response.-weight: 500;">status }); } catch (error) { console.error(`[ERROR] ${error.message}`); res.-weight: 500;">status(500).json({ success: false, error: error.message }); } }); // Error handling middleware app.use((err, req, res, next) => { console.error('Unhandled error:', err); res.-weight: 500;">status(500).json({ error: 'Internal server error' }); }); app.listen(PORT, () => { console.log(`[${new Date().toISOString()}] Server running on port ${PORT}`); console.log(`Health check: http://localhost:${PORT}/health`); }); // Graceful shutdown process.on('SIGTERM', () => { console.log('SIGTERM received, shutting down gracefully'); process.exit(0); }); const express = require('express'); const axios = require('axios'); require('dotenv').config(); const app = express(); const PORT = process.env.PORT || 3001; app.use(express.json()); // Health check endpoint app.get('/health', (req, res) => { res.-weight: 500;">status(200).json({ -weight: 500;">status: 'ok', timestamp: new Date().toISOString() }); }); // Webhook endpoint app.post('/webhook/event', async (req, res) => { try { const { event_type, data } = req.body; console.log(`[${new Date().toISOString()}] Received event: ${event_type}`); // Send to n8n webhook const response = await axios.post(process.env.N8N_WEBHOOK_URL, { event_type, data, received_at: new Date().toISOString(), server_hostname: require('os').hostname() }); res.-weight: 500;">status(200).json({ success: true, message: 'Event processed', n8n_response: response.-weight: 500;">status }); } catch (error) { console.error(`[ERROR] ${error.message}`); res.-weight: 500;">status(500).json({ success: false, error: error.message }); } }); // Error handling middleware app.use((err, req, res, next) => { console.error('Unhandled error:', err); res.-weight: 500;">status(500).json({ error: 'Internal server error' }); }); app.listen(PORT, () => { console.log(`[${new Date().toISOString()}] Server running on port ${PORT}`); console.log(`Health check: http://localhost:${PORT}/health`); }); // Graceful shutdown process.on('SIGTERM', () => { console.log('SIGTERM received, shutting down gracefully'); process.exit(0); }); const express = require('express'); const axios = require('axios'); require('dotenv').config(); const app = express(); const PORT = process.env.PORT || 3001; app.use(express.json()); // Health check endpoint app.get('/health', (req, res) => { res.-weight: 500;">status(200).json({ -weight: 500;">status: 'ok', timestamp: new Date().toISOString() }); }); // Webhook endpoint app.post('/webhook/event', async (req, res) => { try { const { event_type, data } = req.body; console.log(`[${new Date().toISOString()}] Received event: ${event_type}`); // Send to n8n webhook const response = await axios.post(process.env.N8N_WEBHOOK_URL, { event_type, data, received_at: new Date().toISOString(), server_hostname: require('os').hostname() }); res.-weight: 500;">status(200).json({ success: true, message: 'Event processed', n8n_response: response.-weight: 500;">status }); } catch (error) { console.error(`[ERROR] ${error.message}`); res.-weight: 500;">status(500).json({ success: false, error: error.message }); } }); // Error handling middleware app.use((err, req, res, next) => { console.error('Unhandled error:', err); res.-weight: 500;">status(500).json({ error: 'Internal server error' }); }); app.listen(PORT, () => { console.log(`[${new Date().toISOString()}] Server running on port ${PORT}`); console.log(`Health check: http://localhost:${PORT}/health`); }); // Graceful shutdown process.on('SIGTERM', () => { console.log('SIGTERM received, shutting down gracefully'); process.exit(0); }); pm2 -weight: 500;">start app.js --name "webhook-bot" pm2 -weight: 500;">start app.js --name "webhook-bot" pm2 -weight: 500;">start app.js --name "webhook-bot" β”Œβ”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ id β”‚ name β”‚ namespace β”‚ mode β”‚ -weight: 500;">status β”‚ -weight: 500;">restart β”‚ β”œβ”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ 0 β”‚ webhook-bot β”‚ default β”‚ fork β”‚ online β”‚ 0 β”‚ β””β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”Œβ”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ id β”‚ name β”‚ namespace β”‚ mode β”‚ -weight: 500;">status β”‚ -weight: 500;">restart β”‚ β”œβ”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ 0 β”‚ webhook-bot β”‚ default β”‚ fork β”‚ online β”‚ 0 β”‚ β””β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”Œβ”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ id β”‚ name β”‚ namespace β”‚ mode β”‚ -weight: 500;">status β”‚ -weight: 500;">restart β”‚ β”œβ”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ 0 β”‚ webhook-bot β”‚ default β”‚ fork β”‚ online β”‚ 0 β”‚ β””β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -weight: 500;">curl -X POST http://localhost:3001/webhook/event \ -H "Content-Type: application/json" \ -d '{"event_type":"test","data":{"message":"hello"}}' -weight: 500;">curl -X POST http://localhost:3001/webhook/event \ -H "Content-Type: application/json" \ -d '{"event_type":"test","data":{"message":"hello"}}' -weight: 500;">curl -X POST http://localhost:3001/webhook/event \ -H "Content-Type: application/json" \ -d '{"event_type":"test","data":{"message":"hello"}}' module.exports = { apps: [ { name: 'webhook-bot', script: './app.js', instances: 'max', exec_mode: 'cluster', env: { NODE_ENV: 'production', PORT: 3001 }, merge_logs: true, autorestart: true, watch: false, max_memory_restart: '500M', error_file: './logs/err.log', out_file: './logs/out.log', log_date_format: 'YYYY-MM-DD HH:mm:ss Z' } ] }; module.exports = { apps: [ { name: 'webhook-bot', script: './app.js', instances: 'max', exec_mode: 'cluster', env: { NODE_ENV: 'production', PORT: 3001 }, merge_logs: true, autorestart: true, watch: false, max_memory_restart: '500M', error_file: './logs/err.log', out_file: './logs/out.log', log_date_format: 'YYYY-MM-DD HH:mm:ss Z' } ] }; module.exports = { apps: [ { name: 'webhook-bot', script: './app.js', instances: 'max', exec_mode: 'cluster', env: { NODE_ENV: 'production', PORT: 3001 }, merge_logs: true, autorestart: true, watch: false, max_memory_restart: '500M', error_file: './logs/err.log', out_file: './logs/out.log', log_date_format: 'YYYY-MM-DD HH:mm:ss Z' } ] }; pm2 -weight: 500;">stop webhook-bot pm2 -weight: 500;">start ecosystem.config.js pm2 -weight: 500;">stop webhook-bot pm2 -weight: 500;">start ecosystem.config.js pm2 -weight: 500;">stop webhook-bot pm2 -weight: 500;">start ecosystem.config.js β”Œβ”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ id β”‚ name β”‚ namespace β”‚ mode β”‚ -weight: 500;">status β”‚ -weight: 500;">restart β”‚ β”œβ”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ 0 β”‚ webhook-bot β”‚ default β”‚ cluster β”‚ online β”‚ 0 β”‚ β”‚ 1 β”‚ webhook-bot β”‚ default β”‚ cluster β”‚ online β”‚ 0 β”‚ β”‚ 2 β”‚ webhook-bot β”‚ default β”‚ cluster β”‚ online β”‚ 0 β”‚ β”‚ 3 β”‚ webhook-bot β”‚ default β”‚ cluster β”‚ online β”‚ 0 β”‚ β””β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”Œβ”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ id β”‚ name β”‚ namespace β”‚ mode β”‚ -weight: 500;">status β”‚ -weight: 500;">restart β”‚ β”œβ”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ 0 β”‚ webhook-bot β”‚ default β”‚ cluster β”‚ online β”‚ 0 β”‚ β”‚ 1 β”‚ webhook-bot β”‚ default β”‚ cluster β”‚ online β”‚ 0 β”‚ β”‚ 2 β”‚ webhook-bot β”‚ default β”‚ cluster β”‚ online β”‚ 0 β”‚ β”‚ 3 β”‚ webhook-bot β”‚ default β”‚ cluster β”‚ online β”‚ 0 β”‚ β””β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”Œβ”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ id β”‚ name β”‚ namespace β”‚ mode β”‚ -weight: 500;">status β”‚ -weight: 500;">restart β”‚ β”œβ”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ 0 β”‚ webhook-bot β”‚ default β”‚ cluster β”‚ online β”‚ 0 β”‚ β”‚ 1 β”‚ webhook-bot β”‚ default β”‚ cluster β”‚ online β”‚ 0 β”‚ β”‚ 2 β”‚ webhook-bot β”‚ default β”‚ cluster β”‚ online β”‚ 0 β”‚ β”‚ 3 β”‚ webhook-bot β”‚ default β”‚ cluster β”‚ online β”‚ 0 β”‚ β””β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ pm2 logs webhook-bot pm2 logs webhook-bot pm2 logs webhook-bot pm2 logs webhook-bot --lines 100 pm2 logs webhook-bot --lines 100 pm2 logs webhook-bot --lines 100 tail -f ~/projects/automation-bot/logs/out.log tail -f ~/projects/automation-bot/logs/out.log tail -f ~/projects/automation-bot/logs/out.log pm2 -weight: 500;">install pm2-logrotate pm2 -weight: 500;">install pm2-logrotate pm2 -weight: 500;">install pm2-logrotate pm2 set pm2-logrotate:max_size 10M pm2 set pm2-logrotate:retain 7 pm2 set pm2-logrotate:max_size 10M pm2 set pm2-logrotate:retain 7 pm2 set pm2-logrotate:max_size 10M pm2 set pm2-logrotate:retain 7 pm2 startup pm2 startup pm2 startup -weight: 600;">sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u ubuntu --hp /home/ubuntu -weight: 600;">sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u ubuntu --hp /home/ubuntu -weight: 600;">sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u ubuntu --hp /home/ubuntu -weight: 600;">sudo reboot -weight: 600;">sudo reboot -weight: 600;">sudo reboot -weight: 500;">systemctl -weight: 500;">status pm2-ubuntu -weight: 500;">systemctl -weight: 500;">status pm2-ubuntu -weight: 500;">systemctl -weight: 500;">status pm2-ubuntu cat > .env << 'EOF' PORT=3001 NODE_ENV=production N8N_WEBHOOK_URL=https://your-n8n-domain.com/webhook/ cat > .env << 'EOF' PORT=3001 NODE_ENV=production N8N_WEBHOOK_URL=https://your-n8n-domain.com/webhook/ cat > .env << 'EOF' PORT=3001 NODE_ENV=production N8N_WEBHOOK_URL=https://your-n8n-domain.com/webhook/ - n8n Cloud or self-hosted n8n (for orchestrating automated workflows) - Hetzner VPS or Contabo VPS for hosting your Node.js processes - DigitalOcean as an alternative hosting provider - Node.js 18+ installed locally and on your server - SSH access to your VPS - PM2 (we'll -weight: 500;">install this together) - A text editor (VS Code recommended) - Why PM2 Matters for Bot Automation - Installation & Initial Setup - Running Your First Process - Clustering & Load Balancing - Monitoring & Logs in Real-Time - Auto-Restart on Server Reboot - Integration with n8n Webhooks - Getting Started - Restarts crashed processes automatically (no manual intervention) - Clusters your app across CPU cores (load balancing built-in) - Manages logs (no more digging through syslog) - Survives server reboots (auto--weight: 500;">start on startup) - Monitors memory and CPU (alerts when things go wrong) - Handles zero-downtime deployments (reload without dropping connections)