Skip to content
DevOps devops webservers 5 min read

What is a Reverse Proxy?

A reverse proxy is a server that sits in front of your application and forwards incoming requests to it. Instead of users talking to your app directly, they talk to the reverse proxy, and the reverse proxy talks to your app on their behalf. This single idea is the backbone of almost every real-world web deployment, because it lets you add TLS (encryption), load balancing, caching, and security in one place — without changing a single line of your app’s code.

A proxy in plain English

A proxy (a server that acts as a middleman for network traffic) relays requests between two parties so they never connect directly. There are two flavours, and the difference is whose side the proxy is on.

A forward proxy sits in front of clients (the users). When you configure your laptop to use a company proxy, every website you visit sees the proxy’s address, not yours. The proxy works on behalf of the people making requests.

A reverse proxy sits in front of servers (your apps). The user has no idea it exists — they just type your domain name. The reverse proxy receives the request and quietly forwards it to whichever backend (your actual application server) should handle it. It works on behalf of the people receiving requests.

Forward proxyReverse proxy
Sits in front ofClients (users)Servers (your apps)
Who knows about itThe client configures itInvisible to the client
Typical jobFilter/anonymise outgoing trafficTLS, load balancing, caching, routing
ExampleCorporate web filter, VPN gatewayNginx in front of a Node.js app

The word “reverse” only describes the direction it faces. The technology is the same — it is still just a middleman forwarding requests. Don’t overthink the name.

Why you almost always want one

Your application — a Node.js, Python, Java, or Go program — usually listens on a high port like 3000 or 8080 and only on localhost. Exposing that directly to the internet is fragile and insecure. A reverse proxy like Nginx (a fast, popular web server, said “engine-x”) solves a stack of problems at once.

TLS termination

TLS (Transport Layer Security, the encryption behind HTTPS) is hard to do well inside every app. With a reverse proxy you handle HTTPS once at the proxy. The proxy decrypts the request, then forwards plain HTTP to your app over localhost, which is safe because it never leaves the machine. This is called TLS termination — the encrypted connection “terminates” at the proxy.

Load balancing

If one copy of your app can’t handle the traffic, you run several copies and let the reverse proxy spread requests across them. This is load balancing, and it gives you both more capacity and resilience — if one backend dies, the proxy routes around it.

Caching

The proxy can store copies of responses and serve them instantly without bothering your app. This cuts load and speeds up your site for repeated requests.

Hiding app ports

Your app stays bound to 127.0.0.1:3000 (localhost only) and the firewall blocks that port from the outside world. Only ports 80 (HTTP) and 443 (HTTPS) are ever exposed, both owned by the proxy. Attackers can’t reach your app directly.

Serving static files

Images, CSS, and JavaScript don’t need your application’s CPU. The proxy serves these straight from disk, far faster than your app could, freeing it to do real work.

Seeing it in action on Ubuntu

Here is the smallest possible reverse proxy. Assume you have an app running on port 3000. Install Nginx on Ubuntu 22.04 or 24.04 LTS:

sudo apt update
sudo apt install -y nginx
sudo systemctl status nginx

Output:

● nginx.service - A high performance web server and a reverse proxy server
     Loaded: loaded (/lib/systemd/system/nginx.service; enabled; preset: enabled)
     Active: active (running) since Mon 2026-06-15 10:12:03 UTC; 3s ago

Create a site config at /etc/nginx/sites-available/myapp:

server {
    listen 80;
    server_name example.com;

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

The key line is proxy_pass — it tells Nginx “forward this request to my app on localhost port 3000”. The proxy_set_header lines pass the original visitor’s details to your app, which otherwise would only see the proxy.

Enable the site, test the config, and reload:

sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Output:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration test is successful

Finally, lock the firewall so only the proxy is reachable:

sudo ufw allow 'Nginx Full'
sudo ufw enable

ufw (Uncomplicated Firewall) now allows ports 80 and 443 and nothing else. Your app on port 3000 is invisible from the internet, exactly as intended.

Gotcha: binding your app to 0.0.0.0:3000 (all network interfaces) instead of 127.0.0.1:3000 lets people skip the proxy entirely and hit your app raw — bypassing TLS and your security headers. Always bind apps to 127.0.0.1 when a reverse proxy is in front of them.

When to use a reverse proxy (and when not to)

Use one whenever you run an application server that serves users over the internet — which is almost always. It is the standard, expected architecture for any production web app.

You can skip it for a purely static website with no backend (though even then Nginx is a great static server), or for a quick local experiment on your own machine where nothing is exposed publicly. But the moment you need HTTPS, multiple app instances, or a public domain, a reverse proxy stops being optional.

Best Practices

  • Bind your application to 127.0.0.1 and let the reverse proxy be the only thing listening on public ports 80 and 443.
  • Always forward the real client details with X-Real-IP and X-Forwarded-For so your app’s logs and rate limiting stay accurate.
  • Terminate TLS at the proxy and forward plain HTTP only over localhost, never over the network.
  • Run sudo nginx -t before every reload to catch config errors before they take your site down.
  • Keep the firewall (ufw) tight — if a port doesn’t need to face the internet, block it.
  • Set sensible timeouts and limits at the proxy to protect a slow backend from being overwhelmed.
Last updated June 15, 2026
Was this helpful?