ALBANIA

ARGENTINA

AUSTRALIA

AUSTRIA

AZERBAIJAN

BANGLADESH

BELGIUM

BOSNIA AND HERZEGOVINA

BRAZIL

BULGARIA

CANADA

CHILE

CHINA

COLOMBIA

COSTA RICA

CROATIA

CYPRUS

CZECH REPUBLIC

DENMARK

ECUADOR

EGYPT

EL SALVADOR

ESTONIA

FINLAND

FRANCE

GEORGIA

GERMANY

GREECE

GUATEMALA

HUNGARY

ICELAND

INDIA

INDONESIA

IRELAND

ISRAEL

ITALY

JAPAN

KAZAKHSTAN

KENYA

KOSOVO

LATVIA

LIBYA

LITHUANIA

LUXEMBOURG

MALAYSIA

MALTA

MEXICO

MOLDOVA

MONTENEGRO

MOROCCO

NETHERLANDS

NEW ZEALAND

NIGERIA

NORWAY

PAKISTAN

PANAMA

PARAGUAY

PERU

PHILIPPINES

POLAND

PORTUGAL

QATAR

ROMANIA

RUSSIA

SAUDI ARABIA

SERBIA

SINGAPORE

SLOVAKIA

SLOVENIA

SOUTH AFRICA

SOUTH KOREA

SPAIN

SWEDEN

SWITZERLAND

TAIWAN

THAILAND

TUNISIA

TURKEY

UKRAINE

UNITED ARAB EMIRATES

UNITED KINGDOM

URUGUAY

USA

UZBEKISTAN

VIETNAM

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

If you're running applications on a dedicated server, configuring Nginx as a reverse proxy is one of the most impactful things you can do for your infrastructure. It improves performance, tightens security, enables SSL termination, and gives you precise control over how traffic flows to your backend services all from a single, battle-tested web server.

This step-by-step guide walks you through the entire process: from installing Nginx on a dedicated Linux server to configuring upstream backends, enabling HTTPS, tuning performance headers, and hardening your proxy configuration against common threats.

1. What Is a Reverse Proxy and Why Use Nginx?

A reverse proxy is a server that sits in front of your backend applications and forwards incoming client requests to the appropriate service. Unlike a forward proxy (which acts on behalf of clients), a reverse proxy acts on behalf of servers. Clients talk to Nginx, and Nginx talks to your apps.

Nginx has become the industry standard for reverse proxying on dedicated servers for several key reasons:

  • High concurrency with low memory — Nginx's event-driven, asynchronous architecture handles thousands of simultaneous connections without spawning a new thread or process per request.

  • SSL/TLS termination — Offload encryption and decryption from your application servers to Nginx, freeing backend CPU for actual business logic.

  • Centralized traffic control — Route requests to different backend services (Node.js, Django, PHP-FPM, microservices) from a single entry point.

  • Built-in load balancing — Distribute traffic across multiple application instances using round-robin, least connections, or IP hash strategies.

  • Static file serving — Serve static assets directly without touching your application layer, dramatically reducing backend load.

  • Security layer — Hide backend server details, block malicious requests, rate-limit clients, and restrict access by IP before traffic ever reaches your app.

On a dedicated server, where you have full control over system resources and network configuration, Nginx as a reverse proxy is the architectural backbone of a production-grade deployment.

COLO BIRD Tip: If you're hosting your server infrastructure in a colocation facility or on a bare-metal dedicated server, this setup applies directly to your environment. The configuration covered here works on any Linux-based dedicated server running Ubuntu, Debian, or CentOS/AlmaLinux.

2. Prerequisites and Dedicated Server Requirements

Before you begin, confirm your dedicated server meets these requirements:

Requirement Details
Operating System Ubuntu 22.04/24.04 LTS, Debian 11/12, CentOS 8+, AlmaLinux 9
RAM Minimum 1 GB (2 GB+ recommended for production)
Root or sudo access Required for installing packages and editing system config
Domain name Pointed to your server's public IP (for HTTPS setup)
Open ports 80 (HTTP) and 443 (HTTPS) unblocked in your firewall
Backend application At least one running service (e.g., Node.js on port 3000)

You should also have a basic familiarity with the Linux command line. All commands in this tutorial are tested on Ubuntu 22.04 LTS, but they translate directly to other Debian-based systems. CentOS/AlmaLinux differences are noted where relevant.

3. Installing Nginx on Your Dedicated Server

On Ubuntu / Debian

sudo apt update && sudo apt upgrade -y
sudo apt install nginx -y

On CentOS / AlmaLinux / RHEL

sudo dnf install epel-release -y
sudo dnf install nginx -y

Start and Enable the Nginx Service

After installation, start Nginx and configure it to launch automatically on server reboot:

sudo systemctl start nginx
sudo systemctl enable nginx

Confirm it's running:

sudo systemctl status nginx

You should see active (running) in the output. You can also visit your server's IP address in a browser — you'll see the default Nginx welcome page, which confirms the installation is working.

Configure the Firewall

If ufw is active on your server, allow web traffic:

sudo ufw allow 'Nginx Full'
sudo ufw reload
sudo ufw status

Nginx Full opens both port 80 (HTTP) and port 443 (HTTPS). If you prefer to manage these separately, use Nginx HTTP and Nginx HTTPS individually.

4. Understanding the Nginx Configuration Structure

Before writing any reverse proxy configuration, it helps to understand how Nginx organizes its files on a dedicated server:

/etc/nginx/
├── nginx.conf              # Main global configuration file
├── conf.d/                 # Additional config files (loaded globally)
├── sites-available/        # Virtual host config files (Debian/Ubuntu)
├── sites-enabled/          # Symlinks to active virtual hosts
├── snippets/               # Reusable config fragments
└── mime.types              # MIME type mappings

The sites-available / sites-enabled pattern (standard on Ubuntu/Debian) is the cleanest approach for managing multiple domains and services on one server. Each virtual host (server block) lives in its own file under sites-available/, and you activate it by creating a symlink in sites-enabled/.

The nginx.conf file sets global parameters like worker processes, connection limits, and logging. You generally don't need to modify it heavily for a reverse proxy setup — your work happens in the virtual host config files.

Check the Current Nginx Version

nginx -v

It's a good practice to know your version, since some directives (like proxy_ssl_protocols) have version-specific behavior.

5. Setting Up a Basic Nginx Reverse Proxy

Let's configure Nginx to accept HTTP requests on port 80 and forward them to a backend application running on localhost:3000 (a common setup for Node.js or Express apps).

Create a New Virtual Host File

sudo nano /etc/nginx/sites-available/myapp.conf

Paste the following configuration:

server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;

    # Access and error logs for this virtual host
    access_log /var/log/nginx/myapp_access.log;
    error_log  /var/log/nginx/myapp_error.log;

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

        # Forward the original host header to the backend
        proxy_set_header Host              $host;

        # Pass the real client IP address
        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;

        # WebSocket support (add these if your app uses WebSockets)
        proxy_http_version 1.1;
        proxy_set_header Upgrade    $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

Replace yourdomain.com with your actual domain name and 3000 with the port your backend application is listening on.

Activate the Virtual Host

sudo ln -s /etc/nginx/sites-available/myapp.conf /etc/nginx/sites-enabled/

Test and Reload Nginx

Always test your configuration for syntax errors before reloading:

sudo nginx -t

If the output says syntax is ok and test is successful, apply the changes:

sudo systemctl reload nginx

Your dedicated server now forwards all incoming HTTP requests for yourdomain.com to your backend service. That's a working reverse proxy — but there's much more to configure for a production environment.

6. Configuring Upstream Backend Servers

For more complex setups — multiple backend services, named upstream groups, or load-balanced pools — Nginx's upstream block gives you a clean, maintainable way to define your backend targets separately from the proxy location.

upstream myapp_backend {
    server 127.0.0.1:3000;
}

server {
    listen 80;
    server_name yourdomain.com;

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

Using a named upstream makes your configuration much easier to extend later — when you need to add load balancing or switch backend ports, you change one block instead of hunting through multiple location directives.

Routing Different Paths to Different Backend Services

A dedicated server often runs multiple services — an API, a web frontend, an admin panel. Nginx handles this through path-based routing in location blocks:

server {
    listen 80;
    server_name yourdomain.com;

    # Route API requests to a Node.js backend on port 3000
    location /api/ {
        proxy_pass http://127.0.0.1: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;
    }

    # Route everything else to a Python/Django app on port 8000
    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;
    }
}

Note on trailing slashes: In proxy_pass http://127.0.0.1:3000/; — that trailing slash matters. With a trailing slash, /api/users becomes /users at the backend. Without it, /api/users is forwarded as /api/users. Choose based on how your backend app is configured.

7. Enabling HTTPS and SSL Termination with Let's Encrypt

Running a reverse proxy without HTTPS in 2025 is unacceptable for any production server. SSL termination at the Nginx layer is the most efficient approach: Nginx handles the TLS handshake, decrypts the request, and forwards plain HTTP to your backend over localhost (which is already secure since it never leaves the server).

Install Certbot

sudo apt install certbot python3-certbot-nginx -y

Obtain and Install a Free SSL Certificate

sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

Certbot will automatically modify your Nginx configuration to add HTTPS support and set up an HTTP-to-HTTPS redirect. Follow the interactive prompts, entering your email address when asked.

What Certbot Adds to Your Config

After running Certbot, your virtual host file will look similar to this:

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

    ssl_certificate     /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
    include             /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam         /etc/letsencrypt/ssl-dhparams.pem;

    location / {
        proxy_pass http://127.0.0.1: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 yourdomain.com www.yourdomain.com;
    return 301 https://$host$request_uri;
}

Verify Automatic Certificate Renewal

Let's Encrypt certificates expire every 90 days. Certbot sets up a cron job or systemd timer to renew them automatically. Verify it works:

sudo certbot renew --dry-run

8. Proxy Headers and Real IP Forwarding

When Nginx proxies a request, your backend application sees Nginx's IP address (usually 127.0.0.1) as the client, not the real visitor's IP. This breaks analytics, access logs, rate limiting, and geo-based logic in your app. The fix is passing the correct headers.

Essential Proxy Headers Explained

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

    # Tells the backend what domain the request was sent to
    proxy_set_header Host $host;

    # The real client IP address
    proxy_set_header X-Real-IP $remote_addr;

    # Full chain of IP addresses (client → proxy chain → your server)
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    # The original protocol (http or https) — critical for apps that generate URLs
    proxy_set_header X-Forwarded-Proto $scheme;

    # The original port the client connected on
    proxy_set_header X-Forwarded-Port $server_port;
}

Using a Snippet for DRY Configuration

If you have multiple location blocks or multiple virtual hosts all needing the same proxy headers, define them once in a snippet file and include it everywhere:

sudo nano /etc/nginx/snippets/proxy-headers.conf

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_http_version 1.1;
proxy_set_header Upgrade           $http_upgrade;
proxy_set_header Connection        "upgrade";

Then in your virtual host:

location / {
    proxy_pass http://127.0.0.1:3000;
    include snippets/proxy-headers.conf;
}

9. Load Balancing Across Multiple Backend Services

One of the biggest advantages of Nginx as a reverse proxy on a dedicated server is its native load balancing capability. If you're running multiple instances of an application (for redundancy or horizontal scaling), Nginx can distribute requests across them automatically.

Round-Robin Load Balancing (Default)

upstream myapp_pool {
    server 127.0.0.1:3000;
    server 127.0.0.1:3001;
    server 127.0.0.1:3002;
}

server {
    listen 443 ssl;
    server_name yourdomain.com;

    # ... ssl config ...

    location / {
        proxy_pass http://myapp_pool;
        include snippets/proxy-headers.conf;
    }
}

Nginx distributes requests evenly across the three backend instances in rotation.

Least Connections

Sends each new request to the server with the fewest active connections — better for workloads with variable request duration:

upstream myapp_pool {
    least_conn;
    server 127.0.0.1:3000;
    server 127.0.0.1:3001;
}

IP Hash (Session Persistence / Sticky Sessions)

Routes requests from the same client IP to the same backend server consistently — useful when your application stores session data in memory:

upstream myapp_pool {
    ip_hash;
    server 127.0.0.1:3000;
    server 127.0.0.1:3001;
}

Marking a Server as a Backup

upstream myapp_pool {
    server 127.0.0.1:3000;
    server 127.0.0.1:3001;
    server 127.0.0.1:3002 backup;  # Only used if both primary servers are down
}

Health Checks and Failure Handling

upstream myapp_pool {
    server 127.0.0.1:3000 max_fails=3 fail_timeout=30s;
    server 127.0.0.1:3001 max_fails=3 fail_timeout=30s;
}

max_fails=3 marks a server as unavailable after 3 consecutive failed requests. fail_timeout=30s sets how long Nginx waits before retrying that server.

10. Performance Tuning: Caching, Buffering, and Timeouts

A properly tuned Nginx reverse proxy on a dedicated server can significantly reduce backend load and improve response times for end users.

Proxy Buffering

Buffering prevents your backend from being held open while a slow client downloads the response. Nginx receives the full backend response into a buffer, then streams it to the client independently.

location / {
    proxy_pass http://127.0.0.1:3000;
    include snippets/proxy-headers.conf;

    # Enable response buffering
    proxy_buffering    on;
    proxy_buffer_size  4k;
    proxy_buffers      8 16k;
    proxy_busy_buffers_size 32k;
}

Timeouts

Generous timeouts prevent proxy errors on slow backends, while tight timeouts protect against hanging connections:

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

    proxy_connect_timeout  10s;   # Time to establish connection to backend
    proxy_send_timeout     60s;   # Time for sending request to backend
    proxy_read_timeout     60s;   # Time to receive response from backend
    send_timeout           60s;   # Time to send response to client
}

Adjust proxy_read_timeout upward for applications with long-running operations (file processing, report generation, etc.).

Proxy Response Caching

For content that doesn't change frequently, caching backend responses at the Nginx layer drastically reduces load on your applications:

# Define a cache zone in the http{} block of nginx.conf
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=1g inactive=60m use_temp_path=off;

# In your server block location:
location / {
    proxy_pass http://127.0.0.1:3000;
    include snippets/proxy-headers.conf;

    proxy_cache            my_cache;
    proxy_cache_valid      200 302  10m;
    proxy_cache_valid      404      1m;
    proxy_cache_use_stale  error timeout updating http_500 http_502 http_503 http_504;
    add_header X-Cache-Status $upstream_cache_status;
}

The X-Cache-Status header lets you verify whether responses are being served from cache (HIT) or the backend (MISS).

Gzip Compression

Enable gzip compression at the Nginx proxy layer to reduce the size of responses sent to clients:

# In the http{} block of nginx.conf
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

11. Security Hardening for Your Nginx Proxy

Your Nginx reverse proxy is the first thing the internet touches on your dedicated server. Locking it down is critical.

Hide Nginx Version Information

Exposing your Nginx version gives attackers a roadmap for version-specific exploits:

# In the http{} block of nginx.conf
server_tokens off;

Add Security Headers

server {
    # ...

    # Prevent clickjacking
    add_header X-Frame-Options "SAMEORIGIN" always;

    # Block MIME type sniffing
    add_header X-Content-Type-Options "nosniff" always;

    # Enable XSS filtering in older browsers
    add_header X-XSS-Protection "1; mode=block" always;

    # Enforce HTTPS for 1 year (only add after you're sure HTTPS works)
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    # Control referrer information
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}

Rate Limiting

Protect your backend applications from abuse and brute-force attempts:

# Define rate limit zones in the http{} block
limit_req_zone $binary_remote_addr zone=general:10m rate=30r/m;
limit_req_zone $binary_remote_addr zone=api:10m rate=100r/m;

# Apply in location blocks
location / {
    limit_req zone=general burst=10 nodelay;
    proxy_pass http://127.0.0.1:3000;
    include snippets/proxy-headers.conf;
}

location /api/ {
    limit_req zone=api burst=20 nodelay;
    proxy_pass http://127.0.0.1:3000;
    include snippets/proxy-headers.conf;
}

Block Requests Without a Host Header

Prevents direct IP-based scans from hitting your application:

# Add this as the FIRST server block in your config
server {
    listen 80 default_server;
    listen 443 ssl default_server;
    server_name _;
    ssl_certificate     /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
    return 444;  # Close connection without response
}

Restrict Access to Sensitive Paths

# Block access to .env files, git repos, and other sensitive paths
location ~ /\.(env|git|htaccess|svn) {
    deny all;
    return 404;
}

# Restrict admin panel to specific IPs
location /admin/ {
    allow 203.0.113.10;   # Your office IP
    allow 203.0.113.20;   # Your home IP
    deny all;
    proxy_pass http://127.0.0.1:3000;
    include snippets/proxy-headers.conf;
}

12. Testing and Validating Your Reverse Proxy Setup

After making configuration changes, always test before assuming things work correctly.

Syntax Check

sudo nginx -t

This is your most important command. Never skip it.

Reload vs. Restart

# Graceful reload — zero downtime, finishes existing connections first
sudo systemctl reload nginx

# Full restart — drops all active connections (avoid in production)
sudo systemctl restart nginx

Test with curl

From your local machine or another server, test the proxy with curl:

# Test HTTP to HTTPS redirect
curl -I http://yourdomain.com

# Test HTTPS connection
curl -I https://yourdomain.com

# Verbose output showing SSL handshake
curl -v https://yourdomain.com

# Check cache status header
curl -I https://yourdomain.com | grep X-Cache-Status

Check Response Headers

curl -sI https://yourdomain.com | grep -E "(X-Frame|X-Content|Strict|X-Cache|Server)"

You should see your security headers in the response, and Server: nginx without a version number.

Verify SSL Certificate

echo | openssl s_client -connect yourdomain.com:443 2>/dev/null | openssl x509 -noout -dates

This shows the certificate validity dates, confirming Let's Encrypt issued it correctly.

Check Nginx Error Logs in Real Time

sudo tail -f /var/log/nginx/myapp_error.log

Watch this while you make test requests to catch proxy errors as they happen.

13. Troubleshooting Common Nginx Reverse Proxy Errors

502 Bad Gateway

Cause: Nginx can't reach the backend server.

Solutions:

  • Verify your backend application is actually running: sudo systemctl status myapp or ps aux | grep node

  • Confirm the backend is listening on the correct port: ss -tlnp | grep 3000

  • Check if a firewall rule is blocking loopback traffic: sudo ufw status

  • Look at Nginx error logs: sudo tail -50 /var/log/nginx/error.log

504 Gateway Timeout

Cause: The backend took too long to respond.

Solutions:

  • Increase proxy_read_timeout in your location block

  • Investigate performance bottlenecks in your backend application

  • Check if the backend server is under heavy load: top or htop

413 Request Entity Too Large

Cause: The client sent a request body larger than Nginx allows.

Solution: Increase client_max_body_size in your server or location block:

client_max_body_size 50M;

301 Redirect Loop

Cause: Usually happens when your backend app detects HTTP and redirects to HTTPS, but Nginx is already handling HTTPS, the app doesn't know the original request was HTTPS.

Solution: Make sure you're passing X-Forwarded-Proto $scheme in your proxy headers, and configure your application to trust that header.

Mixed Content Warnings

Cause: Your application generates URLs with http:// instead of https://.

Solution: Pass X-Forwarded-Proto https to your app and configure it to use this header when building URLs.

Permission Denied on Socket Files

If you're proxying to a Unix socket (common with PHP-FPM or Gunicorn):

proxy_pass http://unix:/run/myapp.sock;

Ensure the Nginx worker process user (www-data on Ubuntu) has read/write access to the socket file.

14. Frequently Asked Questions

Can I use Nginx as a reverse proxy alongside Apache?

Yes. A common pattern is Nginx on ports 80/443 proxying to Apache on port 8080. Nginx handles SSL, compression, and static files; Apache handles .htaccess and PHP via mod_php.

Does Nginx reverse proxy support WebSockets?

Yes. Set proxy_http_version 1.1 and pass the Upgrade and Connection headers (covered in the proxy headers section above). This is required for apps using Socket.io, server-sent events, or any WebSocket-based protocol.

Should I proxy to localhost or a private IP on my dedicated server?

For applications running on the same physical machine, 127.0.0.1 (or ::1 for IPv6) is correct and adds a minor security benefit — the backend service isn't exposed on your public network interface. Use a private IP only when proxying to a different server on the same private network.

How do I proxy to a Docker container?

Point proxy_pass to the container's mapped port on the host (127.0.0.1:PORT), or use Docker's internal bridge network IP if you're running Nginx inside a container as well.

What's the difference between proxy_pass and rewrite?

proxy_pass forwards the request to a backend server. rewrite modifies the request URI and keeps it within Nginx's own processing. They serve different purposes — don't use rewrite as a substitute for proxy_pass.

How many backend services can one Nginx instance proxy?

There's no hard limit. Each server block handles a domain, each location block handles a path. A single Nginx instance on a production dedicated server commonly proxies 10–50+ distinct backend services.

Complete Production-Ready Configuration Example

Here's a full, commented virtual host configuration that brings together everything covered in this guide:

# /etc/nginx/sites-available/myapp.conf

upstream myapp_backend {
    least_conn;
    server 127.0.0.1:3000 max_fails=3 fail_timeout=30s;
    server 127.0.0.1:3001 max_fails=3 fail_timeout=30s;
    server 127.0.0.1:3002 backup;
}

# HTTP → HTTPS redirect
server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;
    return 301 https://$host$request_uri;
}

# Main HTTPS server block
server {
    listen 443 ssl http2;
    server_name yourdomain.com www.yourdomain.com;

    # SSL configuration (managed by Certbot)
    ssl_certificate     /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
    include             /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam         /etc/letsencrypt/ssl-dhparams.pem;

    # Logging
    access_log /var/log/nginx/myapp_access.log;
    error_log  /var/log/nginx/myapp_error.log warn;

    # Security headers
    add_header X-Frame-Options            "SAMEORIGIN"                      always;
    add_header X-Content-Type-Options     "nosniff"                         always;
    add_header X-XSS-Protection           "1; mode=block"                   always;
    add_header Strict-Transport-Security  "max-age=31536000; includeSubDomains" always;
    add_header Referrer-Policy            "strict-origin-when-cross-origin" always;

    # Max upload size
    client_max_body_size 20M;

    # Serve static files directly (no backend needed)
    location /static/ {
        alias /var/www/myapp/static/;
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # Rate-limited API proxy
    location /api/ {
        limit_req zone=api burst=20 nodelay;

        proxy_pass http://myapp_backend/api/;
        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_http_version 1.1;

        proxy_connect_timeout 10s;
        proxy_send_timeout    60s;
        proxy_read_timeout    60s;
    }

    # Main application proxy
    location / {
        limit_req zone=general burst=10 nodelay;

        proxy_pass http://myapp_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;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_http_version 1.1;
        proxy_set_header Upgrade           $http_upgrade;
        proxy_set_header Connection        "upgrade";

        proxy_buffering         on;
        proxy_buffer_size       4k;
        proxy_buffers           8 16k;

        proxy_connect_timeout   10s;
        proxy_send_timeout      60s;
        proxy_read_timeout      60s;
    }

    # Block sensitive file access
    location ~ /\.(env|git|htaccess) {
        deny all;
        return 404;
    }
}

Key Takeaways

Setting up Nginx as a reverse proxy on a dedicated server involves four core areas:

  • Routing — Use proxy_pass with named upstream blocks to cleanly direct traffic to the right backend services.

  • Security — SSL termination, security headers, rate limiting, and IP blocking all happen at the Nginx layer before requests touch your app.

  • Performance — Buffering, caching, gzip compression, and timeout tuning protect your backend from being overwhelmed and keep response times low.

  • Observability — Dedicated access and error log files per virtual host make debugging fast and precise.

A dedicated server gives you the resources and control to implement all of these properly. Unlike shared or VPS environments, you're not fighting for CPU and memory — your Nginx instance can be tuned to the exact workload your applications demand.