Nginx handles more than a third of the web's traffic in 2026 — it is the default choice for reverse proxying Node, Python, and Go applications, for SSL termination, and for serving static assets. Five years of container-native tooling haven't replaced it; they've wrapped it (most Kubernetes ingress controllers run Nginx under the hood). Here is how to set it up correctly.
What changed in 2026
- HTTP/3 (QUIC) support is stable in Nginx 1.27+. You can enable it with a few lines; it noticeably improves high-latency mobile connections.
- Nginx Unit (the dynamic app server from the Nginx team) gained traction as an alternative to the static config model, but classic Nginx still dominates reverse-proxy use.
- Let's Encrypt wildcard certs via Certbot DNS plugins are now well-supported and preferred for multi-subdomain setups.
- Brotli compression ships in more distro packages; use it alongside gzip for modern browsers.
Installation
Install from the official Nginx mainline repo to get current features:
# Ubuntu/Debian
curl -fsSL https://nginx.org/keys/nginx_signing.key | sudo gpg --dearmor \
-o /usr/share/keyrings/nginx-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] \
http://nginx.org/packages/mainline/ubuntu $(lsb_release -cs) nginx" \
| sudo tee /etc/nginx/nginx.list
sudo apt update && sudo apt install nginx -y
sudo systemctl enable --now nginx
Verify: nginx -v — you should see 1.27 or later.
Basic reverse-proxy config
# /etc/nginx/sites-available/myapp
server {
listen 80;
server_name myapp.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
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;
proxy_cache_bypass $http_upgrade;
}
}
Enable it: sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/ then sudo nginx -t && sudo systemctl reload nginx.
HTTPS with Let's Encrypt (Certbot)
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d myapp.example.com
# Follow prompts; Certbot rewrites the server block automatically
Certbot installs a systemd timer that auto-renews certificates before expiry. Verify renewal works:
sudo certbot renew --dry-run
After Certbot, your config will have listen 443 ssl and the ssl_certificate directives added. Keep a backup.
HTTP/3 (QUIC)
server {
listen 443 ssl;
listen 443 quic reuseport; # HTTP/3
http2 on;
add_header Alt-Svc 'h3=":443"; ma=86400';
# ... rest of your config
}
Open UDP port 443 in your firewall: sudo ufw allow 443/udp.
Performance tuning
# /etc/nginx/nginx.conf
worker_processes auto; # matches CPU cores
worker_rlimit_nofile 65535;
events {
worker_connections 4096; # per worker; total = workers × connections
use epoll;
multi_accept on;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
gzip on;
gzip_types text/plain text/css application/json application/javascript;
gzip_min_length 256;
}
Config comparison at a glance
| Scenario |
Directive |
| Proxy to Node/Python app |
proxy_pass http://127.0.0.1:PORT |
| Serve static files |
root /var/www/html; try_files $uri $uri/ =404; |
| WebSocket support |
proxy_set_header Upgrade $http_upgrade; Connection 'upgrade' |
| Rate limiting |
limit_req_zone + limit_req |
| Load balance upstream |
upstream block with server entries |
| Basic auth |
auth_basic + auth_basic_user_file |
How to pick between Nginx and alternatives
| Tool |
Best for |
| Nginx |
General-purpose reverse proxy, static files, SSL termination |
| Caddy |
Zero-config HTTPS, smaller setups, automatic cert management |
| Traefik |
Kubernetes/Docker-first, dynamic config via labels |
| HAProxy |
High-throughput TCP/HTTP load balancing |
| Apache |
Legacy PHP apps, .htaccess workflows |
Common mistakes
Not testing config before reloading. Always run sudo nginx -t — a syntax error takes down the server on reload.
Forgetting to forward the real IP. Without X-Forwarded-For and X-Real-IP headers your app sees 127.0.0.1 for every request. Pass them explicitly.
Default worker_connections 768. On modern hardware, 4096–8192 per worker is common. Profile your actual concurrency.
Serving large uploads through Nginx without tuning client_max_body_size. The default is 1 MB. Override per location for file upload endpoints.
Using if in location blocks. Nginx's if directive has well-known bugs — use map or try_files instead.
What to skip
- Nginx Plus unless you need the dashboard, active health checks, or OIDC module — the open-source version handles 99% of use cases.
- Complex Lua logic in OpenResty unless you know what you're doing; reach for application-layer middleware instead.
- Manual certificate management — Certbot or Caddy handle this for you.
FAQ
How do I reload Nginx without downtime?
sudo systemctl reload nginx sends SIGHUP, which reloads config gracefully without dropping connections.
Where do I put per-domain configs?
/etc/nginx/sites-available/<domain> with a symlink from sites-enabled/. Never edit nginx.conf for per-site settings.
How do I debug a 502 Bad Gateway?
Check sudo journalctl -u nginx and your app's logs. Usually the upstream process crashed or is not listening on the expected port.
How do I set up a subdomain redirect?
Add a separate server block with server_name sub.example.com; and use return 301 https://www.example.com$request_uri;.
Where to go next