Skip to main content
Advertisement

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
Advertisement