By Akshay Singh
How to Host a Node.js Express App on a VPS Using Nginx, PM2, and Systemd
When you build a Node.js application using Express, testing it locally is simple: you run node server.js or npm start, and your app listens on port 3000.
However, running that same command in a production environment is a terrible idea. If your application crashes due to an unhandled exception, it will stay down. If the server reboots, your app won't restart automatically. Furthermore, exposing a Node.js process directly to port 80 (HTTP) or 443 (HTTPS) requires root privileges and lacks the security and performance optimizations of a dedicated web server.
To host a Node.js application professionally, you need three components:
- A Process Manager (PM2 or Systemd) to restart the app if it crashes and start it on server boot.
- A Web Server / Reverse Proxy (Nginx) to handle incoming HTTP/HTTPS traffic, manage SSL certificates, and route requests to your Node.js app.
- A Linux VPS (Virtual Private Server) to run everything.
In this comprehensive guide, I will show you how to deploy a Node.js Express app on a Ubuntu VPS. We will cover both PM2 and native Systemd for process management, and configure Nginx as a reverse proxy.
Prerequisites
Before diving in, ensure you have:
- A Linux VPS running Ubuntu 22.04 LTS or 24.04 LTS.
- SSH access to your server.
- Node.js and npm installed on your server. (If not, install them using NodeSource).
- A domain name pointing to your server's public IP address.
Step 1: Set Up Your Express Application
First, SSH into your server:
ssh your-username@your-server-ip
Create a directory for your application and navigate into it. The standard location for web applications is /var/www/.
sudo mkdir -p /var/www/my-express-app
sudo chown -R $USER:$USER /var/www/my-express-app
cd /var/www/my-express-app
For the sake of this tutorial, let's create a minimal Express application. If you have an existing app, you can clone it via Git instead.
Initialize the project and install Express:
npm init -y
npm install express
Create a server.js file:
nano server.js
Add the following code:
const express = require('express');
const app = express();
const port = process.env.PORT || 3000;
app.get('/', (req, res) => {
res.send('Hello from Express behind Nginx!');
});
app.listen(port, () => {
console.log(`App running on http://localhost:${port}`);
});
Save and exit.
Step 2: Process Management
As mentioned, we cannot simply run node server.js and walk away. We need a way to keep the application alive. You have two excellent options: PM2 (easier, feature-rich) or Systemd (native, zero dependencies). I will cover both; choose the one that best fits your needs.
Option A: Using PM2
PM2 is an advanced, production process manager for Node.js. It features a built-in load balancer, log management, and automatic restarts.
- Install PM2 globally:
sudo npm install -g pm2
- Start your application:
pm2 start server.js --name "my-express-app"
- Ensure PM2 restarts on server boot:
PM2 provides a startup script that hooks into the OS init system (Systemd). Run:
pm2 startup
PM2 will output a command that you must copy and paste into your terminal (it will look something like sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u your-username --hp /home/your-username).
After running that generated command, save your current PM2 list:
pm2 save
Your app is now running in the background and will survive server reboots.
Option B: Using Native Systemd
If you prefer not to install global npm packages and want to rely on standard Linux tools, you can create a Systemd service file. This is how databases (PostgreSQL) and web servers (Nginx) are managed.
- Create a new service file:
sudo nano /etc/systemd/system/my-express-app.service
- Add the configuration:
Make sure to replace your-username with your actual Linux username, and verify the path to Node (which node).
[Unit]
Description=My Express App
After=network.target
[Service]
Environment=NODE_ENV=production
Environment=PORT=3000
Type=simple
User=your-username
WorkingDirectory=/var/www/my-express-app
ExecStart=/usr/bin/node server.js
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
- Enable and start the service:
Reload the systemd daemon to read your new file, start the service, and enable it on boot:
sudo systemctl daemon-reload
sudo systemctl start my-express-app
sudo systemctl enable my-express-app
You can check its status using sudo systemctl status my-express-app.
Step 3: Install and Configure Nginx
Right now, your Express app is running on port 3000. We need Nginx to listen on port 80 (and later 443 for HTTPS) and forward traffic to port 3000.
- Install Nginx:
sudo apt update
sudo apt install nginx -y
- Create an Nginx Server Block:
Nginx uses configuration files called server blocks. Create one for your domain:
sudo nano /etc/nginx/sites-available/my-express-app
Add the following configuration, replacing example.com with your actual domain:
server {
listen 80;
server_name example.com www.example.com;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_addrs;
}
}
Why these headers?
proxy_pass http://localhost:3000;routes the traffic to your Node app.proxy_set_header UpgradeandConnection 'upgrade'allow WebSocket connections to pass through.X-Real-IPandX-Forwarded-Forensure that Express sees the actual IP address of the client, rather than Nginx's internal IP (127.0.0.1).
- Enable the Server Block:
Create a symbolic link to the sites-enabled directory:
sudo ln -s /etc/nginx/sites-available/my-express-app /etc/nginx/sites-enabled/
Test your configuration for syntax errors:
sudo nginx -t
If the test is successful, restart Nginx:
sudo systemctl restart nginx
If your DNS is configured correctly, visiting http://example.com should now display "Hello from Express behind Nginx!".
Step 4: Secure the App with SSL (Let's Encrypt)
Never serve production traffic over HTTP. We will use Certbot to automatically fetch and configure a free SSL certificate from Let's Encrypt.
- Install Certbot:
sudo apt install certbot python3-certbot-nginx -y
- Obtain the Certificate:
Run the following command, ensuring you use the domains you specified in your Nginx configuration:
sudo certbot --nginx -d example.com -d www.example.com
Certbot will ask for an email address and prompt you to agree to their terms of service. It will automatically update your Nginx configuration to redirect all HTTP traffic to HTTPS.
- Verify Auto-Renewal:
Let's Encrypt certificates expire every 90 days, but Certbot sets up an automatic renewal timer. You can test it by running:
sudo certbot renew --dry-run
Conclusion
Deploying a Node.js application involves more than just running a script. By placing your Express app behind an Nginx reverse proxy and managing its lifecycle with PM2 or Systemd, you have created a robust, scalable, and secure production environment.
From here, you can easily host multiple Node.js applications on the same server—just run them on different internal ports (e.g., 3001, 3002) and create additional Nginx server blocks to route different domains to their respective ports.
