How to Set Up Nginx Reverse Proxy with SSL on a VPS (Step-by-Step)

How to Set Up Nginx Reverse Proxy with SSL on a VPS (Step-by-Step)

A reverse proxy sits in front of your applications and routes incoming web traffic to the right backend service. Instead of exposing multiple ports to the world, you get a clean setup where all traffic enters on port 443 (HTTPS) and Nginx forwards it to whatever app is listening on a local port.

This is the standard production setup for running Node.js, Python, Docker containers, or multiple websites on a single VPS β€” and with a free Let’s Encrypt certificate, you get HTTPS automatically.

πŸ’‘What you’ll build: By the end of this guide, your VPS will have Nginx listening on ports 80 and 443, automatically redirecting HTTP to HTTPS, and proxying requests to a backend app running on a local port β€” all secured with a free auto-renewing SSL certificate.

Prerequisites

Before starting, make sure you have:

  • A VPS running Ubuntu 22.04 LTS with root or sudo access
  • A domain name with its A record pointing to your VPS IP address
  • A backend application already running on a local port (e.g., Node.js on port 3000)
  • Port 80 and 443 open in your firewall
⚠️DNS must be live first. Certbot verifies domain ownership by making an HTTP request to your domain. If your A record hasn’t propagated yet (allow up to 48 hours, usually much faster), the SSL certificate request will fail.

STEP 1
Install Nginx
⏱ 2 min

Start with a fresh system update, then install Nginx from the default Ubuntu repositories.

bash
$ apt update && apt upgrade -y
$ apt install nginx -y

# Start Nginx and enable it on boot
$ systemctl start nginx
$ systemctl enable nginx

# Verify it's running
$ systemctl status nginx

Visit your server’s IP address in a browser β€” you should see the default Nginx welcome page. That confirms Nginx is installed and listening on port 80.

STEP 2
Configure Your Firewall
⏱ 2 min

Allow HTTP and HTTPS traffic through UFW. If you changed your SSH port, make sure that’s already open before proceeding.

bash
# Allow Nginx to handle both HTTP and HTTPS
$ ufw allow 'Nginx Full'

# Verify rules are active
$ ufw status

# Expected output includes:
# Nginx Full    ALLOW    Anywhere
πŸ’‘The 'Nginx Full' profile opens both port 80 (HTTP) and 443 (HTTPS) at once. You can later remove 'Nginx HTTP' after SSL is configured if you want to strictly enforce HTTPS.

STEP 3
Create the Nginx Server Block
⏱ 5 min

A server block (virtual host) tells Nginx how to handle requests for your domain. Create one for your domain before adding SSL β€” Certbot will modify it automatically in Step 4.

bash
$ nano /etc/nginx/sites-available/yourdomain.com

Paste the following basic configuration β€” this is a simple HTTP-only block that Certbot will upgrade to HTTPS:

/etc/nginx/sites-available/yourdomain.com
server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;

    location / {
        proxy_pass         http://localhost:3000;  # Your app's port
        proxy_http_version 1.1;
        proxy_set_header   Upgrade $http_upgrade;
        proxy_set_header   Connection 'upgrade';
        proxy_set_header   Host $host;
        proxy_cache_bypass $http_upgrade;
    }
}
bash β€” enable site
# Create symlink to enable the site
$ ln -s /etc/nginx/sites-available/yourdomain.com \
        /etc/nginx/sites-enabled/

# Test config syntax
$ nginx -t

# Reload Nginx
$ systemctl reload nginx

STEP 4
Install Free SSL with Certbot
⏱ 5 min

Certbot automatically obtains a free Let’s Encrypt SSL certificate and modifies your Nginx config to enable HTTPS and redirect HTTP traffic.

bash
# Install Certbot and the Nginx plugin
$ apt install certbot python3-certbot-nginx -y

# Request and install the certificate
$ certbot --nginx -d yourdomain.com -d www.yourdomain.com

Certbot will ask for your email address and whether to redirect HTTP to HTTPS. Choose option 2 (redirect) β€” this forces all traffic to HTTPS automatically.

bash β€” test auto-renewal
$ certbot renew --dry-run
# Should output: "Congratulations, all simulated renewals succeeded"
πŸ”’Let’s Encrypt certificates expire every 90 days. Certbot installs a systemd timer that automatically renews certificates before they expire. You don’t need to do anything manually.

STEP 5
The Complete Reverse Proxy Config (with SSL)
⏱ 5 min

After Certbot runs, your server block will have SSL sections added automatically. Here’s the production-grade complete configuration that adds important security headers, proxy timeouts, and performance settings β€” replace your config with this version:

/etc/nginx/sites-available/yourdomain.com
# Redirect HTTP β†’ HTTPS
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
return 301 https://$host$request_uri; β‘ 
}server {
listen 443 ssl;
server_name yourdomain.com www.yourdomain.com;

# SSL Certificate paths (set by Certbot)
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem; β‘‘
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;

# Modern SSL settings
ssl_protocols TLSv1.2 TLSv1.3; β‘’
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;

# Security headers
add_header Strict-Transport-Security “max-age=31536000” always; β‘£
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;

# Reverse proxy to your app
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_read_timeout 90s;
proxy_connect_timeout 90s;
proxy_cache_bypass $http_upgrade;
}
}

β‘  Permanent redirect forces all HTTP visitors to HTTPS
β‘‘ Paths automatically set by Certbot β€” don’t change these
β‘’ Only allow TLS 1.2 and 1.3 β€” older versions are insecure
β‘£ HSTS header tells browsers to always use HTTPS for this domain
β‘€ Change 3000 to your app’s actual port (8000, 8080, etc.)
β‘₯ Passes the real visitor IP to your app (not Nginx’s internal IP)
bash β€” apply config
$ nginx -t       # Test for syntax errors
$ systemctl reload nginx

STEP 6
Proxy Multiple Apps / Subdomains
⏱ 5 min per app

One of the biggest advantages of a reverse proxy is routing multiple apps or subdomains from a single server β€” all on port 443. Here are the two most common patterns:

Pattern A: Multiple subdomains β†’ different apps

api.yourdomain.com β†’ port 4000
server {
    listen 443 ssl;
    server_name api.yourdomain.com;

    # Same SSL cert if using wildcard, or run Certbot for this subdomain:
    # certbot --nginx -d api.yourdomain.com

    ssl_certificate     /etc/letsencrypt/live/api.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.yourdomain.com/privkey.pem;

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

Pattern B: Path-based routing β†’ different apps on same domain

yourdomain.com/api β†’ port 4000 | /app β†’ port 5000
location /api/ {
    proxy_pass http://localhost:4000/;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

location /app/ {
    proxy_pass http://localhost:5000/;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
πŸ’‘For each new subdomain, run certbot --nginx -d subdomain.yourdomain.com to get a separate SSL certificate. Or use a wildcard certificate (*.yourdomain.com) via DNS challenge to cover all subdomains with one cert.

STEP 7
Test & Verify Everything Works
⏱ 5 min

Run these checks to confirm your reverse proxy and SSL are working correctly:

Check SSL grade

bash β€” curl SSL check
# Verify HTTPS response and certificate
$ curl -I https://yourdomain.com

# Expected: HTTP/2 200 (or 301 redirect from HTTP)
# Check for: server: nginx

Check HTTP β†’ HTTPS redirect

bash
$ curl -I http://yourdomain.com

# Expected output:
# HTTP/1.1 301 Moved Permanently
# Location: https://yourdomain.com/

Verify SSL certificate details

bash
$ openssl s_client -connect yourdomain.com:443 -servername yourdomain.com \
  | openssl x509 -noout -dates

# Shows certificate expiry date β€” should be ~90 days from now

Check Nginx error logs if something’s wrong

bash β€” debug
$ tail -f /var/log/nginx/error.log
$ tail -f /var/log/nginx/access.log
πŸ†Test your SSL configuration at ssllabs.com/ssltest β€” the configuration in Step 5 should score an A or A+. This also verifies your certificate chain is correct and no legacy TLS versions are enabled.

βœ… Reverse Proxy Setup Checklist

Nginx installed and running (systemctl status nginx shows active)

UFW firewall allows ‘Nginx Full’ (ports 80 + 443)

Server block created in /etc/nginx/sites-available/ and symlinked to sites-enabled/

nginx -t returns “syntax is ok”

Certbot installed and SSL certificate obtained successfully

certbot renew –dry-run succeeds

HTTP β†’ HTTPS redirect working (curl -I http://yourdomain.com returns 301)

Security headers added (HSTS, X-Frame-Options, X-Content-Type-Options)

Real visitor IP passed to app via X-Real-IP header

SSL Labs score is A or A+

Frequently Asked Questions

What’s the difference between a reverse proxy and a load balancer?
A reverse proxy routes traffic from one entry point to one or more backend services on the same server. A load balancer distributes traffic across multiple servers. Nginx can do both β€” you can add upstream server groups to your config to load-balance across multiple backend instances.
My app isn’t receiving the real visitor IP β€” it’s showing 127.0.0.1. Why?
This happens when your app doesn’t read the X-Real-IP or X-Forwarded-For headers. The Nginx config in Step 5 passes these headers, but your app needs to be configured to trust and read them. In Express.js, add app.set('trust proxy', 1). In Django, add USE_X_FORWARDED_HOST = True in settings.py.
Can I use Nginx as a reverse proxy with Docker containers?
Yes β€” this is one of the most common setups. Map your Docker container to a local port (e.g., docker run -p 3000:3000), then proxy to http://localhost:3000 in Nginx just as shown in this guide. Alternatively, use Docker networks and proxy directly to the container name.
Can I use Apache instead of Nginx as a reverse proxy?
Yes. Apache’s mod_proxy module provides similar reverse proxy functionality. However, Nginx is generally preferred for reverse proxy setups due to its lower memory footprint, better concurrent connection handling, and simpler configuration syntax for this use case.
What happens to SSL if my certificate expires?
If Certbot’s auto-renewal fails (e.g., the renewal timer was accidentally disabled), your certificate will expire and browsers will show a security warning to all visitors. Run systemctl status certbot.timer to verify the renewal timer is active. You can also set up email alerts through Certbot’s configuration.

Your Reverse Proxy is Production-Ready

You now have a fully configured Nginx reverse proxy with automatic HTTPS, HTTP-to-HTTPS redirection, security headers, and proper IP forwarding. This setup can comfortably handle multiple apps and subdomains from a single VPS β€” a clean, professional architecture used by developers worldwide.

From here, explore Nginx’s caching capabilities (proxy_cache) to reduce backend load, set up rate limiting to protect your APIs, or add a wildcard SSL certificate to cover all current and future subdomains with a single cert.

 

Fast β€’ Reliable β€’ Affordable VPS - DO It Now!

Get top VPS hosting with VPS.DO’s fast, low-cost plans. Try risk-free with our 7-day no-questions-asked refund and start today!