I have configured nginx more times than I care to count. Every new server, every new project, every time I think I remember the syntax ( I don’t ).

nginx is one of those tools that does everything but the docs feel written for people who already know nginx. So here is what I actually use, in production, on every server I manage.

Server rack with network cables
The server room. Where nginx configs go to die and come back wrong.

1. One server block per file

This is the single thing that saves me the most headache. One file per site in /etc/nginx/sites-available/, symlink to sites-enabled/. No 500-line monolith.

# /etc/nginx/sites-available/example.com
server {
    listen 443 ssl http2;
    server_name example.com www.example.com;

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

    root /var/www/ghost;
    index index.html index.htm;

    location / {
        proxy_pass http://127.0.0.1:2368;
        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;
    }
}

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

Enable it:

sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/

That is it. One file. One site. Easy to debug, easy to remove, easy to copy when I need another one.

2. SSL settings that actually work

Mozilla SSL Configuration Generator is fine but I keep forgetting the URL. Here is what I actually put in every config:

# /etc/nginx/snippets/ssl.conf
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128.GCM-SHA256:ECDHE-ECDSA-AES256.GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;

Then in each server block:

include snippets/ssl.conf;

No, I don’t change the ciphers. No, I don’t enable TLSv1.0 for that one old client. The defaults are fine for everything I run.

3. Security headers in one place

Same deal ( one snippet, included everywhere ):

# /etc/nginx/snippets/security.conf
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';" always;

Yes, unsafe-inline and unsafe-eval. My blog is not a bank. If you run something more sensitive, tighten the CSP. For most of my stuff this is fine.

Important: add_header inside a location block overrides headers from the server block. Either put all headers in one place or use the always flag. I learned this the hard way.

Network cables and connections
Every cable is a potential misconfiguration. So is every nginx directive.

4. Gzip is not optional

Every server I set up gets this in /etc/nginx/nginx.conf:

gzip on;
gzip_vary on;gzip_proxied any;
gzip_comp_level 6;gzip_min_length 256;
gzip_types
    text/plain
    text/css
    text/xml
    text/javascript
    application/json
    application/javascript
    application/xml
    application/rss+xml
    image/svg+xml;

I used to skip gzip because ( the server has fast bandwidth ). Then I checked the logs and realized half my traffic is mobile on shaky connections. Just enable it.

5. Test before reload, always

This is the one rule I break and then immediately regret:

# Do this
sudo nginx -t && sudo systemctl reload nginx

# Not this
sudo systemctl reload nginx  # and pray

nginx -t takes 0.2 seconds and tells you if the config is broken before you take the site down. There is no excuse to skip it. I still skip it sometimes. I still regret it.

Terminal screen with code
The terminal where nginx configs are born. And reborn. And fixed. Again.

The config I copy everywhere

Here is my full /etc/nginx/nginx.conf, the one I copy to every new server:

user www-data;
worker_processes auto;
pid /run/nginx.pid;

events {
    worker_connections 1024;
    multi_accept on;
}

http {
    sendfile on;
    tcp_opush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;

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

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

    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_min_length 256;
    gzip_types text/plain text/css text/xml text/javascript
               application/json application/javascript application/xml
               application/rss+xml image/svg+xml;

    include /etc/nginx/sites-enabled/*;
}

That is it. No magic, no tuning worker_processes to CPU cores ( auto does that ), no custom buffer sizes I don’t understand. It works.

Conclusion

nginx configuration is not hard. It is just badly documented and everyone copies the wrong Stack Overflow answer. One file per site, snippets for shared config, test before reload. Done.

The rest ( rate limiting, caching, load balancing ) is for when you actually need it. Start simple.