Tutorials

Beginner's Guide to Creating a Reverse Proxy with Nginx

Administrator
By Administrator
Published Oct 02, 2025
9 min read
Beginner's Guide to Creating a Reverse Proxy with Nginx

Beginner's Guide to Creating a Reverse Proxy with Nginx

Imagine you're running multiple applications on your server - a Node.js API, a Python Django app, and a WordPress site. Managing different ports, SSL certificates, and security settings for each one sounds like a nightmare, right?

This is exactly where reverse proxies shine. They act like a smart traffic controller for your server, directing incoming requests to the right application while handling common tasks like SSL termination, load balancing, and caching.

I used to think reverse proxies were complex enterprise tools, but after setting one up for my first project, I realized they're incredibly powerful and surprisingly straightforward to configure. They've become an essential part of every web architecture I build.

In this guide, I'll walk you through creating your first reverse proxy with Nginx. We'll start with the basics and gradually build up to more advanced configurations. Let's make your server more organized and efficient! 🚀

What Exactly is a Reverse Proxy?

Before diving in, let's understand what we're building:

A reverse proxy sits in front of your web applications and forwards client requests to them. Think of it like a receptionist for your office building - visitors come to the main entrance (your reverse proxy), and the receptionist directs them to the right office (your application).

Key benefits:

  • Single point of entry: One domain, one SSL certificate
  • Load balancing: Distribute traffic across multiple servers
  • SSL termination: Handle HTTPS in one place
  • Caching: Speed up responses with built-in caching
  • Security: Hide internal application details

When Should You Use a Reverse Proxy?

Reverse proxies are perfect for:

  • Running multiple applications on different ports
  • Microservices architecture
  • Load balancing across multiple servers
  • Adding SSL/TLS to applications that don't support it natively
  • Caching static content to reduce server load
  • Implementing security rules and rate limiting

Prerequisites: Getting Started

Before we begin, make sure you have:

  • Ubuntu Server (18.04, 20.04, or 22.04)
  • Nginx installed: sudo apt install nginx -y
  • SSH access with sudo privileges
  • At least one web application running on a different port (we'll create test examples)

Step 1: Basic Reverse Proxy Configuration

Let's start with the simplest reverse proxy setup. Imagine you have a Node.js application running on port 3000.

Create a Basic Configuration

sudo nano /etc/nginx/sites-available/my-reverse-proxy

Add Basic Proxy Configuration

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;
    }
}

Enable Your Configuration

sudo ln -s /etc/nginx/sites-available/my-reverse-proxy /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

What's happening here?

  • proxy_pass: Tells Nginx where to forward requests
  • proxy_set_header: Passes important information about the original request to your application

Step 2: Testing Your Reverse Proxy

Let's create a simple test application to verify our reverse proxy works.

Create a Simple Test Server

Here's a quick Node.js test server (save as test-server.js):

const express = require('express');
const app = express();
const port = 3000;

app.get('/', (req, res) => {
    res.json({
        message: 'Hello from the backend server!',
        timestamp: new Date().toISOString(),
        headers: req.headers
    });
});

app.listen(port, () => {
    console.log(`Test server running on port ${port}`);
});

Or a simple Python server (save as test-server.py):

from http.server import HTTPServer, BaseHTTPRequestHandler
import json
from datetime import datetime

class SimpleHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.send_header('Content-type', 'application/json')
        self.end_headers()

        response = {
            'message': 'Hello from the Python backend!',
            'timestamp': datetime.now().isoformat(),
            'path': self.path
        }

        self.wfile.write(json.dumps(response).encode())

if __name__ == '__main__':
    server = HTTPServer(('localhost', 3000), SimpleHandler)
    print('Test server running on port 3000')
    server.serve_forever()

Run Your Test Server

# For Node.js
node test-server.js

# For Python
python3 test-server.py

Now visit http://yourdomain.com in your browser. You should see the JSON response from your test server!

Step 3: Advanced Configuration with SSL

Let's make our reverse proxy production-ready with HTTPS and better headers.

Install SSL Certificate

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

Enhanced Configuration with SSL

server {
    listen 80;
    server_name yourdomain.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name yourdomain.com;

    # SSL Configuration
    ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;

    # Security Headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    # Proxy Configuration
    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;

        # Timeouts
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;

        # Buffer settings
        proxy_buffering on;
        proxy_buffer_size 4k;
        proxy_buffers 8 4k;
    }
}

Step 4: Multiple Applications on One Domain

One of the most powerful features of reverse proxies is serving multiple applications from different paths.

Path-Based Routing

server {
    listen 443 ssl http2;
    server_name yourdomain.com;

    # SSL configuration (same as above)

    # API routes to Node.js application
    location /api/ {
        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;
    }

    # Admin panel to Python application
    location /admin/ {
        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;
    }

    # Static files served directly
    location /static/ {
        alias /var/www/static/;
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # Everything else to main application
    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 5: Load Balancing Configuration

Running multiple instances of your application? Let's set up load balancing.

Upstream Configuration

# Define upstream servers
upstream my_app_servers {
    server localhost:3000;
    server localhost:3001;
    server localhost:3002;

    # Load balancing method (default: round-robin)
    # Other options: least_conn, ip_hash
}

server {
    listen 443 ssl http2;
    server_name yourdomain.com;

    # SSL configuration

    location / {
        proxy_pass http://my_app_servers;
        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;

        # Health check settings
        proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
        proxy_connect_timeout 5s;
        proxy_send_timeout 10s;
        proxy_read_timeout 10s;
    }
}

Step 6: Caching Configuration

Add caching to reduce load on your backend servers.

Enable Proxy Caching

Add this to your http block in /etc/nginx/nginx.conf:

# Proxy cache settings
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=10g
                 inactive=60m use_temp_path=off;

Apply Caching to Your Location

location / {
    proxy_cache my_cache;
    proxy_cache_valid 200 302 10m;
    proxy_cache_valid 404 1m;
    proxy_cache_key "$scheme$request_method$host$request_uri";

    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;

    # Cache bypass for logged-in users
    proxy_cache_bypass $cookie_nocache $arg_nocache;
    proxy_no_cache $cookie_nocache $arg_nocache;
}

Common Use Cases and Examples

WordPress with Node.js API

server {
    listen 443 ssl http2;
    server_name yourdomain.com;

    # WordPress at root
    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
    }

    # API routes to Node.js
    location /api/ {
        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;
    }
}

React Frontend with Express Backend

server {
    listen 443 ssl http2;
    server_name yourdomain.com;

    # Static React build files
    location / {
        root /var/www/react-app/build;
        try_files $uri $uri/ /index.html;

        # Cache static assets
        location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
            expires 1y;
            add_header Cache-Control "public, immutable";
        }
    }

    # API routes to Express backend
    location /api/ {
        proxy_pass http://localhost:3001/;
        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;
    }
}

Troubleshooting Common Issues

"502 Bad Gateway" Error

Cause: Backend server isn't responding or proxy configuration is wrong.

Solution:

# Check if backend is running
curl http://localhost:3000

# Check Nginx error logs
sudo tail -f /var/log/nginx/error.log

# Test proxy configuration
sudo nginx -t

CORS Issues

Cause: Backend application expecting direct requests, not proxied ones.

Solution: Add CORS headers in your backend or use Nginx:

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

    # Add CORS headers
    add_header 'Access-Control-Allow-Origin' '$http_origin' always;
    add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
    add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always;
    add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;

    if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Allow-Origin' '$http_origin';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
        add_header 'Access-Control-Max-Age' 1728000;
        add_header 'Content-Type' 'text/plain; charset=utf-8';
        add_header 'Content-Length' 0;
        return 204;
    }
}

WebSocket Support

If you need to proxy WebSocket connections:

location /socket.io/ {
    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;
}

Best Practices and Security Tips

  • Always use HTTPS in production environments
  • Hide server versions: server_tokens off; in nginx.conf
  • Implement rate limiting to prevent abuse
  • Monitor logs regularly for unusual activity
  • Use health checks for load-balanced setups
  • Keep Nginx updated for security patches
  • Test configurations before applying them with nginx -t

Performance Optimization Tips

Enable HTTP/2

listen 443 ssl http2;

Gzip Compression

gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;

Connection Pooling

upstream backend {
    server localhost:3000;
    keepalive 32;
}

server {
    location / {
        proxy_pass http://backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
    }
}

Final Thoughts

Reverse proxies are incredibly powerful tools that can dramatically improve your web architecture. They provide a clean separation between your public-facing server and your backend applications, making your infrastructure more organized, secure, and scalable.

What we've covered today is just the beginning. As you get more comfortable with reverse proxies, you can explore advanced features like:

  • Advanced load balancing algorithms
  • Dynamic configuration with API backends
  • Advanced caching strategies
  • Security rule engines
  • Real-time monitoring and metrics

Start simple, test thoroughly, and gradually add complexity as you understand how everything works together. Your future self will thank you for the clean, maintainable architecture you've built.

Happy proxying! 🚀 Your applications have never been more organized.

Related Articles

How to Backup and Restore a Website on VPS Linux

How to Backup and Restore a Website on VPS Linux

Oct 03, 2025

How to Backup and Restore a Website on VPS Linux That moment when you realize your website is gon...

Setting Up Load Balancing with Nginx for High Traffic Sites

Setting Up Load Balancing with Nginx for High Traffic Sites

Oct 03, 2025

Setting Up Load Balancing with Nginx for High Traffic Sites Your website is growing. Traffic is i...

How to Monitor Server Resources with htop and netstat

How to Monitor Server Resources with htop and netstat

Oct 03, 2025

How to Monitor Server Resources with htop and netstat Ever wonder why your website suddenly slows...

Basic Firewall Configuration for Linux Web Servers

Basic Firewall Configuration for Linux Web Servers

Oct 03, 2025

Basic Firewall Configuration for Linux Web Servers Your web server is like a house in a busy neig...