Nginx: server blocks and reverse proxying
Serving static content and proxying to a backend app on the same server. · 11 min
A "server block" in Nginx (the equivalent of Apache's "virtual host") defines how Nginx handles requests for a specific domain or port. Configuration files typically live in `/etc/nginx/sites-available/` on Debian-family systems, symlinked into `/etc/nginx/sites-enabled/` to actually activate them — a deliberate two-step pattern that lets you keep a config file present but disabled by simply removing the symlink, without deleting the file itself.
For a static site, the key directives are `listen` (which port), `server_name` (which domain(s) this block handles), and `root` (which directory on disk to serve files from). For dynamic backend applications — a Node.js, Python, or Java app listening on its own local port — Nginx commonly acts as a reverse proxy: it receives the public request on port 80/443 and forwards it internally to the backend's actual port, then returns the backend's response to the client. This lets Nginx handle TLS termination, static asset serving, and load balancing in front of an application that itself only needs to worry about application logic.
After any configuration change, `sudo nginx -t` tests the configuration syntax without applying it — catching an error before it takes down the running server, exactly the same discipline as `sshd -t` from Level 9. Only after a clean test should you `sudo systemctl reload nginx` to apply the change.
| Command | Purpose | Example |
|---|---|---|
| nginx -t | Test Nginx configuration syntax without applying it | — |
| systemctl reload nginx | Apply a valid configuration change without dropping active connections | — |
| ln -s /etc/nginx/sites-available/site /etc/nginx/sites-enabled/ | Enable a site (Debian-family layout) | — |
A reverse proxy server block
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;
}
}Common Mistakes
- ⚠ Forgetting the `X-Forwarded-For` / `X-Real-IP` headers on a reverse proxy — without them, the backend application sees every request as coming from 127.0.0.1 (Nginx itself), losing the real client IP entirely
- ⚠ Editing a config file directly without running `nginx -t` first, and finding out about a typo only after Nginx fails to reload
- ⚠ Confusing sites-available (where the config lives) with sites-enabled (which controls whether it's active) — a file present in sites-available alone does nothing until symlinked
Hands-On Lab
Deploy a site behind an Nginx reverse proxy
Objectives
- ✓ Run a simple backend application on a local port
- ✓ Configure Nginx to reverse-proxy public traffic to it
- ✓ Verify the real client IP is correctly forwarded
Instructions
- Start a simple backend: `python3 -m http.server 3000` (standing in for a real application).
- Install Nginx (`sudo apt install nginx`) and create a new site config at `/etc/nginx/sites-available/proxy-test` using the reverse-proxy example above, pointing `proxy_pass` at `http://127.0.0.1:3000`.
- Enable it: `sudo ln -s /etc/nginx/sites-available/proxy-test /etc/nginx/sites-enabled/`, then remove or disable the default site if it conflicts.
- Test config with `sudo nginx -t`, then `sudo systemctl reload nginx`.
- From another machine (or `curl -I http://<server-ip>`), confirm you reach the backend through Nginx on port 80.
Hints (2)
- If you get a "connection refused" from Nginx itself, confirm Nginx is actually running: `systemctl status nginx`.
- If the proxy connects but the backend never responds, confirm the backend is actually listening on the port you configured: `ss -tulpn | grep 3000`.
Takeaway: Always run `nginx -t` before `reload` — it catches configuration errors before they take down a running server, the same discipline that matters for sshd_config.