Set Up Production Nginx Load Balancer on Ubuntu VPS: Complete Tutorial

By Raman Kumar

Share:

Updated on May 04, 2026

Set Up Production Nginx Load Balancer on Ubuntu VPS: Complete Tutorial

Why Production Load Balancing Matters for VPS Hosting

Running multiple application servers behind a load balancer isn't just for enterprise companies anymore. Small businesses, agencies, and hosting customers need reliable traffic distribution to handle growth spikes and maintain uptime.

When your single server hits CPU limits or needs maintenance, a properly configured load balancer keeps your sites running.

This tutorial shows you how to set up Nginx load balancer on Ubuntu VPS using practical configurations that work in real hosting environments. We'll cover SSL termination, health checks, and failover scenarios your customers actually encounter.

You'll learn to distribute traffic across multiple backend servers while maintaining session consistency and handling server failures gracefully. Whether you're managing client sites on a Hostperl VPS or scaling your own applications, this setup provides the foundation for reliable multi-server hosting.

Prerequisites and Server Requirements

You need at least three Ubuntu VPS instances for this setup: one load balancer and two backend servers. Each server should run Ubuntu 20.04 or later with root access.

Minimum specifications per server:

  • 2GB RAM (4GB recommended for production)
  • 20GB SSD storage
  • Network connectivity between all servers
  • Public IP addresses for the load balancer

Install these packages on your load balancer server:

sudo apt update && sudo apt upgrade -y
sudo apt install nginx certbot python3-certbot-nginx -y

Enable and start Nginx:

sudo systemctl enable nginx
sudo systemctl start nginx

Configure Backend Application Servers

Your backend servers need web applications running on specific ports. For this tutorial, we'll use simple Nginx servers, but the same principles apply to Apache, Node.js, Python, or PHP applications.

On Backend Server 1 (replace with your server's IP):

sudo apt install nginx -y
sudo systemctl enable nginx

Create a custom index page to identify this server:

sudo tee /var/www/html/index.html << EOF


    

Backend Server 1

Server IP: $(hostname -I | awk '{print $1}')

Timestamp: $(date)

EOF

Configure Nginx to listen on port 8080:

sudo tee /etc/nginx/sites-available/backend << EOF
server {
    listen 8080;
    server_name _;
    
    root /var/www/html;
    index index.html;
    
    location /health {
        access_log off;
        return 200 "healthy";
        add_header Content-Type text/plain;
    }
    
    location / {
        try_files \$uri \$uri/ =404;
    }
}
EOF

Enable the configuration and restart Nginx:

sudo ln -s /etc/nginx/sites-available/backend /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl restart nginx

Repeat these steps on Backend Server 2, changing "Backend Server 1" to "Backend Server 2" in the HTML file.

Set Up Main Nginx Load Balancer Configuration

Now configure the main load balancer. Create the upstream configuration file:

sudo tee /etc/nginx/conf.d/upstream.conf << EOF
upstream backend_pool {
    least_conn;
    server 10.0.0.10:8080 weight=1 max_fails=3 fail_timeout=30s;
    server 10.0.0.11:8080 weight=1 max_fails=3 fail_timeout=30s;
    keepalive 32;
}
EOF

Replace the IP addresses with your actual backend server IPs. The least_conn directive distributes requests to the server with the fewest active connections.

The max_fails and fail_timeout parameters handle server failures automatically.

Create the main load balancer virtual host:

sudo tee /etc/nginx/sites-available/loadbalancer << EOF
server {
    listen 80;
    server_name your-domain.com www.your-domain.com;
    
    # Security headers
    add_header X-Frame-Options DENY;
    add_header X-Content-Type-Options nosniff;
    add_header X-XSS-Protection "1; mode=block";
    
    # Load balancer health check
    location /nginx-health {
        access_log off;
        return 200 "healthy";
        add_header Content-Type text/plain;
    }
    
    # Proxy to backend servers
    location / {
        proxy_pass http://backend_pool;
        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 5s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }
}
EOF

Enable the configuration:

sudo ln -s /etc/nginx/sites-available/loadbalancer /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx

Implement SSL Termination with Let's Encrypt

SSL termination at the load balancer level simplifies certificate management and reduces backend server overhead. Install your SSL certificate using Certbot:

sudo certbot --nginx -d your-domain.com -d www.your-domain.com

This automatically modifies your Nginx configuration to include SSL settings. For enhanced security, create a custom SSL configuration:

sudo tee /etc/nginx/conf.d/ssl.conf << EOF
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_stapling on;
ssl_stapling_verify on;
EOF

Update your load balancer configuration to handle both HTTP and HTTPS:

sudo tee /etc/nginx/sites-available/loadbalancer << EOF
server {
    listen 80;
    server_name your-domain.com www.your-domain.com;
    return 301 https://\$server_name\$request_uri;
}

server {
    listen 443 ssl http2;
    server_name your-domain.com www.your-domain.com;
    
    ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
    
    # Security headers
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
    add_header X-Frame-Options DENY;
    add_header X-Content-Type-Options nosniff;
    add_header X-XSS-Protection "1; mode=block";
    
    location /nginx-health {
        access_log off;
        return 200 "healthy";
        add_header Content-Type text/plain;
    }
    
    location / {
        proxy_pass http://backend_pool;
        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 https;
        proxy_cache_bypass \$http_upgrade;
        
        proxy_connect_timeout 5s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }
}
EOF

Test and reload the configuration:

sudo nginx -t && sudo systemctl reload nginx

Configure Health Checks and Failover

Nginx automatically removes failed servers from the pool, but you can enhance this with custom health checks. Create a monitoring script:

sudo tee /usr/local/bin/backend-health-check.sh << 'EOF'
#!/bin/bash

BACKEND_SERVERS=("10.0.0.10:8080" "10.0.0.11:8080")
LOG_FILE="/var/log/nginx/health-check.log"

for server in "${BACKEND_SERVERS[@]}"; do
    if curl -f -s "http://$server/health" > /dev/null; then
        echo "$(date): $server is healthy" >> "$LOG_FILE"
    else
        echo "$(date): $server is down - sending alert" >> "$LOG_FILE"
        # Add your alerting mechanism here (email, Slack, etc.)
    fi
done
EOF

Make it executable and set up a cron job:

sudo chmod +x /usr/local/bin/backend-health-check.sh
sudo crontab -e

Add this line to run health checks every 2 minutes:

*/2 * * * * /usr/local/bin/backend-health-check.sh

For immediate server testing, you can manually check backend status:

# Test backend servers directly
curl -I http://10.0.0.10:8080/health
curl -I http://10.0.0.11:8080/health

# Test through load balancer
curl -I http://your-domain.com/

Configure Session Persistence and Sticky Sessions

Some applications require users to stay on the same backend server. Configure IP-based session persistence by modifying your upstream block:

sudo tee /etc/nginx/conf.d/upstream.conf << EOF
upstream backend_pool {
    ip_hash;
    server 10.0.0.10:8080 weight=1 max_fails=3 fail_timeout=30s;
    server 10.0.0.11:8080 weight=1 max_fails=3 fail_timeout=30s;
    keepalive 32;
}
EOF

The ip_hash directive ensures requests from the same IP address always go to the same backend server. This works well for applications that store session data locally rather than in shared storage.

For cookie-based session persistence, you need Nginx Plus or a third-party module. Most VPS hosting solutions work fine with IP-based persistence for typical web applications.

Performance Optimization and Monitoring

Optimize your load balancer for production traffic. Add these settings to your /etc/nginx/nginx.conf file within the http block:

# Worker process optimization
worker_processes auto;
worker_connections 1024;

# Buffer sizes
client_body_buffer_size 10K;
client_header_buffer_size 1k;
client_max_body_size 8m;
large_client_header_buffers 4 4k;

# Timeouts
client_body_timeout 12;
client_header_timeout 12;
keepalive_timeout 15;
send_timeout 10;

# Compression
gzip on;
gzip_comp_level 6;
gzip_min_length 1000;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript;

# Connection limits
limit_conn_zone $binary_remote_addr zone=conn_limit_per_ip:10m;
limit_req_zone $binary_remote_addr zone=req_limit_per_ip:10m rate=5r/s;

Monitor load balancer performance with built-in status pages. Add this location block to your server configuration:

location /nginx_status {
    stub_status on;
    access_log off;
    allow 127.0.0.1;
    allow your-admin-ip;
    deny all;
}

This provides real-time connection statistics accessible at https://your-domain.com/nginx_status.

Testing Load Balancer Functionality

Verify your setup works correctly with these tests. First, check that traffic distributes properly:

# Send multiple requests and observe server responses
for i in {1..10}; do
    curl -s http://your-domain.com/ | grep "Backend Server"
    sleep 1
done

Test failover by stopping one backend server:

# On Backend Server 1
sudo systemctl stop nginx

# From load balancer, test continued availability
curl -I http://your-domain.com/

All requests should now go to Backend Server 2. Start the stopped server and verify traffic resumes to both:

# On Backend Server 1
sudo systemctl start nginx

# Test distribution resumes
for i in {1..6}; do
    curl -s http://your-domain.com/ | grep "Backend Server"
done

Use load testing tools like ab or wrk for performance validation:

sudo apt install apache2-utils -y
ab -n 1000 -c 10 http://your-domain.com/

Monitor logs during testing to identify any issues:

sudo tail -f /var/log/nginx/access.log
sudo tail -f /var/log/nginx/error.log

Security Hardening for Production

Secure your load balancer against common attacks. Configure rate limiting in your server block:

location / {
    limit_conn conn_limit_per_ip 10;
    limit_req zone=req_limit_per_ip burst=20 nodelay;
    
    proxy_pass http://backend_pool;
    # ... other proxy settings
}

Hide server information and add security headers:

# Add to http block in nginx.conf
server_tokens off;

# Add to server block
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;

Configure firewall rules to restrict access to backend servers. On each backend server, only allow connections from your load balancer:

sudo ufw allow from your-load-balancer-ip to any port 8080
sudo ufw deny 8080

For additional security, consider implementing fail2ban on the load balancer to automatically block suspicious IPs, similar to the setup described in our Fail2ban tutorial.

Setting up production load balancers requires reliable infrastructure and proper monitoring. Hostperl VPS hosting provides the performance and network reliability you need for multi-server setups like this.

Frequently Asked Questions

How many backend servers can Nginx load balance?

Nginx can handle hundreds of backend servers in a single upstream pool. The practical limit depends on your server's memory and the complexity of your health checks.

Most production setups work well with 2-20 backend servers per pool.

What happens if all backend servers fail?

Nginx will return a 502 Bad Gateway error when all upstream servers are unavailable. You can configure a backup server using the backup parameter in your upstream block.

You can also create a custom error page to display a maintenance message.

Can I use different ports for each backend server?

Yes, each server in your upstream block can use different ports. This is useful when running multiple application instances on the same server: server 10.0.0.10:8080 and server 10.0.0.10:8081.

How do I add HTTPS to backend connections?

Change proxy_pass http://backend_pool to proxy_pass https://backend_pool and add proxy_ssl_verify off; if using self-signed certificates on backends. However, most setups terminate SSL at the load balancer level for better performance.

What's the difference between least_conn and ip_hash load balancing?

The least_conn method sends requests to the server with fewest active connections, providing better resource distribution. The ip_hash method ensures the same client always reaches the same server, maintaining session consistency but potentially creating uneven load distribution.