How to Set Up Nginx as a Reverse Proxy (Complete Guide)

4 min read
Intermediate Nginx Reverse Proxy SSL DevOps

Prerequisites

  • Nginx installed (apt install nginx)
  • A backend application running on a port (e.g., Node.js on 3000)
  • A domain name (for SSL)

Quick Answer: Create /etc/nginx/sites-available/myapp with proxy_pass http://localhost:3000; inside a location / block. Symlink to sites-enabled. Run nginx -t && systemctl reload nginx. Add SSL with certbot --nginx -d yourdomain.com.

What is a Reverse Proxy?

Without reverse proxy:
User → :3000 → Your App

With reverse proxy:
User → :443 (Nginx) → :3000 (Your App)

Nginx sits in front of your application and handles:

  • SSL/TLS termination — HTTPS without your app dealing with certificates
  • Port routing — serve on port 80/443 instead of 3000
  • Multiple apps on one server — route by domain or path
  • Static file serving — Nginx serves static files much faster than your app
  • Load balancing — distribute traffic across multiple backend instances
  • Security — hide your app behind Nginx, add headers, rate limit

Basic Reverse Proxy

Step 1: Create the Config

Create /etc/nginx/sites-available/myapp:

server {
    listen 80;
    server_name yourdomain.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;
    }
}

Step 2: Enable the Site

ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
rm -f /etc/nginx/sites-enabled/default    # Remove default site
nginx -t                                    # Test config
systemctl reload nginx                      # Apply

Step 3: Add SSL (Let's Encrypt)

apt install certbot python3-certbot-nginx -y
certbot --nginx -d yourdomain.com

Certbot automatically modifies your Nginx config to add SSL. Auto-renewal is set up automatically.

Your site is now live at https://yourdomain.com proxying to your app on port 3000.

Common Backend Configurations

Node.js / Express

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

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

Python (Gunicorn / Uvicorn)

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

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

    # Serve static files directly (faster than Python)
    location /static/ {
        alias /opt/myapp/static/;
        expires 30d;
    }
}

Docker Container

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

    location / {
        proxy_pass http://localhost:8080;    # Docker container port
        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;
    }
}

Frontend + API (Same Domain)

server {
    listen 80;
    server_name example.com;

    # Frontend (static files)
    location / {
        root /var/www/frontend;
        try_files $uri $uri/ /index.html;    # SPA support
    }

    # API proxy
    location /api/ {
        proxy_pass http://localhost:3000/;    # Note trailing slash
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

WebSocket Support

Required for Socket.IO, real-time chat, live updates:

location /ws {
    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_read_timeout 86400;    # Keep connection open for 24h
}

Multiple Apps on One Server

By Subdomain

# api.example.com → Node.js on 3000
server {
    listen 80;
    server_name api.example.com;
    location / {
        proxy_pass http://localhost:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

# dashboard.example.com → Python on 8000
server {
    listen 80;
    server_name dashboard.example.com;
    location / {
        proxy_pass http://localhost:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

By Path

server {
    listen 80;
    server_name example.com;

    location /app1/ {
        proxy_pass http://localhost:3001/;
    }
    location /app2/ {
        proxy_pass http://localhost:3002/;
    }
}

Load Balancing

upstream backend {
    server 127.0.0.1:3001;
    server 127.0.0.1:3002;
    server 127.0.0.1:3003;
}

server {
    listen 80;
    server_name example.com;

    location / {
        proxy_pass http://backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Load balancing methods:

upstream backend {
    # Round robin (default)
    server 127.0.0.1:3001;
    server 127.0.0.1:3002;

    # Least connections
    least_conn;

    # IP hash (sticky sessions)
    ip_hash;

    # Weighted
    server 127.0.0.1:3001 weight=3;    # Gets 3x traffic
    server 127.0.0.1:3002 weight=1;
}

Security Headers

Add these to every reverse proxy config:

# Inside server block
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

Rate Limiting

# Define limit zone (in the http block (not inside a server block))
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;

server {
    location /api/ {
        limit_req zone=api burst=20 nodelay;
        proxy_pass http://localhost:3000/;
    }
}

Caching

# Define cache (in http block)
proxy_cache_path /tmp/nginx_cache levels=1:2 keys_zone=app_cache:10m max_size=1g inactive=60m;

server {
    location / {
        proxy_pass http://localhost:3000;
        proxy_cache app_cache;
        proxy_cache_valid 200 10m;
        proxy_cache_valid 404 1m;
        add_header X-Cache-Status $upstream_cache_status;
    }

    # Don't cache API responses
    location /api/ {
        proxy_pass http://localhost:3000;
        proxy_cache off;
    }
}

Timeouts

location / {
    proxy_pass http://localhost:3000;

    proxy_connect_timeout 60s;     # Time to connect to backend
    proxy_send_timeout 60s;        # Time to send request to backend
    proxy_read_timeout 300s;       # Time to wait for response (increase for slow APIs)

    # For file uploads
    client_max_body_size 100m;
}

Troubleshooting

Problem Fix
502 Bad Gateway Backend is not running. Check: `ss -tlnp \ grep 3000`
504 Gateway Timeout Backend too slow. Increase proxy_read_timeout
Connection refused Wrong port or backend not listening on 127.0.0.1
Real IP not showing Add proxy_set_header X-Real-IP $remote_addr;
CORS errors Add CORS headers in Nginx or backend
WebSocket not working Missing Upgrade and Connection headers
SSL not working Run certbot --nginx -d yourdomain.com
# Debug checklist
nginx -t                           # Test config syntax
systemctl status nginx             # Is Nginx running?
ss -tlnp | grep :3000             # Is backend running?
curl http://localhost:3000         # Can Nginx reach backend?
tail -f /var/log/nginx/error.log  # Check errors

See Also