# What nginx Does in Practice

You've got the mental model: nginx is the receptionist out front, optionally spreading work across a pool.
Now let's make it concrete. In a real deployment, nginx isn't just blindly forwarding - it's quietly doing a
handful of jobs that would be painful to build into your app. This phase walks through the four you'll meet
most, then the operational skill that matters more than any of them: changing config without breaking the
site.

## The "what nginx is doing for me" cheat-card

> **Want to know what each piece of your config buys you? Find it here, then read the section.**

| Job | What it does | Directive you'll see |
|---|---|---|
| TLS termination | Handles HTTPS so your app speaks plain HTTP (§1) | `listen 443 ssl;` `ssl_certificate` |
| Compression | Shrinks text responses before sending (§2) | `gzip on;` |
| Static caching | Serves & caches CSS/JS/images without touching your app (§3) | `location` + `expires` / `root` |
| Rate limiting | Caps how fast a client can hammer you (§4) | `limit_req_zone` / `limit_req` |
| Safe reload | Apply config changes with no dropped connections (§5) | `nginx -t` then `nginx -s reload` |

---

## 1. TLS termination - HTTPS stops here

**What it actually is.** TLS termination means nginx is where the encrypted HTTPS connection ends. The
visitor's browser and nginx do the encrypted handshake; nginx decrypts the request and forwards it to your
app over plain, unencrypted HTTP on a private local port. On the way back, nginx encrypts the response again.

The point: **your app never deals with certificates or encryption.** It speaks ordinary HTTP, as if it were
still on your laptop, while the public connection is fully HTTPS.

```nginx
server {
    listen 443 ssl;                                  # listen for HTTPS
    server_name yoursite.com;

    ssl_certificate     /etc/letsencrypt/live/yoursite.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yoursite.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:3000;            # plain HTTP to the app
        proxy_set_header Host              $host;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;  # tell the app it was HTTPS
    }
}

# A second tiny server block to send plain HTTP visitors to HTTPS.
server {
    listen 80;
    server_name yoursite.com;
    return 301 https://$host$request_uri;            # permanent redirect to HTTPS
}
```

*What just happened:* the first block accepts HTTPS on port 443 using your certificate files, then forwards
decrypted requests to the app on plain HTTP. `X-Forwarded-Proto $scheme` tells your app "the original request
came in over HTTPS" - the connection otherwise looks like plain HTTP to the app. The second block catches
anyone who typed `http://` and redirects them to the secure version.

📝 **Terminology - `301`.** The HTTP status code for "moved permanently." Browsers remember it and go
straight to HTTPS next time, instead of asking for the HTTP version again.

⚠️ **Gotcha - the redirect loop from a missing `X-Forwarded-Proto`.** If your app does its own "force HTTPS"
redirect but can't tell it's *already* on HTTPS (nginx forwarded plain HTTP and you didn't pass the header),
the app keeps redirecting, nginx keeps forwarding as HTTP, and the browser spins in an endless loop. Passing
that header - and configuring your framework to trust it - is the cure. Same family of problem as the
real-client-IP gotcha from [Phase 1](01-what-a-reverse-proxy-is.md): behind a proxy, your app only knows
about the original request through headers you choose to forward.

## 2. gzip - making responses smaller

**What it actually is.** gzip is compression. nginx squeezes text-based responses - HTML, CSS, JavaScript,
JSON - down to a fraction of their size before sending them, and the browser unpacks them on arrival. Less
data on the wire means faster page loads, especially on slow connections.

```nginx
gzip on;
gzip_types text/css application/javascript application/json text/plain;
```

*What just happened:* You turned compression on and told nginx which content types are worth compressing.
nginx now compresses matching responses on the fly before sending them.

💡 **Key point.** Compress text, not media. Images, video, and most fonts are *already* compressed - running
gzip over them wastes CPU for no real gain, which is why you list specific text types rather than "everything."

## 3. Caching and serving static assets

**What it actually is.** Your CSS, JS, images, and fonts don't change between requests - no reason to wake up
your app to hand out the same logo a thousand times. nginx can serve those files straight from disk, and tell
browsers to *cache* them so they don't even re-request them next time.

```nginx
location /static/ {
    root /var/www/yoursite;          # serve files from /var/www/yoursite/static/
    expires 30d;                     # tell browsers to cache for 30 days
}
```

*What just happened:* requests starting with `/static/` are served by nginx directly off disk - your app is
never involved. `expires 30d` adds a caching header so a returning visitor's browser reuses its saved copy
for 30 days instead of asking again. Two wins: your app does less work, and repeat visits feel instant.

⚠️ **Gotcha - caching and updates fight each other.** Tell browsers to cache `app.js` for 30 days, then ship
a new version, and returning visitors keep the old one for up to 30 days. The fix is *cache-busting*: change
the filename when the content changes (e.g. `app.a1b2c3.js`), so a new version is a new URL the browser
hasn't cached. Most build tools do this for you - long cache times and changing files only coexist when the
filename changes with the content.

## 4. Rate limiting - capping the firehose

**What it actually is.** Rate limiting caps how many requests a single client can make in a window of time -
protecting your app from being overwhelmed, whether by a misbehaving script, a scraper, or a brute-force
login attempt, by turning away the excess before it reaches your app.

```nginx
# Define a shared limit: 10 requests per second per client IP.
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;

server {
    location /api/ {
        limit_req zone=mylimit burst=20 nodelay;     # apply it to the API
        proxy_pass http://127.0.0.1:3000;
    }
}
```

*What just happened:* `limit_req_zone` defines a limit of 10 requests/second, tracked per client IP, using a
shared 10-megabyte memory area named `mylimit` to remember everyone's recent rate. Applied to `/api/`, a
client going faster gets turned away with an error instead of reaching your app. `burst=20` allows a short
spike of up to 20 queued requests so normal bursty traffic isn't punished, and `nodelay` lets that burst
through immediately rather than dribbling it out.

⚠️ **Gotcha - rate limiting by IP needs the *real* IP.** `$binary_remote_addr` is the address of whoever
opened the connection. If there's *another* proxy or CDN in front of nginx, that's the upstream proxy's
address, not the visitor's - so every visitor shares one rate limit, or one visitor can dodge it. The
real-client-IP problem from [Phase 1](01-what-a-reverse-proxy-is.md) again, one layer up: behind another
proxy, teach nginx to read the forwarded IP (the `realip` module) before rate limiting behaves as expected.

## 5. The skill that matters most: testing and reloading config safely

Everything above lives in config files. The operational reality: **a typo in your nginx config can take your
entire site down.** So the single most valuable habit is checking before you apply, and applying without
dropping connections.

**Always test first.** Before applying any change, ask nginx to parse the config and confirm it's valid:

```console
$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
```

*What just happened:* `nginx -t` ("test") parsed your config without touching the running server and found no
errors, so it's safe to apply. A typo would print the file and line number instead, so you fix it *before*
anything went live. Running `-t` every time is a cheap habit that prevents the worst kind of outage.

**Reload, don't restart.** Once the test passes, apply the change:

```console
$ sudo nginx -s reload
```

*What just happened:* `reload` tells nginx to re-read its config and gracefully hand over to new worker
processes. In-flight requests on the old workers finish normally; new requests use the new config. **No
connections are dropped.** A *restart*, by contrast, stops nginx entirely and starts it again - a brief
window where nothing is listening and visitors get connection errors.

⚠️ **Gotcha - reload vs restart.** Reach for **reload** for config changes; it's seamless. Save **restart**
for the rare cases that genuinely need it (certain startup-level settings, or recovering a wedged process).
Never run `reload` without `nginx -t` first - reloading a broken config can leave the server in a bad state.
Test, then reload, every time.

**Why this saves you later.** The difference between a calm config change and a 2am outage is almost always
this discipline. Build the habit now, while nothing's on fire, and it'll be automatic when something is.

## Recap

1. **TLS termination** ends HTTPS at nginx; your app speaks plain HTTP. Pass `X-Forwarded-Proto` so it knows
   the request was secure, or risk a redirect loop.
2. **gzip** compresses text responses (not already-compressed media) for faster loads.
3. **Static caching** lets nginx serve assets off disk and tells browsers to cache them - pair long cache
   times with cache-busting filenames.
4. **Rate limiting** caps per-client request rate before traffic hits your app, and depends on nginx seeing
   the real client IP.
5. **Test then reload** (`nginx -t`, then `nginx -s reload`) applies config with no dropped connections.
   Reload for config; restart only when you must.

That's the working picture: a reverse proxy is a forwarder, a load balancer is a forwarder with a pool, and
nginx in practice is where all your infrastructure concerns live. Natural next step: putting one in front of
a real server you control.

> Ready to stand this up? [Deploying to a VPS](/guides/deploying-to-a-vps) walks through getting an app onto
> a server with nginx in front of it, end to end - this guide is the "why" behind the front-door piece you'll
> configure there.
