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.
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
Install Nginx
β± 2 min
Start with a fresh system update, then install Nginx from the default Ubuntu repositories.
$ 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.
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.
# 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
'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.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.
$ 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:
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; } }
# 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
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.
# 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.
$ certbot renew --dry-run # Should output: "Congratulations, all simulated renewals succeeded"
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:
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;
}
}
$ nginx -t # Test for syntax errors $ systemctl reload nginx
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
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
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; }
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.Test & Verify Everything Works
β± 5 min
Run these checks to confirm your reverse proxy and SSL are working correctly:
Check SSL grade
# 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
$ curl -I http://yourdomain.com # Expected output: # HTTP/1.1 301 Moved Permanently # Location: https://yourdomain.com/
Verify SSL certificate details
$ 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
$ tail -f /var/log/nginx/error.log $ tail -f /var/log/nginx/access.log
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
app.set('trust proxy', 1). In Django, add USE_X_FORWARDED_HOST = True in settings.py.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.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.