Pro Tips — Config Separation Strategy, Reload Internals, and Validation Automation
Hard-won lessons from operating Nginx in production: how to structure configuration files, how zero-downtime reload works, and how to build an automated validation pipeline.
Configuration File Separation Strategy
Recommended Directory Structure
/etc/nginx/
├── nginx.conf ← Global settings (minimized)
├── conf.d/
│ ├── upstream.conf ← upstream block definitions
│ ├── gzip.conf ← Shared gzip settings
│ ├── ssl-params.conf ← Common SSL/TLS parameters
│ ├── proxy-params.conf ← Common proxy header settings
│ └── security-headers.conf ← Common security headers
├── sites-available/
│ ├── example.com.conf
│ └── api.example.com.conf
└── sites-enabled/
└── example.com.conf -> ../sites-available/example.com.conf
Minimized nginx.conf
user nginx;
worker_processes auto;
worker_rlimit_nofile 65535;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 4096;
use epoll;
multi_accept on;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
server_tokens off;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*.conf;
}
Shared proxy params file
# /etc/nginx/conf.d/proxy-params.conf
proxy_http_version 1.1;
proxy_set_header Connection "";
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 10s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
Zero-Downtime Reload Internals
When nginx -s reload (or systemctl reload nginx) is executed, configuration is applied without interrupting service.
Reload Sequence
1. Admin: nginx -s reload (or kill -HUP master_PID)
│
2. Master process receives SIGHUP signal
│
3. Master process reads and validates new config
→ If error: reload aborted, existing config preserved
│
4. New worker processes created with new config (start accepting connections)
│
5. Old worker processes signaled to stop accepting new connections
│
6. Old worker processes complete in-flight requests and exit
│
7. Transition complete — zero downtime
# Production zero-downtime reload procedure
sudo nginx -t # Step 1: Validate syntax
sudo systemctl reload nginx # Step 2: Apply if valid
restart vs reload:
restart: Immediately kills master + workers, then restarts → brief connection dropreload: Only replaces workers, in-flight requests complete on old workers → zero downtime
Validation Automation
Deployment Script with Validation
#!/bin/bash
set -e
CONFIG_FILE="/etc/nginx/nginx.conf"
echo "=== Nginx Config Validation ==="
if nginx -t -c $CONFIG_FILE; then
echo "✅ Syntax check passed"
else
echo "❌ Syntax errors found — aborting deployment"
exit 1
fi
echo "=== Reloading Nginx ==="
systemctl reload nginx
echo "✅ Nginx reloaded successfully"
GitHub Actions CI Pipeline
# .github/workflows/nginx-check.yml
name: Nginx Config Validation
on:
pull_request:
paths:
- 'nginx/**'
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Validate Nginx config
run: |
docker run --rm \
-v ${{ github.workspace }}/nginx:/etc/nginx \
nginx nginx -t
- name: Print config (for debugging)
run: |
docker run --rm \
-v ${{ github.workspace }}/nginx:/etc/nginx \
nginx nginx -T
Common Mistakes and Solutions
Mistake 1: add_header Inheritance Issue
http {
add_header X-Frame-Options SAMEORIGIN; # HTTP-level header
server {
location / {
add_header Content-Type text/html;
# X-Frame-Options disappears in this location!
}
}
}
Solution: Declare all headers together in the same block, or use include:
# /etc/nginx/conf.d/security-headers.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;
# In location:
location / {
include /etc/nginx/conf.d/security-headers.conf;
}
Mistake 2: proxy_pass Trailing Slash Confusion
# Wrong: /api/users → backend //users (double slash)
location /api {
proxy_pass http://backend/;
}
# Correct:
location /api/ {
proxy_pass http://backend/;
}
Mistake 3: Leaving worker_processes at Default
# With defaults: max connections = 1 × 1024 = 1024
worker_processes 1;
events { worker_connections 1024; }
# For a 4-core server:
worker_processes auto; # or 4
events { worker_connections 4096; } # Total: 16,384 connections
Nginx Performance Benchmark Tools
ab (Apache Bench)
ab -n 100 -c 10 http://localhost/
# Key metrics:
# Requests per second: throughput
# Time per request: average response time (ms)
# Failed requests: failure count
wrk
wrk -t4 -c100 -d30s http://localhost/api/test
# Results:
# Requests/sec: throughput
# Latency: distribution (avg, stdev, max, 99%)
Summary
| Topic | Key Point |
|---|---|
| Config separation | Minimize nginx.conf + separate role-based files in conf.d |
| Shared settings | Reuse via proxy-params.conf, security-headers.conf |
| Zero-downtime reload | nginx -t then systemctl reload (never use restart) |
| CI automation | Run nginx -t automatically via Git hooks or GitHub Actions |
| Header inheritance | Re-declare all add_headers in same block or use include |