Setting Up Nginx as a Reverse Proxy with SSL
TUTORIAL 8 min read fordnox

Setting Up Nginx as a Reverse Proxy with SSL

Configure Nginx as a reverse proxy with free SSL certificates from Let's Encrypt. Route multiple domains to different services on a single VPS.


Setting Up Nginx as a Reverse Proxy with SSL

Running multiple applications on a single VPS? Nginx as a reverse proxy lets you route traffic to different services, handle SSL certificates automatically, and improve performance—all on one server.

Why This Matters

Without a reverse proxy, you'd need:

A reverse proxy solves all of this. One entry point, multiple backends, automatic HTTPS.

Real benefits:

Prerequisites

Step 1: Install Nginx

# Update packages
sudo apt update

# Install Nginx
sudo apt install nginx -y

# Start and enable
sudo systemctl start nginx
sudo systemctl enable nginx

# Verify it's running
sudo systemctl status nginx

Open your server's IP in a browser—you should see the Nginx welcome page.

Step 2: Understand the Directory Structure

/etc/nginx/
├── nginx.conf              # Main config (rarely edit)
├── sites-available/        # All site configs
├── sites-enabled/          # Symlinks to active configs
├── snippets/               # Reusable config snippets
└── conf.d/                 # Additional configs

Step 3: Configure Your First Reverse Proxy

Let's say you have a Node.js app running on port 3000. Create a config:

sudo nano /etc/nginx/sites-available/myapp.example.com

Add:

server {
    listen 80;
    listen [::]:80;
    server_name myapp.example.com;

    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;
    }
}

Enable the site:

# Create symlink
sudo ln -s /etc/nginx/sites-available/myapp.example.com /etc/nginx/sites-enabled/

# Test configuration
sudo nginx -t

# Reload Nginx
sudo systemctl reload nginx

Step 4: Install Certbot for Free SSL

# Install Certbot and Nginx plugin
sudo apt install certbot python3-certbot-nginx -y

Step 5: Get Your SSL Certificate

sudo certbot --nginx -d myapp.example.com

Certbot will:

  1. Verify you own the domain
  2. Generate certificates
  3. Automatically configure Nginx for HTTPS
  4. Set up auto-renewal

Your config is now updated with SSL settings automatically.

Step 6: Verify Auto-Renewal

# Test renewal process
sudo certbot renew --dry-run

# Check the timer
sudo systemctl status certbot.timer

Certificates auto-renew before expiry. No manual intervention needed.

Step 7: Add Multiple Applications

For each new app, create a config file:

API Backend (port 4000):

sudo nano /etc/nginx/sites-available/api.example.com
server {
    listen 80;
    server_name api.example.com;

    location / {
        proxy_pass http://127.0.0.1:4000;
        proxy_http_version 1.1;
        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;
    }
}

Static Website (different directory):

server {
    listen 80;
    server_name static.example.com;

    root /var/www/static.example.com;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}

Enable and get SSL:

sudo ln -s /etc/nginx/sites-available/api.example.com /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d api.example.com

Step 8: Add Security Headers

Create a reusable snippet:

sudo nano /etc/nginx/snippets/security-headers.conf
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;

Include in your server blocks:

server {
    # ... other config ...
    
    include snippets/security-headers.conf;
    
    location / {
        # ... proxy settings ...
    }
}

Step 9: Configure WebSocket Support

For apps using WebSockets (chat, real-time features):

location /ws {
    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_read_timeout 86400;  # 24 hours for long connections
}

Step 10: Set Up Rate Limiting

Protect against abuse in your main nginx.conf:

sudo nano /etc/nginx/nginx.conf

Add in the http block:

http {
    # Define rate limit zones
    limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
    limit_req_zone $binary_remote_addr zone=api:10m rate=30r/s;
    
    # ... rest of config ...
}

Apply in server blocks:

location / {
    limit_req zone=general burst=20 nodelay;
    proxy_pass http://127.0.0.1:3000;
    # ...
}

location /api/ {
    limit_req zone=api burst=50 nodelay;
    proxy_pass http://127.0.0.1:4000;
    # ...
}

Step 11: Enable Gzip Compression

sudo nano /etc/nginx/conf.d/gzip.conf
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied any;
gzip_comp_level 6;
gzip_types
    text/plain
    text/css
    text/xml
    text/javascript
    application/json
    application/javascript
    application/xml
    application/xml+rss
    application/x-javascript
    image/svg+xml;

Step 12: Set Up Basic Load Balancing

For multiple backend servers:

upstream myapp_backends {
    least_conn;  # Send to least busy server
    server 127.0.0.1:3000 weight=3;
    server 127.0.0.1:3001 weight=2;
    server 127.0.0.1:3002 backup;  # Only if others fail
}

server {
    listen 80;
    server_name myapp.example.com;

    location / {
        proxy_pass http://myapp_backends;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        # ... other headers ...
    }
}

Complete Production Config Example

Here's a full production-ready config:

upstream app_backend {
    server 127.0.0.1:3000;
    keepalive 32;
}

server {
    listen 80;
    listen [::]:80;
    server_name myapp.example.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name myapp.example.com;

    # SSL (managed by Certbot, but you can customize)
    ssl_certificate /etc/letsencrypt/live/myapp.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/myapp.example.com/privkey.pem;
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:50m;
    ssl_session_tickets off;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
    ssl_prefer_server_ciphers off;

    # HSTS
    add_header Strict-Transport-Security "max-age=63072000" always;

    # Security headers
    include snippets/security-headers.conf;

    # Logging
    access_log /var/log/nginx/myapp.access.log;
    error_log /var/log/nginx/myapp.error.log;

    # Rate limiting
    limit_req zone=general burst=20 nodelay;

    # Proxy settings
    location / {
        proxy_pass http://app_backend;
        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_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }

    # Static files (if served by Nginx)
    location /static/ {
        alias /var/www/myapp/static/;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

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

Best Practices

  1. One config file per domain - Easier to manage and debug
  2. Always test before reload - nginx -t catches syntax errors
  3. Use includes for common settings - DRY principle applies to configs
  4. Set appropriate timeouts - Don't let slow clients tie up connections
  5. Enable HTTP/2 - Free performance boost for HTTPS
  6. Log separately per app - Easier debugging and analysis
  7. Use upstream blocks - Even for single servers, makes scaling easier
  8. Keep SSL config updated - Security best practices evolve

Common Mistakes to Avoid

Forgetting to test config - One typo breaks all sites

Not enabling sites - Creating config without symlink does nothing

Wrong proxy_pass trailing slash - /api vs /api/ behaves differently

Missing WebSocket headers - Real-time features silently fail

Ignoring upstream keepalive - Creates unnecessary connections

SSL certificate mismatch - Certificate must match server_name exactly

No rate limiting - Your server becomes a DoS target

Serving static files through app - Let Nginx handle static content directly

Debugging Tips

# Check Nginx error log
sudo tail -f /var/log/nginx/error.log

# Check specific site log
sudo tail -f /var/log/nginx/myapp.error.log

# Test configuration
sudo nginx -t

# Check if port is listening
sudo ss -tlnp | grep nginx

# View active connections
sudo nginx -T | grep server_name

FAQ

How many sites can one Nginx server handle?

Nginx is incredibly efficient. A modest Hostinger VPS can easily handle 50+ low-traffic sites or several high-traffic ones. Memory is usually the limiting factor.

Should I use Apache or Nginx?

Nginx for reverse proxy, almost always. It handles concurrent connections better and uses less memory. Apache is fine for traditional PHP hosting with mod_php.

What's the difference between sites-available and sites-enabled?

sites-available stores all configs. sites-enabled contains symlinks to active ones. This lets you disable a site without deleting its config.

How do I handle www vs non-www?

Add both to server_name and redirect one to the other:

server {
    listen 80;
    server_name www.example.com;
    return 301 https://example.com$request_uri;
}

Why isn't my SSL certificate working?

Common causes: DNS not pointing to server, port 80/443 blocked by firewall, wrong server_name, or Certbot couldn't verify ownership. Check sudo certbot certificates for status.

Can I use Nginx with Docker?

Absolutely! You can either run Nginx in a container or on the host proxying to Docker containers. See our Docker Compose guide for details.


Next steps: Set up monitoring to track your Nginx performance and catch issues before they become problems.

~/nginx-reverse-proxy-guide/get-started

Ready to get started?

Get the best VPS hosting deal today. Hostinger offers 4GB RAM VPS starting at just $4.99/mo.

Get Hostinger VPS — $4.99/mo

// up to 75% off + free domain included

// related topics

Nginx reverse proxy SSL setup Let's Encrypt Nginx configuration VPS web server

fordnox

Expert VPS reviews and hosting guides. We test every provider we recommend.

// last updated: February 6, 2026. Disclosure: This article may contain affiliate links.