Skip to main content

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​

/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 drop
  • reload: 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​

TopicKey Point
Config separationMinimize nginx.conf + separate role-based files in conf.d
Shared settingsReuse via proxy-params.conf, security-headers.conf
Zero-downtime reloadnginx -t then systemctl reload (never use restart)
CI automationRun nginx -t automatically via Git hooks or GitHub Actions
Header inheritanceRe-declare all add_headers in same block or use include