Deploying Node.js / Express Apps (PM2 + Nginx)
"Deploying Node.js / Express Applications" covers running your app as a daemon with the PM2 process manager — including automatic restarts and log management — alongside placing Nginx in front as a reverse proxy. PM2 runs Node.js apps as daemons, providing automatic restarts on crashes, startup auto-launch configuration, and real-time log monitoring. Combined with Nginx, you can efficiently handle HTTPS termination, static file serving, and virtual host management for multiple applications.
Syntax
# -----------------------------------------------
# Install PM2 (globally)
# -----------------------------------------------
# npm i -g pm2
# → Installs PM2 globally
# Example: sudo npm install -g pm2
# -----------------------------------------------
# Start, stop, and restart apps
# -----------------------------------------------
# pm2 start {script} --name {name}
# → Starts a Node.js app as a daemon
# → Assigning a name with --name makes it easier to manage
# Example: pm2 start app.js --name akane-express
# pm2 stop {name|id|all}
# → Stops a process (it remains in the process list)
# Example: pm2 stop akane-express
# Example: pm2 stop all
# pm2 restart {name|id|all}
# → Restarts a process. Use this after updating your code
# Example: pm2 restart akane-express
# pm2 reload {name|id|all}
# → Restarts a process with zero downtime
# → Effective when running in cluster mode
# Example: pm2 reload akane-express
# pm2 delete {name|id|all}
# → Stops a process and removes it from the list entirely
# Example: pm2 delete akane-express
# -----------------------------------------------
# Monitoring processes, logs, and dashboard
# -----------------------------------------------
# pm2 list
# → Displays a table of all managed processes
# pm2 logs {name|id}
# → Streams logs in real time (stdout + stderr)
# → Omit the argument to show logs for all processes
# Example: pm2 logs akane-express
# Example: pm2 logs akane-express --lines 100
# pm2 monit
# → Opens a real-time terminal dashboard showing CPU and memory usage
# -----------------------------------------------
# Configure auto-start on OS boot
# -----------------------------------------------
# pm2 startup
# → Generates a systemd unit so PM2 starts automatically on OS boot
# → You must copy and run the command that is printed
# pm2 save
# → Saves the current process list to a dump file
# → Used together with pm2 startup to restore processes after a server reboot
# -----------------------------------------------
# Ecosystem file (manage multiple apps at once)
# -----------------------------------------------
# pm2 ecosystem
# → Generates a template ecosystem.config.js
# pm2 start ecosystem.config.js
# → Starts all apps defined in the ecosystem file at once
Command Reference
| Command | Description |
|---|---|
pm2 start {script} --name {name} | Starts a Node.js app as a daemon. Assigning a name with --name lets you reference it by name in stop / restart. |
pm2 stop {name|id|all} | Stops a process. Specifying all stops every managed process. The process remains in the list. |
pm2 restart {name|id|all} | Restarts a process. Run this after updating your code. |
pm2 reload {name|id|all} | Performs a zero-downtime restart in cluster mode. Code changes are applied without interrupting the service. |
pm2 delete {name|id|all} | Stops a process and removes it from the list entirely. Use this to clean up apps you no longer need. |
pm2 list | Displays a list of managed processes, including status, CPU, memory usage, and restart count. |
pm2 logs {name|id} | Streams logs in real time. Use --lines {N} to scroll back N lines. |
pm2 monit | Opens a terminal dashboard that shows CPU and memory usage in real time. |
pm2 startup | Generates a systemd unit file so PM2 starts automatically on OS boot. Run the printed command as instructed. |
pm2 save | Saves the current process list to a dump file. Used with pm2 startup to restore processes automatically after a server reboot. |
pm2 ecosystem | Generates a template ecosystem.config.js. Lets you centrally manage multiple apps and environment variables. |
pm2 start ecosystem.config.js | Starts all apps defined in the ecosystem file at once. Switching between production and development environments is straightforward. |
Examples
Start an app with PM2 and configure auto-start
# ----------------------------------------------- # Start an Express app with PM2 and configure it # to start automatically after an OS reboot # ----------------------------------------------- # Navigate to the app directory and install dependencies cd /var/www/akane-express npm install --production # Start the app with PM2 # The name given with --name can be used in pm2 list and pm2 logs pm2 start app.js --name akane-express # ----------------------------------------------- # Configure auto-start on OS boot # ----------------------------------------------- # pm2 startup prints the command to create a systemd unit # Copy and run the printed command exactly as shown pm2 startup # Save the current process list # This ensures akane-express restarts automatically after an OS reboot pm2 save
Run the following command:
$ pm2 list ┌────┬──────────────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐ │ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │ ├────┼──────────────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤ │ 0 │ akane-express │ default │ 1.4.2 │ fork │ 12483 │ 3D │ 0 │ online │ 0% │ 58.2mb │ akane │ disabled │ │ 1 │ ginoza-api │ default │ 2.1.0 │ cluster │ 12501 │ 3D │ 0 │ online │ 0% │ 61.7mb │ akane │ disabled │ └────┴──────────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
/etc/nginx/sites-available/akane-express.conf
# -----------------------------------------------
# Nginx reverse proxy configuration
# Forwarding to a Node.js / Express app (port 3000)
# -----------------------------------------------
server {
listen 80;
server_name akane.psycho-pass.example.com;
# Redirect all HTTP requests to HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name akane.psycho-pass.example.com;
# SSL certificate (specify the files obtained from Let's Encrypt)
ssl_certificate /etc/letsencrypt/live/akane.psycho-pass.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/akane.psycho-pass.example.com/privkey.pem;
# -----------------------------------------------
# Nginx serves static files directly
# without proxying through Node.js, for better performance
# -----------------------------------------------
location /static/ {
alias /var/www/akane-express/public/;
expires 30d;
access_log off;
}
# -----------------------------------------------
# Forward all other requests to the local Express app
# -----------------------------------------------
location / {
proxy_pass http://127.0.0.1:3000;
# Pass the client's real IP and protocol to the app
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;
# Add the following settings if you use WebSockets
# proxy_http_version 1.1;
# proxy_set_header Upgrade $http_upgrade;
# proxy_set_header Connection "upgrade";
}
}
Running these commands produces the following output:
$ sudo nginx -t nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful $ sudo systemctl reload nginx $ pm2 logs akane-express --lines 5 [TAILING] Tailing last 5 lines for [akane-express] process (change the value with --lines option) /root/.pm2/logs/akane-express-out.log last 5 lines: 0|akane-ex | Tsunemori Akane's dashboard started (port: 3000) 0|akane-ex | GET /api/inspectors 200 12ms 0|akane-ex | GET /api/dominators 200 8ms 0|akane-ex | POST /api/crime-coefficient 201 34ms 0|akane-ex | GET /static/img/psycho-pass-logo.png 304 2ms
Manage multiple apps with ecosystem.config.js
# ----------------------------------------------- # Generate a template ecosystem.config.js # ----------------------------------------------- pm2 ecosystem
The same logic can also be written as:
// ecosystem.config.js
// Manages Tsunemori Akane's Express app and Ginoza's API server together
module.exports = {
apps: [
{
// The main Express app managed by Akane
name : 'akane-express',
script : './app.js',
instances: 1,
exec_mode: 'fork',
env: {
NODE_ENV: 'development',
PORT : 3000,
},
env_production: {
NODE_ENV: 'production',
PORT : 3000,
},
// PM2 automatically restarts the app if memory exceeds 1 GB
max_memory_restart: '1G',
// Specify where log files are saved
out_file : '/var/log/akane-express/out.log',
error_file: '/var/log/akane-express/err.log',
},
{
// The internal API server managed by Ginoza
name : 'ginoza-api',
script : './api/server.js',
instances: 2,
exec_mode: 'cluster',
env_production: {
NODE_ENV: 'production',
PORT : 3001,
},
},
],
};
The same logic can also be written as:
# Start in production mode (env_production is applied) pm2 start ecosystem.config.js --env production # Save the process list so it is restored automatically after an OS reboot pm2 save
Overview
When deploying Node.js / Express applications, the combination of PM2 and Nginx is widely used. PM2 is a process manager dedicated to Node.js that provides automatic restarts on crashes, multi-core utilization via cluster mode, and auto-start configuration after OS reboots using pm2 startup and pm2 save. Nginx sits in front as a reverse proxy, handling SSL termination, fast static file serving, and load balancing. The Node.js app listens locally on a port such as 3000, while Nginx accepts external requests on port 443 and forwards them — meaning you do not need to run the Node.js process as root. For details on Nginx reverse proxy configuration, see the Nginx Reverse Proxy page. For how PM2's generated systemd unit works, see the systemd Unit page.
If you find any errors or copyright issues, please contact us.