Why Your Apps Need a Reverse Proxy
If you run more than one application on a single server — a Node.js API on port 3000, a Python backend on 8000, a database admin tool on 8080 — you have probably faced the problem: how do you serve all of them on the standard web ports (80 and 443) without port numbers in every URL?
This is what a reverse proxy solves. It sits in front of your application servers, accepts client requests on ports 80 and 443, and forwards each request to the correct backend based on the domain name or URL path. Your users see https://app.example.com and https://api.example.com instead of https://example.com:3000 and https://example.com:8000.
Beyond clean URLs, a reverse proxy gives you SSL termination (one place to manage certificates), load balancing across multiple backend instances, caching, rate limiting, and a central place to enforce security policies. Nginx is the most widely used reverse proxy in production — it handles millions of requests per second at companies like Netflix, Cloudflare, and WordPress.com, and it works just as well on a single $10 VPS.
In this guide, we will walk through installing Nginx from the official repository, configuring it as a reverse proxy for multiple backends, enabling SSL with Let’s Encrypt, adding load balancing, WebSocket support, and production hardening. By the end, you will have a professional-grade reverse proxy that you can drop in front of any web application.
What You Will Need
Before you start, you need:
- A Linux server (Ubuntu 22.04/24.04, Debian 12, or Rocky Linux 9)
- Root or sudo access
- A domain name pointed to your server’s IP address
- At least one backend application to proxy to (Node.js, Python, static files — any HTTP server works)
- Ports 80 and 443 open in your firewall
For this guide, we recommend a Cloud VPS with at least 1 GB of RAM and 2 CPU cores, running Ubuntu 24.04 LTS. Nginx itself uses very little memory (idle: ~64 MB), but you need room for your application servers too. Canadian Web Hosting offers Cloud VPS plans with Canadian data centres in Vancouver and Toronto, 24/7 support, and full root access — exactly what you need to set up your own reverse proxy.
Installing Nginx from the Official Repository
We strongly recommend using the official Nginx repository rather than your distribution’s default package. Distribution packages (like Ubuntu’s nginx package from apt) often trail the stable release by months. The official Nginx repository gives you the latest stable binary with all modules compiled in.
Ubuntu and Debian
sudo apt update
sudo apt install curl gnupg2 ca-certificates lsb-release ubuntu-keyring
# Import the official Nginx signing key
curl -fsSL https://nginx.org/keys/nginx_signing.key | sudo gpg --dearmor
-o /usr/share/keyrings/nginx-archive-keyring.gpg
# Add the stable repository (replace 'ubuntu' with 'debian' for Debian)
echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg]
https://nginx.org/packages/ubuntu `lsb_release -cs` nginx" |
sudo tee /etc/apt/sources.list.d/nginx.list
sudo apt update
sudo apt install nginx
RHEL, Rocky Linux, AlmaLinux
sudo tee /etc/yum.repos.d/nginx.repo << 'EOF'
[nginx-stable]
name=nginx stable repo
baseurl=https://nginx.org/packages/centos/$releasever/$basearch/
gpgcheck=1
enabled=1
gpgkey=https://nginx.org/keys/nginx_signing.key
module_hotfixes=true
EOF
sudo dnf install nginx
Docker (Containerized Environments)
If you are already running your applications in Docker containers, a Docker-based Nginx proxy fits naturally into your stack. The nginx:stable-alpine image is about 7 MB and includes all common modules.
docker pull nginx:stable-alpine
docker run --name nginx-proxy -p 80:80 -p 443:443
-v /path/to/nginx.conf:/etc/nginx/nginx.conf:ro
nginx:stable-alpine
Verify the installation:
nginx -v
# Expected output: nginx version: nginx/x.y.z # version varies by repository and release channel
Basic Reverse Proxy Configuration
Now that Nginx is installed, let us configure it as a reverse proxy. The core directive is proxy_pass, which tells Nginx where to forward incoming requests.
Single Backend Proxy
The simplest case: you have one application running on localhost:3000 and you want to serve it at https://app.example.com.
Create a new configuration file in /etc/nginx/conf.d/app.conf:
server {
listen 80;
server_name app.example.com;
location / {
proxy_pass http://localhost:3000;
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;
}
}
What each header does:
Host $host— Passes the original hostname so the backend knows what domain was requested. Without this, the backend seeslocalhost:3000and may generate incorrect redirects or links.X-Real-IP $remote_addr— Passes the real client IP address. Without this, the backend sees all requests as coming from Nginx’s IP (127.0.0.1).X-Forwarded-For $proxy_add_x_forwarded_for— Appends the client IP to any existing X-Forwarded-For chain, preserving the full proxy hop history.X-Forwarded-Proto $scheme— Tells the backend whether the original request was HTTP or HTTPS. Essential when Nginx handles SSL termination or your backend generates protocol-sensitive redirects.
Test the configuration and reload:
sudo nginx -t
# Expected: syntax is ok, test is successful
sudo systemctl reload nginx
# Verify with curl
curl -I http://localhost
Multiple Domains on One Server
The real power of a reverse proxy is serving multiple applications on a single server using different domain names. Create one server block per domain:
# /etc/nginx/conf.d/api.conf
server {
listen 80;
server_name api.example.com;
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;
}
}
# /etc/nginx/conf.d/blog.conf
server {
listen 80;
server_name blog.example.com;
location / {
proxy_pass http://localhost:8080;
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;
}
}
Nginx matches the Host header from the incoming request to the server_name directive and routes it to the correct backend. No port numbers in URLs, no conflict between applications.
Path-Based Routing
If you want to serve multiple applications under the same domain but different URL paths, use separate location blocks:
server {
listen 80;
server_name example.com;
location /api/ {
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;
}
location /app/ {
proxy_pass http://localhost:3000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location / {
root /var/www/static;
index index.html;
}
}
Important gotcha: When proxy_pass includes a URI path (the trailing slash in http://localhost:4000/), Nginx replaces the matching location prefix with that URI path. A request to /api/login becomes http://localhost:4000/login, not http://localhost:4000/api/login. If you omit the trailing slash (proxy_pass http://localhost:4000;), the full request URI is passed unchanged. Get this wrong and your application routes will break in subtle ways.
Ops Note: Test the Proxy Before Touching DNS
The safest reverse proxy rollout is local first: validate nginx -t, curl the backend directly, curl the proxy locally with the expected host header, then change DNS or load balancer routing. Most production mistakes come from skipping one of those checks and discovering a bad upstream, header, or TLS path only after traffic arrives.
SSL Termination with Let’s Encrypt
A reverse proxy is the ideal place to handle SSL termination. One certificate, one place to manage renewals, and your backends can communicate over plain HTTP internally (on a private network or localhost).
Install Certbot and obtain a certificate:
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d app.example.com
Certbot automatically modifies your Nginx configuration to add the SSL server block and redirect HTTP to HTTPS. The resulting config looks like this:
server {
listen 443 ssl http2;
server_name app.example.com;
ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
location / {
proxy_pass http://localhost:3000;
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;
}
}
server {
listen 80;
server_name app.example.com;
return 301 https://$host$request_uri;
}
The http2 parameter on the listen directive enables HTTP/2, which multiplexes requests over a single connection and can significantly reduce page load times for sites with many assets. Certbot also sets up automatic renewal via a systemd timer — no manual certificate management needed.
Key security detail: Always set X-Forwarded-Proto $scheme when you terminate SSL at Nginx. Without this header, your backend application thinks it is running over HTTP and may generate HTTPS redirects that create redirect loops, especially with frameworks like Django or Ruby on Rails that enforce HTTPS.
Load Balancing Across Multiple Backends
When one backend instance is not enough, Nginx can distribute traffic across multiple servers using the upstream block:
upstream backend {
least_conn;
server app1.example.com:3000 weight=3;
server app2.example.com:3000 weight=2;
server 10.0.0.3:3000;
}
server {
listen 80;
server_name app.example.com;
location / {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
| Method | Directive | Behaviour |
|---|---|---|
| Round-robin | (default) | Distributes evenly in order |
| Least connections | least_conn |
Sends to the server with fewest active connections |
| IP hash | ip_hash |
Consistent routing by client IP (session stickiness) |
| Generic hash | hash $request_uri |
Custom key-based distribution |
For most setups, least_conn is the best default — it naturally balances load when requests have varying processing times. Use ip_hash only if your application requires session persistence and you are not using a shared session store like Redis.
WebSocket Proxying
Applications using WebSockets (live chat, real-time dashboards, collaborative editing) need special handling because the connection needs to be upgraded from HTTP to the WebSocket protocol. Nginx has supported WebSocket proxying since version 1.3.13:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
server_name chat.example.com;
location /ws/ {
proxy_pass http://localhost:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_read_timeout 86400s;
}
}
The critical setting: proxy_read_timeout 86400s (24 hours). Without this, Nginx closes idle WebSocket connections after the default 60-second timeout. Your WebSocket connections will drop every 60 seconds of inactivity, causing reconnection errors in the browser.
Also ensure your backend application sends periodic ping/pong frames to keep the connection alive. Most WebSocket libraries do this automatically, but some need explicit configuration.
Verifying Your Proxy Is Working
After configuring your reverse proxy, run through these checks to confirm everything works:
# 1. Configuration syntax check (always first)
sudo nginx -t
# Expected: syntax is ok, test is successful
# 2. Verify Nginx is running and listening
sudo ss -tlnp | grep nginx
# Expected: shows 0.0.0.0:80 and 0.0.0.0:443 (if SSL configured)
# 3. Test proxy routing locally
curl -I http://localhost
# Expected: HTTP/1.1 200 OK (or whichever status your backend returns)
# 4. Test via domain name
curl -H "Host: app.example.com" http://localhost/ -w "nHTTP %{http_code}n"
# Expected: 200 OK, proxied to your backend
# 5. Check upstream response headers
curl -I https://app.example.com/ 2>/dev/null | grep -i "x-forwarded"
# Expected: X-Forwarded-For, X-Forwarded-Proto headers present
# 6. Enable the status module for live metrics
# Add to /etc/nginx/conf.d/status.conf:
# server {
# listen 127.0.0.1:80;
# location = /nginx_status {
# stub_status;
# allow 127.0.0.1;
# deny all;
# }
# }
The stub_status module gives you real-time metrics:
curl http://127.0.0.1/nginx_status
# Sample output:
# Active connections: 291
# server accepts handled requests
# 16630948 16630948 31070465
# Reading: 6 Writing: 179 Waiting: 106
What to look for: The Reading and Writing counters should be low in steady state. A high Reading count may indicate slow clients. handled should equal accepts — if it is lower, Nginx is hitting resource limits (increase worker_connections).
Production Hardening
A reverse proxy is a security boundary. Here is how to lock it down for production use.
Firewall Rules
Only expose ports 80 and 443 to the internet. Block all other ports at the firewall level:
# UFW (Ubuntu/Debian)
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw default deny incoming
sudo ufw enable
# firewalld (RHEL/Rocky)
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload
Your backend applications (port 3000, 4000, etc.) should bind to localhost or a private network, never 0.0.0.0. Test this: ss -tlnp | grep 3000 should show 127.0.0.1:3000, not 0.0.0.0:3000.
Security Headers
Add these headers at the proxy level so every proxied application inherits them:
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header X-XSS-Protection "0" always;
Rate Limiting
Protect your backends from abuse with Nginx’s built-in rate limiting:
# In the http block of nginx.conf
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
# In the location block
location /api/ {
limit_req zone=api_limit burst=20 nodelay;
proxy_pass http://backend;
}
This limits each client IP to 10 requests per second, with a burst allowance of 20 before requests are dropped. Adjust the rate based on your application’s normal traffic patterns.
Hide Nginx Version
Prevent attackers from targeting version-specific vulnerabilities:
# In the http block of nginx.conf
server_tokens off;
This removes the Nginx version number from error pages and response headers.
Troubleshooting Common Issues
“upstream sent too big header” in Error Logs
Symptom: Nginx returns 502 Bad Gateway, and the error log shows upstream sent too big header.
Cause: The default proxy buffer size (4 KB) is too small for your backend’s response headers. Common triggers: large authentication cookies, SAML/OAuth responses, or custom application headers.
Fix: Increase the buffer sizes in the offending location block:
location / {
proxy_buffer_size 8k;
proxy_buffers 8 8k;
proxy_busy_buffers_size 16k;
proxy_pass http://localhost:3000;
}
WebSocket Connections Dropping After 60 Seconds
Symptom: Real-time features (chat, live dashboards) disconnect after exactly 60 seconds of inactivity and the client tries to reconnect.
Cause: Nginx’s default proxy_read_timeout is 60 seconds. When the WebSocket has no data for 60 seconds, Nginx closes the connection.
Fix: Increase the timeout for WebSocket locations:
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
Redirect Loop After Enabling HTTPS
Symptom: Your browser shows a redirect loop error after enabling HTTPS.
Cause: Your backend application detects an HTTP request and redirects to HTTPS, but Nginx terminates SSL and forwards to the backend over HTTP. The backend keeps redirecting because it does not know that the original request was HTTPS.
Fix: Ensure these headers are set in your proxy configuration:
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
Then configure your application framework to trust the X-Forwarded-Proto header. For example, in Django:
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
proxy_pass Trailing Slash Pitfalls
Symptom: Your application routes are not matching correctly. Requests to /app/login reach the backend as /login or /apilogin depending on your configuration.
Rule of thumb: If proxy_pass has a URI path (e.g., http://backend/api/), the matching location prefix is replaced by that URI path. If it has no URI (just http://backend), the full original request URI is passed through. Test with a small change if unsure — add or remove the trailing slash and use curl -I to observe the difference.
Next Steps
Once your reverse proxy is running, here are the logical next steps for your server infrastructure:
- Set up a Cloud VPS if you have not already — a reverse proxy on a dedicated server gives you full control over your whole stack
- Add server monitoring to track Nginx metrics, SSL expiry, and upstream health
- Implement automated backups for your Nginx configuration files and SSL certificates
- Check out our Caddy: Reverse Proxy with Auto-HTTPS guide if you prefer an alternative that handles SSL certificates automatically
- Set up Docker properly on your VPS before deploying containerized applications behind your proxy
If managing your own reverse proxy sounds like more than you want to take on, Canadian Web Hosting offers Managed Support — our team handles the setup, security patches, and ongoing maintenance so you can focus on your applications.
If your reverse proxy stops working because a container won’t start, our Docker deployment troubleshooting guide covers port conflicts and other common causes.
Ops Note: Test the Proxy Before Touching DNS
The safest reverse proxy rollout is local first: validate nginx -t, curl the backend directly, curl the proxy locally with the expected host header, then change DNS or load balancer routing. Most production mistakes come from skipping one of those checks and discovering a bad upstream, header, or TLS path only after traffic arrives.
Sources and Command Notes
This Nginx reverse proxy guide was refreshed in June 2026. The sample proxy server block was smoke-tested with nginx -t in Ubuntu 24.04. Package repository steps and current Nginx versions can vary by distribution, so verify against the official Nginx package documentation for your OS before production use.
Be First to Comment