Skip to content

Nginx Cheat Sheet

Nginx is an event-driven web server, reverse proxy, and load balancer. This cheatsheet focuses on real-world configuration patterns and ops workflows.


Install & Service Control

Common paths

  • Main config: /etc/nginx/nginx.conf
  • Site configs (Debian/Ubuntu): /etc/nginx/sites-available/ + /etc/nginx/sites-enabled/
  • Logs: /var/log/nginx/access.log and /var/log/nginx/error.log
  • PID (common): /run/nginx.pid or configured via pid in nginx.conf

Service commands (systemd)

sudo systemctl status nginx
sudo systemctl start nginx
sudo systemctl stop nginx
sudo systemctl restart nginx
sudo systemctl reload nginx

Validate config and reload safely

sudo nginx -t
sudo nginx -T            # dump full config (includes)
sudo nginx -s reload     # signal master to reload
sudo nginx -s quit       # graceful shutdown
sudo nginx -s stop       # fast stop

Config File Structure

Top-level blocks

# global (main context)
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /run/nginx.pid;

events { worker_connections 1024; }

http {
  include       /etc/nginx/mime.types;
  default_type  application/octet-stream;

  log_format main '$remote_addr - $remote_user [$time_local] '
                  '"$request" $status $body_bytes_sent '
                  '"$http_referer" "$http_user_agent" '
                  'rt=$request_time uct=$upstream_connect_time '
                  'uht=$upstream_header_time urt=$upstream_response_time';

  access_log /var/log/nginx/access.log main;

  sendfile on;
  tcp_nopush on;
  keepalive_timeout 65;

  include /etc/nginx/conf.d/*.conf;
  include /etc/nginx/sites-enabled/*;
}

Directive scope

Some directives are valid only in specific contexts (http, server, location, etc.). If NGINX complains about “directive is not allowed here”, it’s usually a scope issue.


Server Blocks

Minimal HTTP server

server {
  listen 80;
  server_name example.com www.example.com;

  root /var/www/example;
  index index.html;

  location / {
    try_files $uri $uri/ =404;
  }
}

Default server (catch-all)

server {
  listen 80 default_server;
  server_name _;
  return 444; # drop connection (optional)
}

Location Matching (Important)

NGINX chooses a location using a defined order. Practical tips: - Use = for exact matches - Use ^~ to prefer a prefix match over regex matches - Regex locations (~, ~*) are evaluated after prefix selection

location = /healthz { return 200 "ok\n"; }
location ^~ /static/ { root /srv/site; }
location ~* \.(png|jpg|css|js)$ { expires 30d; }
location / { try_files $uri $uri/ /index.html; }

Static Files

Efficient static delivery

server {
  listen 80;
  server_name static.example.com;

  root /srv/static;

  location / {
    try_files $uri =404;
  }

  location ~* \.(css|js|png|jpg|jpeg|gif|svg|ico|woff2?)$ {
    access_log off;
    expires 30d;
    add_header Cache-Control "public, max-age=2592000, immutable";
    try_files $uri =404;
  }
}

Single Page App (SPA) fallback

location / {
  try_files $uri $uri/ /index.html;
}

Reverse Proxy

Basic proxy to an upstream app

upstream app_backend {
  server 127.0.0.1:8080;
  keepalive 32;
}

server {
  listen 80;
  server_name app.example.com;

  location / {
    proxy_pass http://app_backend;

    proxy_http_version 1.1;
    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;

    proxy_connect_timeout 5s;
    proxy_send_timeout 30s;
    proxy_read_timeout 30s;
  }
}

WebSockets

location /ws/ {
  proxy_pass http://app_backend;

  proxy_http_version 1.1;
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection "upgrade";
}

Load Balancing (Upstreams)

Round robin (default)

upstream api {
  server 10.0.0.10:9000;
  server 10.0.0.11:9000;
}

Weighted

upstream api {
  server 10.0.0.10:9000 weight=3;
  server 10.0.0.11:9000 weight=1;
}

Failure handling

upstream api {
  server 10.0.0.10:9000 max_fails=3 fail_timeout=10s;
  server 10.0.0.11:9000 max_fails=3 fail_timeout=10s;
}

Caching (Reverse Proxy Cache)

proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=appcache:50m
                 max_size=5g inactive=60m use_temp_path=off;

server {
  listen 80;
  server_name cache.example.com;

  location / {
    proxy_pass http://app_backend;

    proxy_cache appcache;
    proxy_cache_key "$scheme$request_method$host$request_uri";
    proxy_cache_valid 200 302 10m;
    proxy_cache_valid 404 1m;

    add_header X-Cache-Status $upstream_cache_status;
  }
}

Compression

gzip

gzip on;
gzip_comp_level 5;
gzip_min_length 1024;
gzip_types
  text/plain text/css application/json application/javascript
  application/xml image/svg+xml;
gzip_vary on;

TLS (HTTPS)

Minimal TLS server

server {
  listen 443 ssl http2;
  server_name example.com;

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

  location / {
    proxy_pass http://app_backend;
  }
}

Redirect HTTP → HTTPS

server {
  listen 80;
  server_name example.com;
  return 301 https://$host$request_uri;
}

HSTS (be careful in production)

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

Security Headers (Common Baseline)

add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "no-referrer" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
# CSP is app-specific; start strict and iterate
# add_header Content-Security-Policy "default-src 'self';" always;

Rate Limiting & Connection Limiting

Requests per second (token bucket)

limit_req_zone $binary_remote_addr zone=req_zone:10m rate=10r/s;

server {
  location /api/ {
    limit_req zone=req_zone burst=20 nodelay;
    proxy_pass http://api;
  }
}

Concurrent connections

limit_conn_zone $binary_remote_addr zone=conn_zone:10m;

server {
  location / {
    limit_conn conn_zone 20;
    proxy_pass http://app_backend;
  }
}

Access Control

Allow/Deny by IP

location /admin/ {
  allow 10.0.0.0/8;
  deny all;
  proxy_pass http://app_backend;
}

Basic Auth

location /private/ {
  auth_basic "Restricted";
  auth_basic_user_file /etc/nginx/.htpasswd;
  proxy_pass http://app_backend;
}

Create password file:

sudo apt-get install -y apache2-utils
htpasswd -c /etc/nginx/.htpasswd user1


Logging

Custom log format with upstream timing

log_format main '$remote_addr - $remote_user [$time_local] '
                '"$request" $status $body_bytes_sent '
                '"$http_referer" "$http_user_agent" '
                'rt=$request_time uct=$upstream_connect_time '
                'uht=$upstream_header_time urt=$upstream_response_time';

access_log /var/log/nginx/access.log main;
error_log  /var/log/nginx/error.log warn;

Common variables

  • $remote_addr, $host, $request, $request_uri, $status
  • $http_user_agent, $http_referer
  • $request_time
  • $upstream_addr, $upstream_status, $upstream_response_time

Troubleshooting Playbook

1) Syntax and effective config

nginx -t
nginx -T | less

2) Check logs fast

tail -f /var/log/nginx/error.log
tail -f /var/log/nginx/access.log

3) Validate upstream connectivity from the NGINX host

curl -v http://127.0.0.1:8080/health

4) Common symptoms

  • 403: file permissions, deny all, missing index, wrong root/alias
  • 404: root path mismatch, try_files order, alias usage
  • 502: upstream down, wrong proxy_pass, bad DNS, timeout, app crash
  • 504: upstream slow; increase proxy_read_timeout, fix app performance

5) Debug logging (temporary)

error_log /var/log/nginx/error.log debug;
Reload, reproduce, then revert (debug logs are noisy).


Root vs Alias (Classic Gotcha)

# root appends the URI to the path
location /static/ {
  root /srv/site;         # /static/app.css -> /srv/site/static/app.css
}

# alias replaces the matching prefix
location /static/ {
  alias /srv/site/static; # /static/app.css -> /srv/site/static/app.css
}