TutorialsAugust 4, 20264 min read

How to Set Up Nginx as a Reverse Proxy

Put Nginx in front of your Node, Python, or Docker app: proxy_pass, headers, WebSockets, and HTTPS done right on Ubuntu 24.04.

NBy Nxeon

A reverse proxy sits in front of your application and forwards incoming web traffic to it. This is how almost every production app is served: your Node, Django, or Docker process listens on a local port like 3000, and Nginx on ports 80/443 handles TLS, compression, and clean URLs. By the end you will have a working reverse proxy with correct forwarded headers and WebSocket support.

Why proxy at all?

Running your app directly on port 80 means running it as root (bad), losing the ability to host multiple apps, and reimplementing TLS, gzip, caching, and rate limiting yourself. Nginx does all of that well and lets your app stay on a safe internal port. If you have not installed it yet, start with installing Nginx on Ubuntu 24.04.

The minimal proxy config

Assume your app is listening on 127.0.0.1:3000. Create /etc/nginx/sites-available/app.example.com:

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

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

Enable and reload:

sudo ln -s /etc/nginx/sites-available/app.example.com /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

Why those headers matter

Without proxy_set_header, your app sees every request as coming from 127.0.0.1 and thinks it is on plain HTTP. The four headers above pass the real client IP, the original host, and the true protocol so your framework can build correct absolute URLs, log real visitor IPs, and set secure cookies. X-Forwarded-Proto is especially important — many frameworks refuse to mark cookies Secure unless they know the outer connection was HTTPS.

The Nxeon game-server control panel — live console, player slots, and TPS
The Nxeon game-server control panel — live console, player slots, and TPS

Add WebSocket support

If your app uses WebSockets (Socket.IO, live dashboards, hot reload), you must forward the Upgrade handshake. Add these two lines inside the location block:

        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

The proxy_http_version 1.1 line above is required for this to work — HTTP/1.0 has no concept of connection upgrade.

Proxy multiple apps by path

You can route different URL paths to different backends from one server block:

    location /api/ {
        proxy_pass http://127.0.0.1:4000/;
    }
    location / {
        proxy_pass http://127.0.0.1:3000;
    }

Note the trailing slash on proxy_pass http://127.0.0.1:4000/; — with it, Nginx strips the /api/ prefix before forwarding. Without it, the prefix is kept. This trailing-slash behaviour trips up almost everyone once, so remember it.

Tune buffers and timeouts

For apps that stream responses or handle large uploads, add sensible limits at the server or http level:

    client_max_body_size 50m;
    proxy_read_timeout 300s;

The default client_max_body_size is only 1 MB, which silently rejects larger file uploads with a 413 error — a classic gotcha.

Put HTTPS on top

A reverse proxy is the perfect place to terminate TLS. Install Certbot's Nginx plugin and it will add a 443 block and redirect HTTP automatically — our Certbot guide walks through it. Traffic between Nginx and your app stays on localhost, so plain HTTP there is fine.

This pattern is exactly how you serve a Node.js app deployed with PM2, a Next.js app, or a FastAPI app behind Uvicorn.

Add rate limiting and security headers

A reverse proxy is the right place to protect your app. Basic rate limiting throttles abusive clients before they ever reach your backend. Define a zone in the http block of /etc/nginx/nginx.conf:

limit_req_zone $binary_remote_addr zone=applimit:10m rate=10r/s;

Then apply it inside your location:

        limit_req zone=applimit burst=20 nodelay;

This allows 10 requests per second per IP with a short burst, returning 503 to floods. While you are here, add a few security headers so every response carries them:

        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;

Reload with sudo nginx -t && sudo systemctl reload nginx. Your app now sits behind a proxy that shields it from casual abuse and hardens the browser side too.

FAQ

What is the difference between a reverse proxy and a forward proxy?

A forward proxy sits in front of clients and hides them from servers (like a corporate VPN gateway). A reverse proxy sits in front of servers and hides them from clients — it is what Nginx does here, presenting one public front door to one or many backend apps.

Why does my app generate http:// links behind HTTPS?

Your framework is not trusting the proxy. Make sure you send X-Forwarded-Proto $scheme and enable "trust proxy" in your framework (for example app.set('trust proxy', 1) in Express or SECURE_PROXY_SSL_HEADER in Django).

Can Nginx load-balance across several backends?

Yes. Define an upstream block with multiple server lines and proxy_pass to the upstream name. Nginx round-robins by default and can health-check and weight backends.

Do I need to reload Nginx after editing the proxy config?

Yes — run sudo nginx -t to validate, then sudo systemctl reload nginx to apply with no dropped connections.

Nxeon VPS hosting for developers gives you the root access and clean Ubuntu base you need to run Nginx in front of any stack — and if you are moving an app from another host, our team will help you migrate it for free.

#nginx#reverse proxy#vps#ubuntu#web server#seobatch

Deploy your first server in under a minute

Creating an account is free and takes no card details. You pay when you deploy — choose a billing term and pay from your wallet or by card at checkout.