Set Up a Reverse Proxy on Linux Hosting technician installing rack-mounted network hardware in server room
Web Hosting
William  

Nginx as Linux Reverse Proxy: Setup and Alternatives

On Linux, reach for nginx as your default reverse proxy. It's fast, its event-driven workers hold up under load, and the docs are honest about what each directive does. But don't run it as a black box. If you can't trace a request from the kernel's socket buffers through nginx's workers to your upstream's logs, you don't have a reverse proxy linux setup, you have a mystery box that fails in production. Caddy wins when you want automatic TLS and a config you can read. HAProxy wins when you need serious load balancing and connection-level visibility. Everything else on the "top 10 proxies" lists is a niche fit, not a default.

Last updated: 2026-07-21

What a reverse proxy actually does at the connection level

A reverse proxy sits in front of your servers and answers for them. The public hostname points at the proxy. The proxy terminates the client's TCP connection, then opens a separate connection to your real backend and shuttles bytes between the two.

That "separate connection" part is the whole point, and most tutorials skip it. The client never talks to your app server. It talks to nginx, which talks to your app on a private link. Two sockets, not one.

Don't confuse this with a forward proxy. A forward proxy acts for the client: a machine inside a network uses it to reach the internet. A reverse proxy acts for the server: the internet reaches your backend through it. Same word, opposite direction.

Because the proxy owns the client-facing socket, it also owns everything at the edge: TLS, rate limits, access rules, and which backend gets the request. Your backends stay simple and hidden.

How do you set up a reverse proxy on Linux

Install the package, write one server block, reload, then prove it works with curl instead of trusting a browser tab. Here's the sequence I use on any distro.

  1. Install nginx from your distro's package manager, not a random tarball.
  2. Enable and start it with systemctl enable --now nginx.
  3. Confirm the daemon is listening: ss -tlnp | grep nginx. That tells you port 80 is actually bound, not just that the service claims to be up.
  4. Add a server block with a proxy_pass line pointing at your backend.
  5. Run nginx -t to check syntax before you reload. This one habit saves you from taking the site down with a typo.
  6. Reload with systemctl reload nginx, not restart. Reload keeps existing connections alive.

Now verify from the wire, not the UI:

curl -i http://localhost/

Read the headers. A Server: nginx line with your app's body means the proxy answered and forwarded correctly. If you want to see the two-socket dance for real, run strace -f -e trace=connect -p $(pgrep -o nginx) while you fire a request. You'll watch the worker open a fresh connection to the upstream. That's the proof the pattern is doing what you think.

If curl hangs, the problem is almost always upstream reachability or a firewall, not nginx config. More on that below.

Which reverse proxy server should you actually run

Pick based on what you need at the edge, not on a feature checkbox table. Here's how I split them.

ToolConcurrency modelTLS handlingConfigMy pick for
nginxEvent-driven workersManual, or certbotTerse, block-basedThe default for most sites
HAProxyEvent-driven, L4+L7Manual, strongVerbose but preciseLoad balancing and observability
CaddyGoroutine-basedAutomatic HTTPSReadable, tinySmall setups, hands-off TLS
TraefikGo, dynamicAutomaticLabels/discoveryContainers and Kubernetes
Apache httpdProcess/thread hybridmod_sslHeavyOnly if you already run it

nginx is the right default. It's mature, fast, and well understood, and a single worker handles up to 2048 concurrent connections out of the box. That ceiling is a config knob, not a wall, so tune worker_connections before you blame the software.

HAProxy is what I reach for when load balancing has to be exact. Its health checks and stats socket show you connection state at a level nginx doesn't. If you want to see precisely what's happening per backend, this is the tool.

Caddy gets automatic TLS right and gives you a config file you can actually reason about. I use it when readable config and hands-off certificates matter more than squeezing out the last request per second.

Traefik earns its place in containerized and Kubernetes setups, because it discovers services dynamically. Outside that world it's more moving parts than you need.

Apache httpd with mod_proxy works, but I wouldn't adopt it fresh in 2026 just to reverse proxy. Use it only if Apache is already serving your other workloads. For the full nginx configuration reference, read the primary docs, not a Stack Overflow paste.

How do you configure a reverse proxy on Debian

On Debian, sudo apt update && sudo apt install nginx gets you a stable, conservative build. Config lives under /etc/nginx, with sites-available and sites-enabled as the Debian-flavored layout. You drop a file in the first and symlink it into the second.

Debian stable ships an older nginx on purpose. That's usually fine. If you need a newer feature, pull from backports rather than adding a sketchy third-party repo that fights the package manager later.

The gotcha: Debian's default site is enabled and grabs port 80. Delete or disable the default symlink in sites-enabled before your own server block will answer. People forget this and spend an hour wondering why every request hits the welcome page.

Test with nginx -t, which resolves those symlinks and reports the real merged config. Then reload.

How do you set up a reverse proxy on Ubuntu

Ubuntu installs nginx the same way with apt, and the config still lives under /etc/nginx with the same sites-available and sites-enabled split. The differences are the traps around it.

Skip the snap version of anything proxy-related. The snap runs in its own confinement, and its file paths and permissions don't match the apt docs you're reading. You'll chase ghosts. Install from apt, or add the official nginx PPA if you genuinely need a newer release.

Ubuntu ships UFW, the uncomplicated firewall, and it will block your traffic silently if you forget it. Open the web ports:

sudo ufw allow 'Nginx Full'

That profile opens both HTTP and HTTPS. Check it landed with sudo ufw status. If a request still times out from outside, your cloud provider's security group is blocking the port before UFW ever sees it. Two firewalls, two places to check.

Using a proxy to protect internal backends

Put every backend on a private interface and let only the proxy reach it. The proxy terminates TLS at the edge, then talks plain HTTP to the app over a link the internet can't touch. Clients see one hostname on port 443. Your app servers see nothing but the proxy.

The failure mode is leaving the backend listening on 0.0.0.0 with its port open to the world. Now the proxy is theater. Anyone who guesses the port skips your TLS, your rate limits, and your access rules entirely.

Bind app servers to 127.0.0.1 or a private VLAN address, and firewall the upstream port to the proxy's IP only. Verify it from an outside host with curl --connect-timeout 3 http://backend-ip:8080. If that connects, you have a hole. It should refuse or time out.

How do you control access at the front-end proxy layer

Enforce access at the proxy, then confirm from the logs that the directive actually fired. A rule you never tested is a rule you don't have.

Your main levers:

  • IP allowlists with allow and deny for admin paths.
  • Basic auth with an htpasswd file for quick gating.
  • mTLS, where the client must present a certificate you trust.
  • Rate limiting with limit_req_zone to blunt brute-force and cheap floods.

Here's the mistake I see: someone adds a deny all block, reloads, and assumes it works. Test it. Hit the path from a blocked IP and watch the access log:

tail -f /var/log/nginx/access.log

A 403 in that log is your proof. If you see a 200 instead, your location block ordering is wrong and a broader match is winning. nginx picks the most specific match, and getting that precedence wrong is the classic way access rules silently do nothing.

For TLS itself, hold the line at TLS 1.2 as your floor. Anything older is broken and should be refused, not proxied.

Where the reverse proxy sits in your firewall and network

Set Up a Reverse Proxy on Linux Hosting diagram with laptop switch and servers

A reverse proxy is not a firewall, and treating it like one gets people breached. The proxy makes decisions about HTTP: paths, headers, rates, certificates. A packet filter like nftables or iptables makes decisions about ports and addresses. You need both.

The firewall's job is to make sure only ports 80 and 443 ever reach the proxy host, and only the proxy can reach the backends. The proxy's job is to decide what happens to the requests that get through. One works at layer 3 and 4, the other at layer 7.

Segment the network so a compromised app server can't pivot sideways. The proxy in a front-facing zone, the apps in a private zone, and a tight rule set between them. If you're shaky on the routing side of this, my notes on tracing network paths in Linux walk through confirming where packets actually go.

What a proxy does not protect against: a vulnerability in the app it forwards to. It passes the request along faithfully, exploit and all. The firewall doesn't help there either. That's the app's problem to fix.

How do you diagnose a reverse proxy that isn't working

Read the error log first. Every real answer is already in it:

tail -f /var/log/nginx/error.log

The message tells you which failure you have, and they want different fixes:

  • connect() failed (111: Connection refused) means the upstream isn't listening where you told nginx to look. Check the backend with ss -tlnp and fix the proxy_pass address.
  • upstream timed out (110) means the backend accepted the connection but didn't answer in time. Now go read the app's logs, not nginx's. nginx is the messenger here.
  • 13: Permission denied on a connection is almost always SELinux, not file permissions.

That last one bites people on Rocky Linux, RHEL, and Fedora. nginx tries to open a socket to your backend port and the kernel refuses. Fix it by flipping the SELinux boolean:

sudo setsebool -P httpd_can_network_connect 1

Confirm the denial was real before you touch anything: ausearch -m avc -ts recent or dmesg | grep -i denied. Don't disable SELinux to make this go away. You're papering over the one control that would have caught a misconfigured backend.

When the log isn't enough, watch the wire. tcpdump -i any port 8080 -A shows you whether nginx even opens the upstream connection and what comes back. If you see the SYN go out and nothing return, the problem is downstream of nginx. Buffer size mismatches on large headers show up here too, as an upstream reset right after the request. My longer network troubleshooting method works the stack from the bottom up, which is the order that saves you time.

Do application servers need a reverse proxy in front of them

Yes, even though Gunicorn, uWSGI, Node, and Puma all speak HTTP directly. Serving them raw to the internet is a mistake for three concrete reasons.

Slow clients. An app worker handling one byte-per-second client is a worker doing nothing for everyone else. nginx buffers the whole slow request, then hands your app a complete one instantly. Your expensive workers stay busy with real work.

Concurrency limits. Application servers run a small pool of workers. nginx holds thousands of idle keep-alive connections cheaply and feeds requests to that pool at a rate it can handle. Without it, a traffic spike starves the pool and everything queues.

Static files. Let nginx serve your CSS, JS, and images straight from disk. It does that far better than an app runtime, and it frees the app to do only what it's for. If you're tuning past this point, the web server optimization notes cover the next round of knobs.

So the answer isn't "the app can't serve HTTP." It can. It just shouldn't face the open internet alone.

FAQ

Can I run a reverse proxy and the app on the same server?

Yes, and it's a common single-box setup. Bind the app to 127.0.0.1:8080 and point proxy_pass at that loopback address. The app never touches a public interface, so the only door in is the proxy. Just don't forget to firewall the app port anyway, in case someone later rebinds it to 0.0.0.0.

How do I get HTTPS certificates for my proxy?

For nginx, use certbot to issue and auto-renew from Let's Encrypt. For Caddy, do nothing: it fetches and renews certificates on its own, which is the main reason to pick it. Either way, redirect plain HTTP to the encrypted side and refuse anything below your TLS floor.

Why does my proxy return 502 Bad Gateway?

A 502 means nginx reached out to the upstream and got nothing usable back. The backend is down, listening on a different port, or crashed mid-request. Check the app is actually running with ss -tlnp, then read the app's own log for the crash. nginx is reporting the failure, not causing it.

Is HAProxy better than nginx for load balancing?

For pure load balancing across many backends with detailed health checks, yes, HAProxy gives you finer control and better visibility into connection state. For a mix of static files, TLS, and forwarding to one or two apps, nginx is simpler and does everything you need. Match the tool to the job instead of chasing benchmarks.

Where do nginx config files live across distros?

On Debian and Ubuntu, the tree is under /etc/nginx with the sites-available and sites-enabled split. On CentOS, RHEL, and Fedora, the main file is /etc/nginx/nginx.conf and there's no sites split by default. Run nginx -T to dump the full merged config so you know exactly what's loaded, not what you think is loaded.