All Tools View Categories Blog About Contact Privacy

How to Configure Nginx: A Complete Guide (With Examples)

How to Configure Nginx: A Complete Guide (With Examples)

Nginx powers 30%+ of the web as a web server, reverse proxy, and load balancer — but one missing semicolon or a proxy_pass slash breaks it. This complete guide shows how to configure Nginx from first server block to production (locations, proxy, upstream, TLS, basic auth, and log levels) with tested examples — no prior Nginx needed.

TL;DR — How to Configure Nginx:
  • File: /etc/nginx/nginx.conf includes http { server { location / {} } } — every directive lives in a valid context; wrong context → emerg unknown directive.
  • First server: server { listen 80; server_name example.com; root /var/www/html; location / { try_files $uri $uri/ /index.html; } }nginx -t && nginx -s reload (always test before reload — graceful, zero-downtime).
  • Proxy: location /api/ { proxy_pass http://app:3000; include proxy_params; } with proxy_set_header Host $host; X-Real-IP $remote_addr; X-Forwarded-For $proxy_add_x_forwarded_for; X-Forwarded-Proto $scheme;
  • Balance: upstream app { least_conn; server app1:3000 weight=3; server app2:3000; }location / { proxy_pass http://app; }
  • Auth & logs: protect with auth_basic "Restricted"; auth_basic_user_file /etc/nginx/.htpasswd; — generate the file with our basic auth config generator (no htpasswd CLI needed); set verbosity via error_log /var/log/nginx/error.log warn; — pick the right level with our error log level config generator.

What Is Nginx and When Should You Use It?

Nginx (engine-x) is event-driven software that handles 10k+ concurrent connections with very low RAM — unlike thread-per-connection servers. It sits in front of or instead of your app for four jobs:

  1. Web server — serve static files: HTML, CSS, JS, images via root + try_files. Ideal for SPA builds (React/Vue) and marketing sites.
  2. Reverse proxy — forward /api/ to Node/Python/Go at proxy_pass http://app:3000, adding TLS termination, caching, headers, and rate limiting at the edge.
  3. Load balancer — distribute across upstream { server app1:3000; server app2:3000; } with methods like least_conn, ip_hash.
  4. Edge — TLS, gzip, expires, basic_auth, access_log/error_log control.
Nginx what is - web server reverse proxy load balancer use cases

If your app is a static SPA, Nginx serves index.html with try_files $uri /index.html. If it's an API, Nginx sits in front and buffers clients slowly while proxying fast to the app — protecting Node from slow-client attacks. Docs: Beginner's Guide and NGINX Admin Guide — Web Server.

Nginx vs Apache — Quick Take

Both serve HTTP, but Nginx's event-driven worker model (one worker handles thousands via epoll/kqueue) scales with less RAM than Apache's process/thread models for many concurrent keep-alive connections. For most new builds, Nginx is the default edge; Apache's .htaccess per-directory overrides still suit shared hosting where users lack root.

Install and Find Your Config in 2 Minutes

  • Ubuntu/Debian: sudo apt update && sudo apt install nginx && sudo nginx -t && sudo systemctl enable --now nginx → verify curl -I http://localhost200 OK with Server: nginx.
  • RHEL/CentOS: sudo dnf install nginx → same enable.
  • Docker: docker run -d -p 80:80 -v ./nginx.conf:/etc/nginx/nginx.conf:ro nginx:alpine
  • Config locations: main /etc/nginx/nginx.conf, includes /etc/nginx/conf.d/*.conf and /etc/nginx/sites-enabled/* (Debian convention) — include pulls them into http {}.

Check full effective config anytime: nginx -T (capital T) dumps the merged file with includes expanded — invaluable when you split sites across files. Reference: Alphabetical index of directives.

Syntax — Contexts, Directives, and Why "Unknown directive" Happens

Nginx syntax is directive value; terminated by semicolon, grouped in contexts {}. Hierarchy:

events { worker_connections 1024; }
http {
  include mime.types;
  access_log /var/log/nginx/access.log;
  server {
    listen 80; server_name example.com;
    location / { root /var/www/html; }
    location /api/ { proxy_pass http://app:3000; }
  }
}
Nginx contexts hierarchy http server location events master worker

Every directive is valid only in certain contexts. Example: worker_connections is events-only; server_name is server-only; proxy_pass is location/ if / limit_except. Put it in the wrong block → emerg "proxy_pass" directive is not allowed here. See ngx_http_core_module per-directive Context line.

Master → Worker, reload Without Downtime

master reads config and manages workers; workers handle connections via non-blocking events. Workflow: edit file → nginx -tnginx -s reload (graceful: new workers with new config, old workers drain). Never kill -9 master — that drops in-flight requests. See Controlling nginx.

Your First Server Block — Static Site That Actually Works on Refresh

server {
  listen 80;
  server_name example.com www.example.com;
  root /var/www/html;
  index index.html;
  location / {
    try_files $uri $uri/ /index.html;
  }
  location /assets/ {
    expires 7d;
    add_header Cache-Control "public, immutable";
  }
  # block PHP attempts on static host
  location ~ \.php$ { return 404; }
}
Nginx first server block location priority exact prefix regex

listen 80; binds port; server_name picks the block by Host header (first exact match, then wildcard, then regex). root points to disk; try_files $uri $uri/ /index.html is the SPA rule — without it, refreshing /dashboard returns 404 because no file /dashboard exists; with it, Nginx falls back to index.html and the JS router handles it.

Location Priority — The Order That Trips Beginners

Nginx tests in priority, not file order:

  1. location = /exact — exact, e.g., = /health
  2. location ^~ /prefix — prefix, stop regex if matched
  3. location ~ \.php$ — regex case-sensitive; ~* \.jpg$ case-insensitive
  4. location /prefix — prefix
  5. location / — fallback

Example: request /assets/app.css matches /assets/ with ^~ → no regex check → serves static with expires. Request /api/users misses ^~ /assets/ then tests regexes. Docs: location.

Reverse Proxy — Put Nginx in Front of Node/Python/Go

Your app shouldn't face raw internet directly if you need TLS, caching, or slow-client protection. Nginx buffers.

upstream app {
  server 127.0.0.1:3000;
}
server {
  listen 80;
  server_name example.com;
  location / {
    proxy_pass http://app;
    include /etc/nginx/proxy_params; # Host, X-Real-IP, X-Forwarded-*
  }
  location /api/ {
    proxy_pass http://app; # trailing slash matters — see below
  }
}

proxy_params typically contains:

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;

Without these, your Node app sees 127.0.0.1 and http even when the client used https. With them, Express/Koa can trust req.ip and redirect correctly. Critical detail: proxy_pass http://app/ with trailing slash rewrites URI (strips matched prefix), without slash keeps full URI — mixing them causes /api/users//users double slash bugs. Docs: proxy_pass and Reverse Proxy guide.

Load Balancing — One Upstream, Many Backends

upstream backend {
  least_conn;
  server app1.example.com:3000 weight=3 max_fails=3 fail_timeout=30s;
  server app2.example.com:3000 weight=2;
  server app3.example.com:3000 backup;
}
server {
  listen 80;
  location / {
    proxy_pass http://backend;
    proxy_next_upstream error timeout http_500 http_502;
  }
}
Nginx reverse proxy headers and load balancing upstream least_conn

Methods: round-robin (default), least_conn (to least busy), ip_hash (sticky by client IP), hash $request_uri (cache-friendly). weight biases, max_fails/fail_timeout marks bad, backup/down controls. For sticky sessions, prefer ip_hash or set a cookie in app — Nginx Open Source has no cookie sticky, NGINX Plus does via sticky cookie. Reference: ngx_http_upstream_module and HTTP Load Balancing.

TLS/SSL — From HTTP to HTTPS Without Fear

server {
  listen 80;
  server_name example.com;
  return 301 https://$host$request_uri;
}
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;
  include /etc/letsencrypt/options-ssl-nginx.conf;
  ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
  add_header Strict-Transport-Security "max-age=63072000" always;
}

Use Certbot: sudo certbot --nginx -d example.com -d www.example.com generates the above and sets renewal. Nginx just needs ssl_certificate + ssl_certificate_key; the return 301 server forces HTTPS. Test: curl -I https://example.com200 with strict-transport-security. Docs: Configuring HTTPS.

Lock It — Basic Auth Without the htpasswd Headache

Staging or internal tools need one password before the app loads. Nginx does it natively:

server {
  listen 443 ssl;
  server_name staging.example.com;
  auth_basic "Restricted";
  auth_basic_user_file /etc/nginx/.htpasswd;
  location / {
    proxy_pass http://app;
  }
  # public health check without auth
  location = /health { auth_basic off; proxy_pass http://app; }
}
Nginx TLS basic auth htpasswd error log levels

.htpasswd format is user:encrypted via bcrypt. On many systems you'd run htpasswd -c /etc/nginx/.htpasswd alice, but minimal Docker images and Windows lack it. Generate the exact auth_basic block + .htpasswd line in your browser with our basic auth config generator — pick user/pass, copy the auth_basic snippet and the user:$2y$... line into .htpasswd, no CLI needed. Scope: auth_basic off; inside a location exempts health checks. Docs: ngx_http_auth_basic_module.

Error Log Levels — Pick Warn, Not Debug, for Prod

Nginx logs verbosity is a single directive:

error_log /var/log/nginx/error.log warn;
# levels: debug | info | notice | warn | error | crit | alert | emerg (least → most severe)

debug logs every select/write — gigabytes per hour under load, useful only for 5 minutes of diagnosis then back to warn. warn is prod default: warnings + errors + above, without info noise. You can also set per-context: global error_log in main, override inside http or server for one vhost. Don't have to memorize order — use our error log level config generator to preview the line with the right path and level plus the access_log companion. Context: error_log and access_log (combined vs buffer=32k gzip tuning).

Includes, Access Log, gzip, expires, and Rate Limit — Production Essentials

http {
  include /etc/nginx/mime.types;
  include /etc/nginx/conf.d/*.conf;
  access_log /var/log/nginx/access.log combined buffer=32k gzip flush=5m;
  gzip on; gzip_types text/plain text/css application/json application/javascript; gzip_min_length 1024;
  limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;
  server {
    location / {
      limit_req zone=mylimit burst=20 nodelay;
      proxy_pass http://app;
    }
    location ~* \.(jpg|png|woff2)$ { expires 30d; add_header Cache-Control "public, immutable"; }
  }
}

Split sites: include /etc/nginx/sites-enabled/*.conf; keeps git history per domain. access_log ... buffer=gzip cuts disk I/O. gzip halves JSON/CSS size at tiny CPU cost — leave images already compressed. limit_req protects login endpoints from bursts — burst=20 nodelay queues then 503s. Full: gzip, expires, limit_req.

Test, Reload, and Troubleshoot — Don't Guess

nginx -t            # syntax + file existence
nginx -T            # dump merged config (check include expansion)
nginx -s reload     # graceful reload
tail -f /var/log/nginx/error.log  # live error watch
curl -I http://localhost/health   # 200? 301? 404?
Nginx test reload gzip rate limit pitfalls

Before every push: nginx -t && nginx -s reload-t catches missing ;, wrong context, unreadable ssl_certificate, or bad upstream name. Then curl -I confirms status code and Location header. In prod, watch error.log at warn tail to spot upstream timeouts immediately.

Top 5 Pitfalls Beginners Hit

SymptomCause → Fix
404 on SPA refresh /dashboardlocation / { try_files $uri /index.html; } missing → add fallback; docs try_files
/api/users → //users or 404proxy_pass http://app/; trailing slash strips /api/ — decide: with slash rewrites, without keeps full URI
emerg unknown directiveDirective in wrong context (e.g., server_name inside location) → move to valid block per docs
App sees 127.0.0.1 not client IPMissing proxy_set_header X-Forwarded-For → include proxy_params
Site loads old CSS after deployexpires 30d but no cache-busting filename → use main.abc123.js hashing, keep immutable

Other gotcha: if inside location is famously error-prone — prefer try_files and separate location blocks per If is Evil.

How to Keep Learning — Modular Files and Next Steps

Production layout most teams use:

/etc/nginx/nginx.conf      # events + http + include conf.d/*.conf
/etc/nginx/conf.d/app.conf # upstream + server { } per app
/etc/nginx/proxy_params    # shared headers
/etc/nginx/.htpasswd       # basic auth file (chmod 640)
/var/log/nginx/access.log + error.log

Generate tedious blocks instead of copying wrong Stack Overflow snippets: our Nginx tools collection builds correct auth_basic, error_log, and upcoming rate-limit/gzip snippets with the right context and semicolons — paste, nginx -t, reload. When you outgrow single host, layer DNS or cloud LB in front of multiple Nginx edges — same server {} syntax, just more IPs.

Cache, WebSockets, and Rate Limit — Production Patterns

Beyond serving and proxying, three patterns separate a dev config from prod:

  1. proxy_cache (micro-cache): for /api/ that can tolerate 10s staleness, add proxy_cache mycache; proxy_cache_valid 200 10s; proxy_cache_use_stale error timeout; with proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=mycache:10m inactive=60m; in http {}. Even 5s cuts upstream load by 10× under burst. Invalidate via proxy_cache_bypass $http_pragma; for authenticated requests. Docs: proxy_cache.
  2. WebSockets: add inside location /ws/ { proxy_pass http://app; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } — without Upgrade, handshake fails with 400. Keep proxy_read_timeout 3600s; for long-lived sockets.
  3. Rate limit login: define limit_req_zone $binary_remote_addr zone=login:10m rate=5r/s; then location = /login { limit_req zone=login burst=10 nodelay; proxy_pass http://app; } — bursts queue 10 then 503s, protecting brute force without blocking real users. Tuning at limit_req.

Security Headers and .htpasswd Reality — What to Set on Day One

Three headers most prod Nginx should add in http or server:

add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# for HTTPS only:
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

Don't add X-XSS-Protection blindly — modern browsers ignore it. For .htpasswd, Nginx expects bcrypt $2y$ — generated by htpasswd or our tool; .htpasswd must be outside root (e.g., /etc/nginx/.htpasswd, chmod 640 root:www-data) so it's not downloadable as static. Example line: alice:$2y$05$.... Without this placement, location / { root /var/www; } might serve /.htpasswd as text — test with curl http://localhost/.htpasswd should → 404, not 200.

Include Strategy — One File Per Site, No Monster nginx.conf

Month two, nginx.conf becomes 800 lines and merge conflicts bloom. Split early:

# /etc/nginx/nginx.conf
http {
  include mime.types;
  include /etc/nginx/conf.d/*.conf;  # global: gzip, limit_req_zone, upstream
  include /etc/nginx/sites-enabled/*.conf; # per-domain server { }
}
# /etc/nginx/sites-enabled/example.com.conf
server {
  listen 80; server_name example.com;
  return 301 https://$host$request_uri;
}
server {
  listen 443 ssl; server_name example.com;
  include /etc/nginx/snippets/ssl-example.com.conf;
  location / { proxy_pass http://backend; }
}

Benefits: git diff per file, nginx -T still shows merged view for debug, sites-enabled symlink pattern (Debian) lets you disable via rm symlink && nginx -s reload zero-downtime. Reference: include.

Checklist before you reload — copy these 3:
nginx -t && echo "syntax ok"
curl -I http://localhost/health  # expect 200 vs 301 vs 404 reveals server_name/proxy_pass mistake
tail -n 50 /var/log/nginx/error.log | grep -E "emerg|error|crit"  # should be quiet at warn level
If nginx -t reports duplicate listen or conflicting server name, two server blocks share the same listen + server_name — rename or merge.

Common Production Gotchas — Permissions, Real IP, and "if is evil"

Three mistakes fill forums weekly:

  1. 403 Forbidden on static: root /var/www/app but files owned by deploy:deploy 700www-data worker can't read. Fix: chown -R www-data:www-data /var/www/app && chmod 755 dirs or set user www-data; in top /etc/nginx/nginx.conf to match owner. Check with sudo -u www-data ls /var/www/app/index.html.
  2. App logs 10.0.0.2 not client IP: behind cloud LB, extra hop strips X-Forwarded-For. Add real_ip_header X-Forwarded-For; set_real_ip_from 10.0.0.0/8; real_ip_recursive on; so $remote_addr becomes real client — critical for rate limit and audit. Module: realip.
  3. if inside location: location / { if ($uri ~* api) { proxy_pass ... } } unpredictably re-evaluates phases. Rule per Nginx wiki If is Evil: avoid if for routing — use separate location = /api {} or try_files + rewrite in server context.

Logging Deep Dive — access_log Format and error_log Per Server

Default combined is often enough, but prod wants buffer + gzip + json for log pipelines:

log_format json_combined escape=json '{ "time":"$time_iso8601", "ip":"$remote_addr", "method":"$request_method", "uri":"$request_uri", "status":$status, "rt":$request_time, "ua":"$http_user_agent" }';
access_log /var/log/nginx/access.log json_combined buffer=32k gzip flush=5m;
# per vhost override: no access log for health
location = /health { access_log off; proxy_pass http://app; }
error_log /var/log/nginx/error.log warn;
# one noisy vhost more verbose:
server { listen 80; server_name debug.example.com; error_log /var/log/nginx/debug_error.log notice; }

warn vs notice vs debug dramatically changes I/O — debug at 1k rps writes ~500 MB/hour. If you must enable debug, scope it: error_log /var/log/nginx/debug.log debug; events { debug_connection 192.0.2.1; } so only that IP triggers verbose. Docs: error_log levels.

From Config to Production — Docker, Systemd, and Zero-Downtime Deploy

Whether bare metal or container, the reload model stays: nginx -t && nginx -s reload. In Docker, docker compose up -d --no-deps nginx && docker exec nginx nginx -t validates without restart. Systemd: systemctl reload nginx is the same graceful reload. For immutable deploys, bake nginx.conf into the image at COPY nginx.conf /etc/nginx/nginx.conf:ro and test at build time: RUN nginx -t in Dockerfile fails the build on typo — catching missing ; before prod.

Finally, version your configs: nginx -T > /etc/nginx/snapshot-$(date +%F).conf.dump before each reload so diff shows what changed when 3am paging hits. Pair with tail -f /var/log/nginx/error.log at warn for the first minute post-reload — quiet means healthy.

Keep testing tight: even after nginx -t passes, run curl -I http://localhost/ and curl -I https://localhost/ -k plus nginx -T | grep -E "server_name|listen|proxy_pass|error_log" — the three checks catch server_name typo, listen collision, and proxy_pass slash mistake before users do.

Copy working server {} blocks as templates — one correct block reused beats four hand-typed variants with different typos.

Bonus: keep sites-enabled symlinks in git — disabling a site is rm symlink && nginx -s reload, no file delete.

Frequently Asked Questions

Where is nginx.conf and how do I find the active config?

Main is /etc/nginx/nginx.conf but it includes conf.d/*.conf and sites-enabled/*. Run nginx -T (capital T) to dump the full merged config with all includes expanded, or nginx -V for compile flags and default paths.

What is the difference between root and alias?

root /var/www/app appends full URI: /i/top.gif/var/www/app/i/top.gif. alias /var/www/app/ replaces matched part: location /i/ { alias /data/w3/images/; }/i/top.gif/data/w3/images/top.gif. Need trailing slash on alias — missing it causes 404.

Why does proxy_pass with vs without trailing slash matter?

proxy_pass http://app/api/; with slash replaces the matched location prefix; without slash passes full URI. Example: location /api/ { proxy_pass http://app/; } maps /api/users/users at upstream, while proxy_pass http://app; keeps /api/users. Pick one intentionally per proxy_pass.

How do I enable HTTPS on Nginx?

Add a second server { listen 443 ssl; ssl_certificate ...; ssl_certificate_key ...; } plus a port 80 server that return 301 https://$host$request_uri;. Easiest: sudo certbot --nginx -d example.com creates it and renews via cron. See Configuring HTTPS.

How do I protect a site with a password (basic auth)?

Add auth_basic "Restricted"; auth_basic_user_file /etc/nginx/.htpasswd; in server or location, create .htpasswd with htpasswd -c /etc/nginx/.htpasswd alice or generate the line via our basic auth generator. Use auth_basic off; in excluded locations like = /health.

What error_log level should I use?

Levels: debug < info < notice < warn < error < crit < alert < emerg. Prod default warn (warnings + above), not debug which fills disk. Set via error_log /var/log/nginx/error.log warn; per error_log — or pick via our error log level generator.