How to Install Node.js and PM2 on Ubuntu: A Complete Production Guide
Node.js is an asynchronous, event-driven JavaScript runtime built on Chrome's V8 engine, designed to execute JavaScript code server-side at high throughput. PM2 is a production-grade process manager for Node.js applications that provides daemonization, automatic crash recovery, log aggregation, cluster mode load balancing, and startup script generation β all from a single CLI interface.
This guide covers every installation method, configuration option, and operational pattern you need to run Node.js applications reliably on Ubuntu 20.04, 22.04, or 24.04 LTS in a production environment.
Prerequisites
Before proceeding, confirm the following:
- Operating system: Ubuntu 20.04, 22.04, or 24.04 LTS (server or desktop)
- User privileges: `sudo` or root access
- Network access: Outbound HTTPS to download packages and scripts
- curl installed: Run `sudo apt install curl -y` if not already present
If you are running this on a cloud server, a VPS Hosting environment is the most common deployment target for Node.js workloads, and everything in this guide applies directly.
Step 1: Update System Packages
Always synchronize your package index and apply pending upgrades before installing new software. Stale package metadata is a frequent source of dependency conflicts.
“`bash
sudo apt update
sudo apt upgrade -y
“`
After the upgrade completes, reboot if the kernel was updated:
“`bash
sudo reboot
“`
Step 2: Install Node.js β Choosing the Right Method
There are three primary installation methods for Node.js on Ubuntu. Each has distinct trade-offs in terms of version control, isolation, and system-wide availability.
Method Comparison
| Feature | NodeSource (apt) | NVM | System apt (universe) |
|---|
| — | — | — | — |
|---|
| Version control | Single pinned major version | Multiple versions per user | Typically outdated LTS |
|---|
| System-wide install | Yes | No (per-user by default) | Yes |
|---|
| Switching versions | Requires re-running setup script | `nvm use <version>` | Not supported |
|---|
| Best for | CI/CD, single-version servers | Development, multi-project | Quick testing only |
|---|
| sudo required for npm globals | Yes | No | Yes |
|---|
| Production suitability | High | Medium | Low |
|---|
Method 1: Install Node.js via NodeSource (Recommended for Servers)
NodeSource maintains up-to-date Debian/Ubuntu repositories for every active Node.js release line. This is the preferred approach for production servers where a single, stable version is required system-wide.
Add the NodeSource repository for the current LTS release:
“`bash
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash –
“`
This script performs several actions: it detects your Ubuntu version, adds the appropriate NodeSource apt repository, imports the GPG signing key, and runs `apt-get update`. The `-E` flag preserves your environment variables when escalating to sudo, which matters if you have proxy settings configured.
Install Node.js and npm:
“`bash
sudo apt install nodejs -y
“`
The NodeSource package bundles both `node` and `npm` in a single `nodejs` package. Unlike the Ubuntu universe package, it does not split them.
Verify the installation:
“`bash
node -v
npm -v
“`
Expected output example:
“`
v20.14.0
10.7.0
“`
Pinning to a specific major version: If you need Node.js 18 instead of the current LTS, replace `setup_lts.x` with `setup_18.x` in the curl command. Available streams include `setup_18.x`, `setup_20.x`, and `setup_22.x`.
Method 2: Install Node.js via NVM (Recommended for Development and Multi-Version Environments)
NVM (Node Version Manager) installs Node.js into your home directory, requiring no root privileges for either Node.js itself or globally installed npm packages. This eliminates the permission issues that commonly arise when running `npm install -g` on system-installed Node.js.
Install NVM:
“`bash
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
“`
Check the official NVM repository for the latest release tag before running this command β the version number in the URL changes with each release.
Reload your shell environment:
“`bash
source ~/.bashrc
“`
If you use Zsh, source `~/.zshrc` instead. The NVM installer appends its initialization block to whichever shell configuration file it detects.
Install the latest LTS version of Node.js:
“`bash
nvm install –lts
“`
Install a specific version alongside the LTS:
“`bash
nvm install 18
nvm install 20
“`
Switch between installed versions:
“`bash
nvm use 20
nvm alias default 20
“`
The `alias default` command sets the version that activates in new shell sessions, which is critical for scripts and cron jobs that do not source your interactive shell profile.
Verify:
“`bash
node -v
npm -v
“`
Critical NVM pitfall for production: Because NVM is user-scoped, PM2's startup scripts and systemd units will not have access to the NVM-managed Node.js binary unless you explicitly configure the path. See Step 5 for how to handle this correctly.
Step 3: Install PM2
PM2 is distributed as an npm package and should be installed globally so its CLI is available system-wide.
Install PM2:
“`bash
sudo npm install -g pm2
“`
If you installed Node.js via NVM, omit `sudo`:
“`bash
npm install -g pm2
“`
Verify the installation:
“`bash
pm2 -v
“`
Install a specific PM2 version (useful when pinning infrastructure):
“`bash
sudo npm install -g pm2@5.3.1
“`
PM2 version 5.x is the current stable line. It introduced significant improvements to the web dashboard (pm2 plus), log rotation, and module system compared to v4.
Step 4: Managing Node.js Applications with PM2
Starting an Application
Navigate to your application directory and start it:
“`bash
cd /var/www/my-app
pm2 start app.js –name "my-app"
“`
Always assign a `–name` flag. Without it, PM2 uses the filename as the process name, which becomes ambiguous when you have multiple services.
Starting with additional options:
“`bash
pm2 start app.js –name "my-app" –watch –max-memory-restart 300M
“`
- `–watch`: Restarts the process when source files change (useful in staging, not recommended in production)
- `–max-memory-restart 300M`: Automatically restarts the process if it exceeds 300 MB of RSS memory β a critical safeguard against memory leaks
Viewing Running Processes
“`bash
pm2 list
“`
This outputs a table showing process ID, name, mode (fork/cluster), PID, status, CPU usage, memory consumption, and restart count.
For a real-time dashboard:
“`bash
pm2 monit
“`
Controlling Processes
“`bash
pm2 restart my-app # Graceful restart
pm2 reload my-app # Zero-downtime reload (cluster mode only)
pm2 stop my-app # Stop without removing from process list
pm2 delete my-app # Stop and remove from process list
“`
The difference between `restart` and `reload`: `restart` kills the process and starts a new one, causing a brief downtime. `reload` (available only in cluster mode) cycles workers one at a time, maintaining availability throughout. Always use `reload` in production cluster deployments.
Log Management
“`bash
pm2 logs # Stream logs from all processes
pm2 logs my-app # Stream logs for a specific process
pm2 logs my-app –lines 200 # Show last 200 lines
pm2 flush # Clear all log files
“`
PM2 stores logs at `~/.pm2/logs/` by default. For production servers, install the log rotation module to prevent disk exhaustion:
“`bash
pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 50M
pm2 set pm2-logrotate:retain 10
pm2 set pm2-logrotate:compress true
“`
This rotates logs when they reach 50 MB, retains 10 compressed archives, and prevents runaway log growth β a common issue on long-running servers that is frequently overlooked until disk space runs out.
Step 5: Configure PM2 to Start on System Boot
PM2 must be configured to survive server reboots. This is handled through a systemd service unit generated by PM2 itself.
Generate the startup command:
“`bash
pm2 startup
“`
PM2 will output a command tailored to your init system and current user. It looks like this:
“`
[PM2] Init System found: systemd
[PM2] To setup the Startup Script, copy/paste the following command:
sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u ubuntu –hp /home/ubuntu
“`
Copy and execute that exact command. Do not modify it β the path to the PM2 binary and the home directory must match your system's configuration.
NVM-specific startup configuration: If you installed Node.js via NVM, the path in the generated command will point to your NVM-managed binary. This is correct, but you must ensure the NVM version is set as default before running `pm2 startup`:
“`bash
nvm alias default 20
pm2 startup
“`
Save the current process list:
“`bash
pm2 save
“`
This writes the process list to `~/.pm2/dump.pm2`. On reboot, the systemd unit reads this file and restores all saved processes. If you add or remove applications later, run `pm2 save` again to update the snapshot.
Verify the systemd unit is active:
“`bash
systemctl status pm2-ubuntu
“`
Replace `ubuntu` with your actual username.
Step 6: Production Deployment with PM2 Ecosystem Files
For anything beyond a single-script application, use PM2's ecosystem configuration file. It provides reproducible, version-controlled process definitions and eliminates the need to remember long CLI flags.
Generating the Ecosystem File
“`bash
pm2 ecosystem
“`
This creates `ecosystem.config.js` in the current directory.
A Production-Ready Ecosystem Configuration
“`javascript
module.exports = {
apps: [
{
name: 'api-server',
script: './src/server.js',
instances: 'max',
exec_mode: 'cluster',
watch: false,
max_memory_restart: '500M',
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
error_file: '/var/log/pm2/api-server-error.log',
out_file: '/var/log/pm2/api-server-out.log',
merge_logs: true,
env: {
NODE_ENV: 'development',
PORT: 3000
},
env_production: {
NODE_ENV: 'production',
PORT: 8080
}
},
{
name: 'worker',
script: './src/worker.js',
instances: 2,
exec_mode: 'fork',
cron_restart: '0 2 * * *',
env_production: {
NODE_ENV: 'production'
}
}
]
};
“`
Key configuration decisions explained:
- `instances: 'max'`: PM2 automatically spawns one worker per logical CPU core. On a 4-core server, this creates 4 Node.js processes, fully utilizing the hardware.
- `exec_mode: 'cluster'`: Uses Node.js's built-in cluster module. All instances share the same port through the operating system's socket load balancing.
- `exec_mode: 'fork'`: Runs the process as a standalone child process. Required for applications that are not HTTP servers (queue workers, scheduled jobs, WebSocket servers with sticky sessions).
- `merge_logs: true`: Combines stdout from all cluster instances into a single log file, making log analysis significantly easier.
- `cron_restart`: Schedules automatic restarts using cron syntax. Useful for workers that accumulate state or for applying nightly configuration changes.
Starting with the Ecosystem File
“`bash
pm2 start ecosystem.config.js –env production
pm2 save
“`
Zero-Downtime Deployment Workflow
When deploying a new version of your application:
“`bash
git pull origin main
npm install –production
pm2 reload ecosystem.config.js –env production
“`
`pm2 reload` sends a `SIGINT` to each worker one at a time, waits for the new worker to become ready, then terminates the old one. Your application must handle `SIGINT` gracefully and signal readiness using `process.send('ready')` for this to work correctly.
Graceful shutdown handler in your application:
“`javascript
process.on('SIGINT', () => {
server.close(() => {
console.log('HTTP server closed');
process.exit(0);
});
});
“`
Step 7: PM2 Monitoring and Observability
Built-in Monitoring
“`bash
pm2 monit
“`
Displays real-time CPU and memory graphs for each process in the terminal.
Process Information
“`bash
pm2 show my-app
“`
Outputs detailed metadata: uptime, restart count, versioning, interpreter path, environment variables, and log file locations.
PM2 Web Dashboard (PM2 Plus)
PM2 offers a hosted monitoring dashboard at pm2.io. Connect your server:
“`bash
pm2 link <secret_key> <public_key>
“`
This provides historical metrics, alerting, and remote process management β particularly valuable when managing multiple servers or when you need visibility without SSH access.
Step 8: Updating Node.js and PM2
Updating PM2
“`bash
sudo npm install -g pm2@latest
pm2 update
“`
`pm2 update` is essential after upgrading the PM2 binary β it updates the in-memory PM2 daemon without interrupting running processes.
Updating Node.js via NodeSource
Re-run the setup script for the new major version:
“`bash
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash –
sudo apt install nodejs -y
“`
After updating Node.js, restart PM2 to ensure it uses the new binary:
“`bash
pm2 restart all
“`
Updating Node.js via NVM
“`bash
nvm install 22
nvm alias default 22
nvm use 22
pm2 restart all
“`
If you changed the default NVM version, regenerate the PM2 startup script to update the binary path in the systemd unit:
“`bash
pm2 unstartup
pm2 startup
pm2 save
“`
Security Hardening Considerations
Running Node.js applications in production requires attention beyond process management:
- Run as a non-root user: Never run PM2 or Node.js as root. Create a dedicated system user (`adduser –system –group nodeapp`) and run PM2 under that account.
- Environment variable management: Do not hardcode secrets in `ecosystem.config.js`. Use a `.env` file loaded via `dotenv`, or inject secrets through your deployment pipeline. The ecosystem file is typically committed to version control.
- Reverse proxy: Place Nginx or Caddy in front of your Node.js application. This handles TLS termination, static file serving, rate limiting, and request buffering. Pair this with an SSL Certificates solution to enforce HTTPS.
- Firewall rules: Block direct access to your Node.js port (e.g., 3000, 8080) from the public internet. Only the reverse proxy should communicate with Node.js.
- Resource limits: Set `max_memory_restart` in PM2 and configure system-level `ulimit` values to prevent a single runaway process from destabilizing the server.
For high-traffic production deployments where resource isolation is critical, a Dedicated Servers environment provides full hardware control and eliminates the noisy-neighbor problem inherent to shared infrastructure.
Choosing the Right Hosting Environment for Node.js
| Workload | Recommended Environment | Rationale |
|---|
| — | — | — |
|---|
| Personal projects, staging | [VPS Hosting](https://alexhost.com/vps/) | Cost-effective, full root access, scalable |
|---|
| High-traffic APIs | [Dedicated Servers](https://alexhost.com/dedicated-servers/) | Predictable performance, no resource contention |
|---|
| ML inference + Node.js | [GPU Hosting](https://alexhost.com/gpu-hosting/) | Offload compute-intensive tasks to GPU workers |
|---|
| Managed control panel | [VPS with cPanel](https://alexhost.com/vps/control-panels/cpanel-vps/) | GUI-based process and file management |
|---|
Technical Decision Checklist
Use this checklist before deploying Node.js and PM2 to production:
- [ ] Node.js installed via NodeSource (server) or NVM (development) β not the Ubuntu universe package
- [ ] PM2 installed globally with the correct permissions for your installation method
- [ ] Application started with `–name` flag and a defined `–max-memory-restart` threshold
- [ ] Cluster mode enabled for HTTP servers; fork mode used for background workers
- [ ] `pm2 startup` executed and the generated command run with sudo
- [ ] `pm2 save` run after all processes are configured
- [ ] Log rotation module installed and configured
- [ ] Nginx or Caddy configured as a reverse proxy with TLS
- [ ] Application handles `SIGINT` gracefully for zero-downtime reloads
- [ ] Secrets managed outside of `ecosystem.config.js`
- [ ] PM2 systemd unit verified with `systemctl status pm2-<username>`
- [ ] Firewall blocks direct access to Node.js ports from the public internet
FAQ
What is the difference between PM2 fork mode and cluster mode?
Fork mode spawns the application as a single child process β one instance, one CPU core utilized. Cluster mode uses Node.js's native cluster module to spawn multiple worker processes that all share the same TCP port, enabling true multi-core utilization and zero-downtime reloads. Use cluster mode for HTTP/HTTPS servers and fork mode for workers, cron jobs, or applications that maintain internal state incompatible with multi-process sharing.
Why does PM2 not restart after a server reboot even though I ran `pm2 startup`?
The most common cause is that `pm2 save` was not run after configuring processes, so the dump file is empty or missing. The second most common cause is an NVM path mismatch: if the NVM default version was changed after generating the startup script, the systemd unit points to a non-existent binary. Resolve this by running `pm2 unstartup`, setting the correct NVM default, then re-running `pm2 startup` and `pm2 save`.
Can PM2 manage non-Node.js processes?
Yes. PM2 can manage any executable by specifying the interpreter. For example: `pm2 start script.py –interpreter python3`. This makes PM2 useful as a general-purpose process supervisor for mixed-language microservice architectures.
How do I run multiple Node.js applications on different ports behind a single server?
Define each application as a separate entry in `ecosystem.config.js` with distinct `PORT` environment variables. Configure Nginx as a reverse proxy with separate `server` blocks or `location` directives routing to each port. All applications share the same PM2 daemon and are managed through a single `pm2 list` view.
Should I use NVM or NodeSource for a production VPS?
NodeSource is generally preferable for production servers. It installs Node.js as a system package, making it available to all users and system services without shell initialization dependencies. NVM's per-user, per-shell activation model introduces subtle failure modes in systemd units, cron jobs, and CI/CD pipelines that run outside an interactive shell session. Reserve NVM for local development machines where managing multiple Node.js versions simultaneously is a genuine requirement.
