1 What is NGINX?
NGINX (say "engine-x") is the traffic director of the web — the first program a request meets when it reaches your server.
One program, three jobs
| Job | What it means | Example |
|---|---|---|
| Web server | Hand files (HTML, CSS, images) straight to browsers — blisteringly fast | your React build's index.html |
| Reverse proxy | Receive a request, pass it to an app behind it, return the app's answer | forward /api/… to a Node app on port 4000 |
| Load balancer | Spread requests across several copies of an app | 3 backends share the traffic |
💡 "Reverse" proxy because it hides servers from clients (a normal "forward" proxy hides clients from servers). Users only ever talk to nginx — the apps behind it are invisible.
Why every DevOps engineer meets it
Roughly a third of all websites sit behind nginx — including this very learning portal! When you open
academy.brabi.in, an nginx on the server reads the request, terminates HTTPS, and quietly forwards
it to the app in a Docker container. In Kubernetes, the standard ingress controller is… nginx again
(you saw it in the K8s module). Learn it once, use it everywhere.
2 A Request's Journey
Follow one click from a browser to your app and back. Step through it.
🎬 Try it — walk the request through the system
The key idea: two separate conversations
The browser talks to nginx (encrypted, on port 443). nginx then opens its own plain-HTTP conversation with the app (on localhost, port 3000). The app never sees the internet directly — nginx shields it, adds headers, and can cache, compress, limit and log everything in between.
3 Install & the Commands You'll Actually Use
Installation is one line. Daily life is four commands.
# Ubuntu / Debian
sudo apt update && sudo apt install nginx
# is it running?
sudo systemctl status nginx
The 4 daily commands
| Command | What it does | When |
|---|---|---|
sudo nginx -t | Test the config for syntax errors | ALWAYS before reloading |
sudo systemctl reload nginx | Apply config without dropping connections | after every config change |
sudo systemctl restart nginx | Full stop/start (brief downtime) | rarely — reload is almost always enough |
sudo tail -f /var/log/nginx/error.log | Watch errors live | when something 502s |
💡 Golden habit: nginx -t && systemctl reload nginx — the reload only runs if the test passes. A typo can otherwise take every site on the server down.
4 Config Anatomy
nginx config is just directives (settings ending in ;) grouped into blocks ({ }). Click any line below to decode it.
🎬 Try it — click each line of a real config
server {
listen 80;
server_name academy.brabi.in;
location / {
proxy_pass http://127.0.0.1:3100;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
Where the files live (Ubuntu)
| Path | Holds |
|---|---|
/etc/nginx/nginx.conf | global settings; includes the folders below |
/etc/nginx/sites-available/ | one file per website (the library) |
/etc/nginx/sites-enabled/ | symlinks to the sites that are actually ON |
/var/log/nginx/ | access.log + error.log |
# turn a site on = create the symlink, test, reload
sudo ln -s /etc/nginx/sites-available/academy /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
💡 $host, $scheme, $remote_addr… — words starting with $ are variables nginx fills in per request.
5 Server Blocks — Many Sites, One Server
One nginx can host dozens of domains on the same IP. The Host header decides which block answers.
🎬 Try it — type a domain, see which block wins
This server has three blocks (a real pattern — it's how one EC2 hosts a main site, a superadmin and an academy):
server { server_name www.brabi.in; proxy_pass → :8082; }
server { server_name superadmin.brabi.in; proxy_pass → :8080; }
server { server_name academy.brabi.in; proxy_pass → :3100; }
What if nothing matches?
nginx falls back to the default server — the first block defined, or one marked
listen 80 default_server;. Pro move: make the default return 444 (drop) or
404, so random bots scanning your IP get nothing.
6 Location Matching — Which Rule Wins?
Inside a server block, location decides what happens per URL path. The matching ORDER surprises everyone once.
The 4 modifiers
| Syntax | Meaning | Priority |
|---|---|---|
location = /ping | EXACT match only | 1 — highest |
location ^~ /static/ | prefix; if it's the longest, skip regexes | 2 |
location ~ \.php$ / ~* | regex (case-sensitive / insensitive), first one wins | 3 |
location /api/ | plain prefix — longest one wins | 4 — fallback |
🎬 Try it — the location referee
Five rules are defined. Type a path and see the referee's decision:
① location = /health ② location ^~ /assets/
③ location ~* \.(png|jpg)$ ④ location /api/
⑤ location /
💡 The classic trap: /assets/logo.png hits ^~ /assets/ (regexes skipped),
but /team/photo.png hits the regex ③ — even though prefix ⑤ also matched. Regexes beat plain prefixes.
7 Static Files, SPAs & Caching
Serving files is nginx's superpower — but React/Vue apps need one special line.
A static site (or a built React app)
server {
listen 80;
server_name app.example.com;
root /usr/share/nginx/html; # folder with index.html
index index.html;
location / {
try_files $uri $uri/ /index.html; # ← the SPA line
}
# hashed assets never change — let browsers keep them for a year
location ~* \.(js|css|png|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
gzip on; # compress text responses (~70% smaller)
}
💡 Why try_files? A React Router URL like /portal/settings is not a real file on
disk. Without the fallback to /index.html, refreshing that page = 404. With it, nginx serves the app
shell and the router takes over. Every SPA deployment needs this line.
root vs alias — the classic confusion
location /img/ { root /data; } # /img/cat.png → /data/img/cat.png (path APPENDED)
location /img/ { alias /data/; } # /img/cat.png → /data/cat.png (prefix REPLACED)
8 Reverse Proxy — nginx in Front of Your App
The bread-and-butter production setup: nginx owns port 443, your app hides on a local port.
The production-grade proxy block
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; # websockets
proxy_set_header Connection 'upgrade';
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;
}
🎬 Try it — why those X-Forwarded headers matter
A student at IP 49.43.248.228 logs in. What does the app see?
💡 True story from THIS portal: without X-Forwarded-Host, the app built redirect
links pointing to 0.0.0.0:3000 — its own container address — instead of the public domain.
Behind a proxy, the app only knows what the headers tell it.
9 Load Balancing
When one app copy isn't enough, define an upstream pool and let nginx deal the cards.
upstream api_pool {
server 127.0.0.1:3001;
server 127.0.0.1:3002;
server 127.0.0.1:3003;
# least_conn; ← or pick the least-busy instead of round-robin
}
server {
location / { proxy_pass http://api_pool; }
}
🎬 Try it — deal requests to the pool
10 HTTPS & Certbot
Free certificates from Let's Encrypt, renewed automatically. Two commands, done.
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d academy.example.com
Certbot proves to Let's Encrypt that you control the domain, then edits your server block for you:
server {
server_name academy.example.com;
location / { proxy_pass http://127.0.0.1:3000; }
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/…/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/…/privkey.pem; # managed by Certbot
}
server { # and a port-80 twin that just redirects
listen 80;
server_name academy.example.com;
return 301 https://$host$request_uri;
}
| Fact | Detail |
|---|---|
| Certificates last | 90 days |
| Renewal | automatic — certbot installs a timer; check with sudo certbot renew --dry-run |
| Requirement | the domain's DNS must already point at your server before you run certbot |
| TLS termination | nginx decrypts; the hop to your app is plain HTTP on localhost — that's normal |
11 Troubleshooting — Reading nginx's Mind
Three error codes tell you 90% of every story.
| Code | nginx is saying… | First thing to check |
|---|---|---|
| 502 Bad Gateway | "I forwarded the request but nothing answered" | is the app running? docker ps / curl 127.0.0.1:3000 |
| 504 Gateway Timeout | "the app answered too slowly" | slow DB query? raise proxy_read_timeout only if truly needed |
| 413 Entity Too Large | "the upload exceeds my limit" | client_max_body_size 10m; |
| 404 on SPA refresh | "no such file on disk" | the try_files … /index.html line (section 7) |
The debug toolbox
sudo nginx -t # config valid?
sudo tail -f /var/log/nginx/error.log # what exactly failed
curl -I http://127.0.0.1:3000/ # is the app up, bypassing nginx?
curl -I https://mysite.com/ # what does the world see?
sudo ss -tlnp | grep :80 # who is listening on which port
💡 Debug like a plumber: the request flows browser → nginx → app. curl each junction
and find the segment where the water stops.
12 Practice Exercises
Do these on any Linux VM (or a docker run -p 8080:80 nginx container). Peek only after trying.
Install nginx and open the server's IP in a browser. You should see the welcome page. Find the file it served.
Show solution
sudo apt install nginx
systemctl status nginx # active (running)
# the page lives at /var/www/html/index.nginx-debian.htmlServe a folder /srv/hello with your own index.html on port 8081.
Show solution
# /etc/nginx/sites-available/hello
server {
listen 8081;
root /srv/hello;
index index.html;
}
# enable + apply
sudo ln -s /etc/nginx/sites-available/hello /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
curl -I http://localhost:8081/Run docker run -d -p 3000:80 nginxdemos/hello, then make your main site's /demo/ path proxy to it, passing the real client IP.
Show solution
location /demo/ {
proxy_pass http://127.0.0.1:3000/; # note trailing / strips /demo
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}Stop the container from exercise 3. Load /demo/ — observe the 502. Check the error log, then restart the container and confirm recovery.
Show solution
docker stop <id> # breaks it
sudo tail -1 /var/log/nginx/error.log # "connect() failed … Connection refused"
docker start <id> # fixed — nginx recovers instantly, no reload neededBuild any React/Vite app, serve its dist/ with nginx, and make a deep route like /settings/profile survive a refresh.
Show solution
location / {
try_files $uri $uri/ /index.html;
}13 Cheat Sheet
Everything on one screen.
## daily driver
sudo nginx -t && sudo systemctl reload nginx
sudo tail -f /var/log/nginx/error.log
## minimal reverse proxy
server {
listen 80;
server_name app.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;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
## static site / SPA
root /srv/app; index index.html;
try_files $uri $uri/ /index.html;
gzip on;
client_max_body_size 10m;
## location priority: = exact → ^~ prefix → ~ regex → plain prefix
## HTTPS
sudo certbot --nginx -d app.example.com
sudo certbot renew --dry-run
## errors: 502 app down · 504 app slow · 413 body too big · SPA-404 missing try_files