Nginx Best Practices
Nginx (pronounced “engine-x”) is a web server and a reverse proxy (a server that sits in front of your app and forwards requests to it). It is one of the most popular pieces of software for serving websites and APIs on Linux. Getting the configuration right matters a lot: a small mistake can take a site offline, leak private information, or make pages load slowly. This page is a practical checklist of habits that keep your Nginx setup fast, secure, and easy to maintain on Ubuntu 22.04 and 24.04 LTS (Long Term Support).
Always test the config before you reload
Nginx reads its configuration from text files. If one of those files has a typo, Nginx can refuse to start or fail to reload, and your site goes down. The good news is Nginx can check a config for syntax errors without applying it. Always run this check first.
sudo nginx -t
Output:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
Only after you see “test is successful” should you apply the change. Use reload, not restart, because reload applies the new config without dropping existing connections (a “graceful” reload).
sudo systemctl reload nginx
Never run
sudo systemctl restart nginxon a live site without testing first. Arestartfully stops the server, and if the config is broken Nginx will not come back up, leaving your site down until you fix it.
When to use this: every single time you edit any file under /etc/nginx. Make it a reflex.
Use one server block per site
A “server block” is the chunk of config that describes how to serve one website or domain (it is Nginx’s version of what Apache calls a virtual host). On Ubuntu, each site gets its own file in /etc/nginx/sites-available/, and you “turn it on” by creating a symbolic link (a shortcut) into /etc/nginx/sites-enabled/.
sudo nano /etc/nginx/sites-available/example.com
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx
Keeping one site per file means you can enable, disable, or edit a site without touching the others. When NOT to do this: if you genuinely run only one site forever, a single file is fine, but the per-site pattern costs nothing and scales cleanly.
Here is a minimal, correct server block:
server {
listen 80;
server_name example.com www.example.com;
root /var/www/example.com/public;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
Enable gzip compression and caching
Compression shrinks text responses (HTML, CSS, JavaScript, JSON) before they travel over the network, so pages load faster. Gzip is the standard, widely supported method. Add this inside the http { } block of /etc/nginx/nginx.conf.
gzip on;
gzip_comp_level 5;
gzip_min_length 256;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml;
For files that rarely change (images, fonts, compiled CSS/JS), tell browsers to cache them so repeat visitors do not re-download them.
location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff2|svg)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
When to use this: almost always for static assets. When NOT to: do not cache HTML for dynamic pages that show user-specific or fast-changing data, or visitors may see stale content.
Set security headers
Security headers are extra instructions Nginx sends with every response that tell the browser how to behave more safely. They defend against common attacks like clickjacking and content-type tricks. Add them in your server block.
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
The always keyword makes sure the header is sent even on error responses. Add Strict-Transport-Security (HSTS, which forces browsers to use HTTPS) only once you are confident HTTPS works, because it is hard to undo.
Rate-limit sensitive endpoints
Rate limiting caps how many requests a single client can make in a window of time. It protects login pages and APIs from brute-force attacks (rapid password guessing) and abuse. Define a “zone” in the http { } block, then apply it where needed.
# In http { }
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/s;
# In the server block
location /login {
limit_req zone=login burst=10 nodelay;
proxy_pass http://127.0.0.1:3000;
}
This allows 5 requests per second per IP address, with a short burst of 10 before Nginx returns HTTP 503. When to use this: login, signup, password-reset, and any expensive API route. When NOT to: do not rate-limit static files aggressively, or normal page loads (which pull many assets) may break.
Keep TLS strong
TLS (Transport Layer Security, the technology behind HTTPS) encrypts traffic between the browser and your server. The easiest way to get and renew free certificates on Ubuntu is Certbot from Let’s Encrypt.
sudo apt update
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d example.com -d www.example.com
Certbot edits your server block to add the certificate and an HTTP-to-HTTPS redirect, and it sets up automatic renewal via a systemd timer. Verify renewal works.
sudo certbot renew --dry-run
In your TLS config, only allow modern protocols. TLS 1.0 and 1.1 are obsolete and insecure.
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
Keep configs in version control
Treat your Nginx config like code. Storing /etc/nginx in Git (a version control system that records every change) lets you review edits, roll back a bad change, and copy a known-good setup to a new server.
cd /etc/nginx
sudo git init
sudo git add .
sudo git commit -m "Initial Nginx config"
Add a
.gitignorefor anything secret (private keys,.htpasswdfiles) so credentials never land in your repository. TLS private keys under/etc/letsencryptshould also stay out of Git.
Nginx vs Apache — when to use which
| Need | Nginx | Apache |
|---|---|---|
| Reverse proxy in front of a Node/Python app | Excellent, built for it | Works, heavier |
| Many concurrent connections | Very efficient (event-driven) | Higher memory per request |
Per-directory .htaccess overrides | Not supported | Supported |
| Serving static files fast | Excellent | Good |
For most modern app deployments, Nginx is the default choice. Choose Apache when you need .htaccess overrides on shared hosting.
Best Practices
- Run
sudo nginx -tbefore every reload, and usereload(graceful) instead ofrestart. - Keep one server block per site under
sites-available, enabled via a symlink insites-enabled. - Turn on gzip and cache static assets with long
expiresheaders; never cache user-specific HTML. - Send security headers (
X-Content-Type-Options,X-Frame-Options, HSTS) on every response withalways. - Rate-limit login and expensive API routes to slow down brute-force attacks.
- Allow only TLS 1.2 and 1.3, and let Certbot manage and auto-renew certificates.
- Store
/etc/nginxin Git, and exclude private keys and password files from the repository.