LAB 04 – Static content, caching & compression¶
Story: Marketing complains the shop is slow on mobile, and the catalog API melts under traffic although the catalog changes twice a day. Make the edge do the heavy lifting.
| Duration | ~40 min |
| You edit | conf.d/05-compression.conf, conf.d/00-cache.conf, 10-shop.conf, 30-api.conf |
| Solution | solutions/LAB04/ (or ./bin/ngx catchup 4) |
flowchart LR
C[client] --> E[edge NGINX]
E -- static + gzip --> F[(/srv/www/shop)]
E -- MISS --> A[api_backend]
E -- HIT / STALE --> K[(proxy cache)]
Part A – Compression¶
- Measure first:
curl -s -o /dev/null -w "plain: %{size_download} bytes\n" http://shop.localtest.me/assets/app.js
curl -s -o /dev/null -w "gzip : %{size_download} bytes\n" -H "Accept-Encoding: gzip" http://shop.localtest.me/assets/app.js
Both the same? NGINX does not compress by default.
- Create
env/nginx/conf.d/05-compression.conf(http level):
gzip on;
gzip_comp_level 5;
gzip_min_length 1024;
gzip_vary on;
gzip_proxied any;
gzip_types text/plain text/css application/javascript application/json
image/svg+xml application/xml;
✅ Checkpoint: repeat the measurement – the gzip version is ~90% smaller and the response has Content-Encoding: gzip and Vary: Accept-Encoding. (text/html is always compressed – do not list it.)
- Pre-compression – compress once at build time instead of on every request:
Part B – Browser caching for static assets¶
Replace the /assets/ and image locations of the shop (you can drop the X-Location debug headers now):
location ^~ /assets/ {
gzip_static on; # serve app.js.gz if it exists
access_log off;
add_header Cache-Control "public, max-age=2592000, immutable" always;
}
location ~* \.(png|jpe?g|gif|svg|ico)$ {
add_header Cache-Control "public, max-age=86400" always;
}
✅ Checkpoint: curl -sI -H "Accept-Encoding: gzip" http://shop.localtest.me/assets/app.js shows Cache-Control: public, max-age=2592000, immutable and Content-Encoding: gzip. Run curl -sI twice – the ETag / Last-Modified stay the same.
Why
immutableis safe only for fingerprinted file names (app.3f9a1c.js) – discuss.
Part C – Proxy cache for the API¶
- Define the cache storage –
env/nginx/conf.d/00-cache.conf:
proxy_cache_path /var/cache/nginx/edge/api levels=1:2 keys_zone=api_cache:10m
max_size=200m inactive=10m use_temp_path=off;
- Add a cached location to the api server:
location /catalog/ {
proxy_pass http://api_backend;
include /etc/nginx/snippets/proxy-headers.conf;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_cache api_cache;
proxy_cache_key $scheme$host$request_uri;
proxy_cache_valid 200 30s;
proxy_cache_valid 404 5s;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_background_update on;
proxy_cache_lock on;
proxy_cache_bypass $http_x_no_cache;
proxy_no_cache $http_x_no_cache;
add_header X-Cache-Status $upstream_cache_status always;
}
- Watch the cache work:
for i in 1 2 3 4; do curl -s -D - http://api.localtest.me/catalog/ -o /tmp/body | grep -i x-cache-status; grep Name /tmp/body; done
✅ Checkpoint: MISS then HIT, HIT, HIT – and always the same Name, although round robin is active. That's the cache answering.
Repeating the lab later? The cache lives on a Docker volume and survives restarts, so the first answer may be EXPIRED or STALE (an old copy older than 30 s) instead of MISS. Use a new path such as /catalog/2 to see a clean MISS.
- Bypass:
curl -sI -H "X-No-Cache: 1" http://api.localtest.me/catalog/ | grep -i x-cache→BYPASS. - Cache as a safety net – stop all API instances:
docker compose -f env/docker-compose.yml stop api-1 api-2 api-3
sleep 31
curl -sI http://api.localtest.me/catalog/ | grep -i x-cache-status
docker compose -f env/docker-compose.yml start api-1 api-2 api-3
✅ Checkpoint: STALE – the shop keeps working while the API is down.
- Look inside:
docker compose -f env/docker-compose.yml exec edge find /var/cache/nginx/edge/api -type f | head
Bonus¶
- Microcaching:
proxy_cache_valid 200 1s;on/and runheywith and without it. How many requests reach the backends? - Why is
proxy_cache_key $uri(seen in many online tutorials) dangerous? (two hosts, query strings…) - Cache purge is a Plus feature – how would you invalidate the cache in OSS? (short TTL + background update, versioned URLs, or delete files on the volume)
- Brotli usually beats gzip by 15–20% – but needs the third-party
ngx_brotlimodule. You will build modules in LAB09.
Troubleshooting¶
| Symptom | Likely cause |
|---|---|
always MISS |
backend sends Set-Cookie / Cache-Control: private, or proxy_cache_valid missing |
no X-Cache-Status header |
add_header without always on error responses, or wrong location |
gzip_static ignored |
no .gz file next to the original, or the client sent no Accept-Encoding: gzip (remember to re-run gzip -k after every build – nginx does not compare timestamps) |
Files for this lab: Think-Cube/nginx-labs/labs/LAB04-caching-compression