You have built a Node.js application locally and it works perfectly on localhost:3000. Now you need it running in production, accessible to the world, with HTTPS, automatic restarts, and zero downtime. This guide takes you from a fresh Linux VPS to a fully deployed Node.js application with Nginx as a reverse proxy, PM2 as the process manager, and Let's Encrypt for SSL.
We will use Ubuntu 24.04 LTS throughout this tutorial. The same principles apply to any Express, Fastify, Next.js, or other Node.js-based application.
Prerequisites
- A VPS running Ubuntu 24.04 LTS with at least 1 GB RAM
- A non-root user with sudo privileges (see our VPS setup guide if you need help with this)
- A domain name pointed to your server's IP address
- Your Node.js application code in a Git repository
Step 1: Install Node.js
Install Node.js using the NodeSource repository to get the latest LTS version. As of this writing, that is Node.js 22:
# Install Node.js 22 LTS via NodeSource
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install nodejs -y
# Verify the installation
node --version
npm --version
Alternatively, if you need to manage multiple Node.js versions (for example, if you run several apps that require different versions), install nvm:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.bashrc
nvm install 22
nvm use 22
Step 2: Clone and Set Up Your Application
Create a directory for your application and clone your code:
# Install Git if not already present
sudo apt install git -y
# Clone your repository
cd /home/deploy
git clone https://github.com/yourusername/your-app.git
cd your-app
# Install dependencies
npm install --production
# If your app has a build step (Next.js, TypeScript, etc.)
npm run build
Test that the application runs correctly:
node app.js # or: npm start
Verify it is listening on the expected port (typically 3000), then stop it with Ctrl+C. We will use PM2 to manage it properly in the next step.
Step 3: Set Up PM2 Process Manager
Running your Node.js app directly with node app.js is fine for development, but in production you need a process manager that handles automatic restarts, log management, and cluster mode. PM2 is the industry standard:
# Install PM2 globally
sudo npm install -g pm2
# Start your application with PM2
pm2 start app.js --name "my-app"
# Or if you use npm start:
pm2 start npm --name "my-app" -- start
Useful PM2 Commands
# View running processes
pm2 list
# View logs
pm2 logs my-app
# Restart the app
pm2 restart my-app
# Stop the app
pm2 stop my-app
# Monitor CPU/memory in real time
pm2 monit
Enable Cluster Mode
If your application is stateless (most Express/Fastify APIs are), PM2 can run multiple instances across your CPU cores, dramatically increasing throughput:
# Run one instance per CPU core
pm2 start app.js --name "my-app" -i max
# Or specify the number of instances
pm2 start app.js --name "my-app" -i 2
Auto-Start on Boot
Configure PM2 to start automatically when your server reboots:
# Generate the startup script
pm2 startup systemd
# PM2 will output a command - copy and run it. It looks like:
# sudo env PATH=$PATH:/usr/bin pm2 startup systemd -u deploy --hp /home/deploy
# Save the current process list
pm2 save
Now if your server reboots for any reason, PM2 will automatically restart your application.
Step 4: Set Up Environment Variables
Never hardcode secrets, API keys, or database credentials in your code. Create a .env file:
nano /home/deploy/your-app/.env
NODE_ENV=production
PORT=3000
DATABASE_URL=mongodb://localhost:27017/myapp
JWT_SECRET=your-secret-key-here
API_KEY=your-api-key-here
Make sure this file is not in your Git repository (add it to .gitignore). If your app uses dotenv, it will load these variables automatically. For PM2, you can also use an ecosystem file:
nano ecosystem.config.js
module.exports = {
apps: [{
name: 'my-app',
script: 'app.js',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
PORT: 3000
}
}]
};
# Start with the ecosystem file
pm2 start ecosystem.config.js
Step 5: Install and Configure Nginx
Nginx acts as a reverse proxy, forwarding incoming HTTP/HTTPS requests to your Node.js application. This gives you SSL termination, static file serving, request buffering, and the ability to run multiple apps on one server:
sudo apt install nginx -y
sudo systemctl enable nginx
Create a server block configuration for your domain:
sudo nano /etc/nginx/sites-available/myapp.com
server {
listen 80;
server_name myapp.com www.myapp.com;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
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;
proxy_cache_bypass $http_upgrade;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Serve static files directly (if applicable)
location /static/ {
alias /home/deploy/your-app/public/;
expires 30d;
add_header Cache-Control "public, immutable";
}
}
Enable the site and test:
sudo ln -s /etc/nginx/sites-available/myapp.com /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx
Visit http://myapp.com in your browser. You should see your Node.js application.
Step 6: Set Up SSL with Let's Encrypt
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d myapp.com -d www.myapp.com
Certbot will automatically update your Nginx configuration to redirect HTTP to HTTPS and configure the certificate. Verify auto-renewal works:
sudo certbot renew --dry-run
Step 7: Configure the Firewall
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo ufw status
This allows SSH (port 22), HTTP (port 80), and HTTPS (port 443). Your Node.js app's port (3000) does not need to be open because Nginx proxies traffic to it internally.
Step 8: Set Up Zero-Downtime Deployments
When you push new code, you want to update the server without dropping active connections. Create a deployment script:
nano /home/deploy/deploy.sh
#!/bin/bash
set -e
APP_DIR="/home/deploy/your-app"
echo "Pulling latest code..."
cd $APP_DIR
git pull origin main
echo "Installing dependencies..."
npm install --production
echo "Building (if applicable)..."
npm run build 2>/dev/null || true
echo "Reloading application..."
pm2 reload my-app
echo "Deployment complete!"
chmod +x /home/deploy/deploy.sh
The key command is pm2 reload (not pm2 restart). In cluster mode, PM2 reload performs a rolling restart: it brings up new instances with the updated code before shutting down old ones, so there is zero downtime.
Step 9: Set Up Log Management
PM2 stores logs in ~/.pm2/logs/. Without rotation, these files will grow indefinitely. Install the log rotation module:
pm2 install pm2-logrotate
# Configure: rotate at 10MB, keep 30 files
pm2 set pm2-logrotate:max_size 10M
pm2 set pm2-logrotate:retain 30
pm2 set pm2-logrotate:compress true
Production Checklist
Before considering your deployment "done," verify each of these items:
- NODE_ENV is set to production. This enables performance optimizations in Express and other frameworks.
- Error handling is in place. Unhandled promise rejections and uncaught exceptions should be logged, not silently swallowed.
- Rate limiting is configured. Use
express-rate-limitor Nginx rate limiting to prevent abuse. - CORS is properly set. Only allow origins that should access your API.
- Helmet.js is installed (for Express apps). It sets security-related HTTP headers automatically.
- Database connections use connection pooling. Do not open a new connection per request.
- Health check endpoint exists. A simple
/healthroute that returns a 200 status is useful for monitoring. - Backups are automated. Set up daily database backups at a minimum.
Deploy Your Node.js App on Reliable Infrastructure
A well-deployed Node.js application needs infrastructure that does not let it down. MassiveGRID's Cloud VPS plans start at $1.99/month and run on Proxmox HA clusters with Ceph distributed storage. That means your application data is replicated across multiple physical drives, and if a hardware node fails, your VPS automatically migrates to a healthy one. With data centers in New York, London, Frankfurt, and Singapore, you can deploy close to your users for the lowest possible latency. Pick your specs, choose your region, and deploy in minutes.