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.
# A second tiny server block to send plain HTTP visitors 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: 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.
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.
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.
# Define a shared limit: 10 requests per second per client IP.
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;
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 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:
$ 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:
$ 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
- TLS termination ends HTTPS at nginx; your app speaks plain HTTP. Pass
X-Forwarded-Protoso it knows the request was secure, or risk a redirect loop. - gzip compresses text responses (not already-compressed media) for faster loads.
- Static caching lets nginx serve assets off disk and tells browsers to cache them - pair long cache times with cache-busting filenames.
- Rate limiting caps per-client request rate before traffic hits your app, and depends on nginx seeing the real client IP.
- Test then reload (
nginx -t, thennginx -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 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.
← Phase 2: Load Balancing · Guide overview
Before the quiz: without looking back, say (or jot down) the core idea of this phase in your own words.
Check your understanding 2 questions
1. TLS termination means...
2. The safe way to apply an nginx config change is...