Back to tutorials
Tutorial

Linux VPS Node.js Application Deployment Tutorial: Complete PM2, Nginx, and SSL Setup for Production in 2026

Learn Node.js application deployment on Linux VPS with PM2 process manager, Nginx reverse proxy, SSL certificates, and production monitoring.

By Anurag Singh
Updated on May 08, 2026
Category: Tutorial
Share article
Linux VPS Node.js Application Deployment Tutorial: Complete PM2, Nginx, and SSL Setup for Production in 2026

Setting Up the Node.js Environment on Ubuntu 24.04

Before deploying your Node.js application on a VPS, you need a properly configured runtime environment. This tutorial covers production deployment using Ubuntu 24.04 LTS with PM2 process management and Nginx as a reverse proxy.

Start by updating your system and installing Node.js through the NodeSource repository:

sudo apt update
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs
node --version

The NodeSource repository provides the latest stable Node.js 20.x LTS release. Verify npm is installed alongside Node.js by running npm --version.

Preparing Your Application for Linux VPS Node.js Deployment

Production applications require specific configurations that differ from development setups. Create a dedicated user account for your application to improve security:

sudo adduser nodeapp
sudo usermod -aG sudo nodeapp
sudo su - nodeapp

Clone your application repository into the user's home directory.

Starting from scratch? Create a sample Express.js application:

mkdir ~/myapp
cd ~/myapp
npm init -y
npm install express
cat > app.js << 'EOF'
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;

app.get('/', (req, res) => {
  res.json({ message: 'Hello from Node.js on VPS!', timestamp: new Date() });
});

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});
EOF

Create a package.json start script that works with PM2:

{
  "scripts": {
    "start": "node app.js",
    "dev": "node app.js"
  }
}

Installing and Configuring PM2 Process Manager

PM2 handles process management, automatic restarts, logging, and monitoring. Install PM2 globally and configure it for your application:

sudo npm install -g pm2
cd ~/myapp
pm2 start app.js --name "myapp"
pm2 save
pm2 startup

The pm2 startup command generates a system service configuration. Copy and run the provided command as root to enable automatic startup on boot.

Create a PM2 ecosystem file for advanced configuration:

cat > ecosystem.config.js << 'EOF'
module.exports = {
  apps: [{
    name: 'myapp',
    script: './app.js',
    instances: 2,
    exec_mode: 'cluster',
    env: {
      NODE_ENV: 'production',
      PORT: 3000
    },
    error_file: './logs/err.log',
    out_file: './logs/out.log',
    log_file: './logs/combined.log',
    time: true
  }]
};
EOF

This configuration runs two instances in cluster mode for better CPU utilization.

Create the logs directory and restart PM2 with the new configuration:

mkdir logs
pm2 delete myapp
pm2 start ecosystem.config.js

Nginx Reverse Proxy Configuration

Nginx serves as a reverse proxy to handle SSL termination, static file serving, and load distribution.

Install Nginx and remove the default configuration:

sudo apt install nginx
sudo rm /etc/nginx/sites-enabled/default

Create a new server block for your application:

sudo cat > /etc/nginx/sites-available/myapp << 'EOF'
server {
    listen 80;
    server_name yourdomain.com www.yourdomain.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_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;
        proxy_read_timeout 300s;
        proxy_connect_timeout 75s;
    }

    location /health {
        access_log off;
        return 200 "healthy\n";
        add_header Content-Type text/plain;
    }
}
EOF

Enable the configuration and test the Nginx setup:

sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx

The health check endpoint allows monitoring tools to verify your application status without generating access logs.

SSL Certificate Installation with Let's Encrypt

Secure your application with SSL certificates from Let's Encrypt. Install Certbot and obtain certificates:

sudo apt install snapd
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

Certbot automatically modifies your Nginx configuration to handle SSL.

The updated server block includes redirects and security headers:

server {
    listen 443 ssl http2;
    server_name yourdomain.com www.yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
    
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Frame-Options DENY always;
    add_header X-Content-Type-Options nosniff always;

    location / {
        proxy_pass http://localhost:3000;
        # ... (same proxy configuration as before)
    }
}

Test automatic renewal to keep certificates current:

sudo certbot renew --dry-run

Deploying on HostMyCode Node.js hosting? SSL certificates are already integrated into the management panel for easier configuration.

Application Performance Monitoring and Logging

Monitor your application performance using PM2's built-in monitoring capabilities.

View real-time statistics:

pm2 monit
pm2 logs myapp --lines 100
pm2 show myapp

Set up log rotation to prevent disk space issues:

pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 50M
pm2 set pm2-logrotate:retain 7
pm2 set pm2-logrotate:compress true

Configure application-level logging with proper error handling:

const winston = require('winston');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
    new winston.transports.File({ filename: 'logs/combined.log' })
  ]
});

if (process.env.NODE_ENV !== 'production') {
  logger.add(new winston.transports.Console({
    format: winston.format.simple()
  }));
}

Database Integration and Environment Variables

Most applications require database connectivity. Install and configure your preferred database.

For MongoDB:

sudo apt install -y mongodb
sudo systemctl start mongodb
sudo systemctl enable mongodb

Create a .env file for environment variables:

cat > .env << 'EOF'
NODE_ENV=production
PORT=3000
MONGO_URI=mongodb://localhost:27017/myapp
JWT_SECRET=your-secure-jwt-secret
EOF

Install dotenv to load environment variables:

npm install dotenv
# Add to the top of app.js:
require('dotenv').config();

Update your PM2 ecosystem configuration to use the environment file:

module.exports = {
  apps: [{
    name: 'myapp',
    script: './app.js',
    instances: 2,
    exec_mode: 'cluster',
    env_file: '.env'
  }]
};

For database-heavy applications, consider HostMyCode database hosting for optimized performance and automated backups.

Security Hardening and Firewall Configuration

Secure your deployment by configuring UFW firewall and implementing security middleware.

Enable UFW and allow necessary ports:

sudo ufw enable
sudo ufw allow ssh
sudo ufw allow 'Nginx Full'
sudo ufw status

Add security middleware to your Express application:

npm install helmet rate-limiter-flexible

const helmet = require('helmet');
const { RateLimiterMemory } = require('rate-limiter-flexible');

const rateLimiter = new RateLimiterMemory({
  keyGenerator: (req) => req.ip,
  points: 100, // Number of requests
  duration: 60, // Per 60 seconds
});

app.use(helmet());
app.use(async (req, res, next) => {
  try {
    await rateLimiter.consume(req.ip);
    next();
  } catch (rejRes) {
    res.status(429).send('Too Many Requests');
  }
});

This configuration prevents common attacks and implements rate limiting to protect against abuse.

Deployment Automation and CI/CD Integration

Automate deployments using a simple bash script that handles application updates:

cat > deploy.sh << 'EOF'
#!/bin/bash
set -e

echo "Starting deployment..."
cd ~/myapp
git pull origin main
npm install --production
pm2 reload ecosystem.config.js
echo "Deployment completed successfully!"
EOF

chmod +x deploy.sh

For more advanced CI/CD, create a GitHub Actions workflow:

name: Deploy to VPS
on:
  push:
    branches: [ main ]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
    - name: Deploy to server
      uses: appleboy/ssh-action@v0.1.5
      with:
        host: ${{ secrets.HOST }}
        username: ${{ secrets.USERNAME }}
        key: ${{ secrets.KEY }}
        script: |
          cd ~/myapp
          ./deploy.sh

Performance Optimization and Scaling

Optimize your application performance with caching and compression.

Enable Nginx gzip compression:

sudo nano /etc/nginx/nginx.conf
# Add to http block:
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;

Implement Redis caching for frequently accessed data:

sudo apt install redis-server
npm install redis

const redis = require('redis');
const client = redis.createClient();

app.get('/api/data/:id', async (req, res) => {
  const cacheKey = `data:${req.params.id}`;
  
  try {
    const cached = await client.get(cacheKey);
    if (cached) {
      return res.json(JSON.parse(cached));
    }
    
    const data = await fetchDataFromDB(req.params.id);
    await client.setex(cacheKey, 3600, JSON.stringify(data));
    res.json(data);
  } catch (error) {
    res.status(500).json({ error: 'Server error' });
  }
});

Monitor application performance and scale horizontally when needed.

Add more PM2 instances or deploy additional servers with load balancing as your traffic grows.

Deploy your applications with confidence on HostMyCode Node.js hosting. Our managed VPS solutions include pre-configured PM2, Nginx, and automated SSL certificates, making production deployment straightforward and reliable.

Frequently Asked Questions

How many PM2 instances should I run for my Node.js application?

Start with one instance per CPU core, then adjust based on your application's memory usage and performance requirements. Monitor CPU and memory utilization using pm2 monit to find the optimal configuration.

What's the difference between PM2 fork and cluster mode?

Fork mode runs a single instance of your application. Cluster mode creates multiple instances that share the same port. Cluster mode provides better CPU utilization and automatic load distribution across worker processes.

How do I handle database connections in cluster mode?

Use connection pooling libraries like mysql2 or pg-pool that handle multiple worker processes automatically. Avoid singleton database connections that don't work across cluster instances.

Should I serve static files through Node.js or Nginx?

Always serve static files through Nginx rather than Node.js. Nginx handles static file serving much more efficiently, freeing up your application resources for business logic. Configure a separate location block in your Nginx configuration for static assets.

How do I troubleshoot PM2 application crashes?

Check PM2 logs using pm2 logs myapp and examine both error logs and application output. Use pm2 describe myapp to see restart counts and error details. Most crashes result from unhandled promise rejections or memory leaks.