Every Tuesday - Deep dives, architecture lessons, and real engineering stories.
Every Saturday - The best DevOps, SRE, Cloud, AI, tools, tutorials, and projects from the week.
📬 Missed This Week’s Uptime Sync?
This week’s best DevOps, SRE, Cloud, Kubernetes, database, AI infrastructure, and production engineering reads:
What eight compromised eMudhra certificates mean for your infrastructure
How a leaked key led to LiteLLM’s compromised release
Why successful PostgreSQL backups don’t prove you can restore
What 14 LLMs proposed after an AWS credential compromise
How Prisma moved its control plane to Postgres without downtime
How to trace MCP tool calls with OpenTelemetry.
Plus: giving agents context before buying an AI SRE, parallelizing MySQL snapshots, protecting secrets during agent deployments, near-zero-downtime RDS MariaDB upgrades, Mac automation with Ansible, Azure Functions monitoring, and self-hosted tools for agent credentials, logs, and deployments
At PhonePe, I managed the largest Nginx cluster in the org, processing more than a billion requests a day. That’s why I think Nginx is worth learning properly if you’re heading into DevOps or SRE. It sits between users and your application, handling connections, HTTPS, and where requests go next. Understanding that flow helps you make sense of slow requests, failed connections, and traffic that never reaches your app. Today, we’ll start with how it works, then you’ll run a small setup yourself.
What is a reverse proxy?
A proxy is a middleman. There are two kinds, and people mix them up all the time.
A forward proxy sits in front of clients. Think of a company network where all employee traffic goes out through one proxy. The websites see the proxy, not the employees.
A reverse proxy sits in front of servers. Users talk to the proxy, and the proxy talks to your app. Your app stays hidden behind it. Nginx is the most common tool for this job, and MDN's proxy guide lists the usual uses: load balancing, caching, decryption and compression.

Why bother? Start with the problem
Imagine your app runs on a server, port 8080, open to the internet. That one process has to accept HTTPS connections, hold the certificate, and survive on its own. Its address is public, so anyone can poke at it. And since it's a single instance, every restart means downtime.
Put Nginx in front, and the browser talks to Nginx instead. Your app can move to a private address behind it. TLS, routing and load balancing now live in one place. You also pick up a few new things to worry about, and I'll point at those below.
Two connections, not one pipe
Most people picture Nginx as a box that forwards a port. The better picture is two separate connections:
Browser to Nginx.
Nginx to your app.
Nginx makes its own decisions in the middle. Which backend gets this request, which headers go along, whether the second connection is encrypted. The browser never sees any of that. It only knows the public address.
DNS resolves the public name to Nginx's address
Browser
| connection 1: TCP + TLS handshake to Nginx on :443
v
Nginx (the public endpoint)
| decrypt, pick app1 or app2
| connection 2: new HTTP connection to a private address
v
app1:8080 or app2:8080
| status, headers, body
v
Nginx
| back over connection 1
v
BrowserA TLS handshake is the step where browser and server agree on encryption and the server proves its identity with a certificate. It happens before any real data moves.
Because connection 2 is brand new, your app never sees the browser. It sees Nginx. Unless you pass the original details along in headers, your app will log Nginx's IP as the client. We'll fix that in the config below.
Where HTTPS ends
The certificate and private key live on Nginx:
listen 443 ssl;
ssl_certificate /etc/nginx/certs/example.com.pem;
ssl_certificate_key /etc/nginx/certs/example.com.key;The paths are examples. See the NGINX HTTPS docs for the full set of directives. The browser leg is encrypted. Nginx decrypts the request and sends it on over connection 2. That's what people mean by "terminating TLS".
Connection 2 can be plain HTTP or HTTPS. If Nginx and your app share one machine, plain HTTP never leaves it. If the traffic crosses a network you don't control, anyone on that path can read it. Ask who can see the path between Nginx and your app, then decide.
The lab below uses plain HTTP on port 80 to keep things simple.
Reverse proxy vs load balancer
An interview favorite. A reverse proxy is a role: it sits in front of servers and speaks HTTP for them. Load balancing is one thing it can do: spread requests across several backends. One Nginx can do both. A proxy that sends everything to a single backend is still a reverse proxy, it just isn't balancing anything.
The config
Nginx calls a group of backend servers an upstream. Here's a config with two of them:
upstream app {
server app1:8080;
server app2:8080;
}
server {
listen 80;
location / {
proxy_pass http://app;
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_pass http://app points at the whole group. When you don't pick a balancing method, Nginx uses round-robin, per the load balancing docs. Requests alternate between app1 and app2.
The four proxy_set_header lines pass the browser's details to your app, since the proxy module docs say Nginx changes some headers by default:
Host $hostpasses the name the browser asked for. Without it, your app seesHost: appinstead ofexample.comand may build wrong URLs.X-Real-IP $remote_addrcarries the address of whoever connected to Nginx.X-Forwarded-For $proxy_add_x_forwarded_foradds that address to a running list of addresses the request passed through.X-Forwarded-Proto $schemetells your app whether the browser used http or https. Without it, an app behind a TLS-terminating proxy may think every request is plain HTTP and redirect wrongly.
One warning for later: don't trust these headers blindly. A client can send fake ones. I'll cover how to handle that in a follow-up issue.
Lab: run it on your laptop
You need Docker with Compose. Make a folder with two files.
docker-compose.yml:
services:
app1:
image: traefik/whoami
hostname: app1
command: --port=8080
app2:
image: traefik/whoami
hostname: app2
command: --port=8080
nginx:
image: nginx:stable
ports:
- "8080:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- app1
- app2nginx.conf: paste the upstream and server blocks from the config section above, exactly as written.
traefik/whoami is a tiny web server that replies with its own hostname and the headers it received. That makes it perfect for seeing what Nginx does. Mounting your file over default.conf also avoids a clash with the image's own port 80 config.
Start it and run a quick config check:
docker compose up -d
docker compose exec nginx nginx -tNow run these experiments.
Send requests and watch the traffic split:
Look at what your app receives:
Break it on purpose. Delete the four
proxy_set_headerlines fromnginx.conf, then reload:Kill a backend:
Check that the backends are really private. Neither app1 nor app2 has a
ports:entry in the compose file, so this should fail:
What this config doesn't solve
A working proxy proves requests flow. It doesn't prove everything else works. Here's the first problem you'll meet in real life.
Say your app keeps login sessions in its own memory. Round-robin doesn't pin a user to one instance. A user logs in on app1, the next request lands on app2, and app2 has never heard of them. They get logged out at random. The usual fixes are shared session storage or sticky routing, and each has trade-offs. This is my own inference from how round-robin works, so test it on your own app.
Next issue, we'll cover what else breaks in production: health checks, retries, timeouts and spoofed headers.
Where you'll see this again: Kubernetes Ingress
When you learn Kubernetes, you'll meet Ingress. An Ingress controller is a reverse proxy running inside your cluster. Some controllers run Nginx itself. It receives outside traffic, picks a backend, and forwards the request, just like the lab. The upstream group maps roughly to the pods behind a Kubernetes Service. Once the lab makes sense, Ingress is much easier to read.
Do you need a reverse proxy?
No source says it's mandatory. A single simple app can run without one, for example when TLS is already handled somewhere else. Add one when you want a controlled public edge, with TLS, routing or logging in one place, or when you have more than one backend. That's my rule of thumb, built on the uses MDN and the NGINX docs list.
A reverse proxy is one more part you have to run, size and watch.
References

👋 Find me on Twitter | Linkedin | Connect 1:1
Thank you for supporting this newsletter.
Y’all are the best.

