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. If you prefer a Docker-native approach, check out our Traefik guide instead.

Why This Matters

Why This Matters

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 (also important for VPS security)
  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. For an even simpler setup, consider a self-hosted PaaS like Coolify or Dokploy.

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

// related guides

Andrius Putna

Andrius Putna

I am Andrius Putna. Geek. Since early 2000 in love tinkering with web technologies. Now AI. Bridging business and technology to drive meaningful impact. Combining expertise in customer experience, technology, and business strategy to deliver valuable insights. Father, open-source contributor, investor, 2xIronman, MBA graduate.

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