Best Practices #
We close this guidebook with a collection of best practices distilled from experience managing Nginx in various large-scale production environments. Some of these concepts may have been discussed scattered across previous articles, but collecting them in one place as a final reference will greatly help us put together a readiness checklist before publishing a site (deployment).
Nginx configuration in production isn’t just about making our site accessible, but about how we ensure the server is easy to maintain, secure by default, monitored in performance, and can be instantly restored if a failure occurs. In this closing article, we’ll discuss file organization strategies, safe deployment workflows, Git-based configuration management, security checklists, and preventive monitoring.
Safe Git-Nginx Deployment Workflow #
Making configuration changes directly on a production server without testing and tracking is a high-risk action. We must implement a structured workflow involving syntax validation and change history storage before activating new configurations.
Here’s a diagram of the safe workflow we must follow for every Nginx configuration change:
flowchart TD
Edit["1. Modify the Configuration File<br/>(/etc/nginx/conf.d/)"] --> Test["2. Syntax Validation<br/>(sudo nginx -t)"]
Test -->|"Syntax Error"| Fix["Fix Typo / Syntax"]
Fix --> Test
Test -->|"Syntax OK"| Commit["3. Commit to Git VCS<br/>(git commit -m)"]
Commit --> Reload["4. Graceful Nginx Reload<br/>(systemctl reload nginx)"]
Reload --> Verify{"5. Test Access & Check Logs?"}
Verify -->|"There's a Problem"| Rollback["6. Instant Rollback via Git<br/>(git checkout HEAD~1 & reload)"]
Verify -->|"Everything Runs Normally"| Live["7. Configuration Active in Production"]
classDef default fill:#f9f9f9,stroke:#d1d5db,stroke-width:1px,color:#111827;
classDef stepStyle fill:#f0fdf4,stroke:#15803d,stroke-width:2px,color:#166534;
classDef errStyle fill:#fef2f2,stroke:#ef4444,stroke-width:2px,color:#991b1b;
class Edit,Test,Commit,Reload,Verify,Live stepStyle;
class Fix,Rollback errStyle;Modular Configuration File Organization Structure #
For long-term maintenance ease, we must avoid writing giant configurations inside a single nginx.conf file. We must divide the configuration into small, neatly organized modules inside the /etc/nginx/ directory:
/etc/nginx/
├── nginx.conf ← Main global configuration (worker, events, global http block)
├── conf.d/
│ ├── 00-default.conf ← Default server block (catch-all to block IP scans)
│ ├── app.conf ← One file per application domain virtual host
│ └── api.conf
└── snippets/
├── ssl-params.conf ← Industry-standard TLS/SSL parameters used together
├── security-headers.conf ← A collection of browser security headers
└── proxy-params.conf ← Standard reverse proxy forwarding headers
Creating Modular Snippets #
By arranging snippets, we avoid duplicating configuration code inside virtual host files. For example, we create the /etc/nginx/snippets/proxy-params.conf file:
# /etc/nginx/snippets/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;
# Standard proxy timeouts
proxy_connect_timeout 5s;
proxy_read_timeout 60s;
proxy_send_timeout 60s;
Inside our virtual host configuration file (e.g., /etc/nginx/conf.d/app.conf), we just call that snippet with the include directive:
server {
listen 443 ssl http2;
server_name app.unisbadri.com;
location / {
include snippets/proxy-params.conf;
proxy_pass http://nodejs_upstream;
}
}
A Secure Default Server (Catch-All Block) #
By default, if a request comes to our server IP using a domain not registered in the configuration (e.g., the result of automated scans by attacker bots), Nginx routes that request to the first server block it loads. This can accidentally expose our internal applications.
We must create a default server (catch-all) configuration file at /etc/nginx/conf.d/00-default.conf whose job is to catch and immediately reject all misdirected requests:
# /etc/nginx/conf.d/00-default.conf
# We name it starting with "00-" so this file loads first by Nginx
server {
listen 80 default_server;
listen 443 ssl default_server;
server_name _; # Catches all non-matching domains
# Nginx needs a dummy certificate so listen 443 default_server works
# We create a dummy self-signed certificate specifically for this default block
ssl_certificate /etc/nginx/ssl/default-dummy.crt;
ssl_certificate_key /etc/nginx/ssl/default-dummy.key;
# Nginx's non-standard 444 response code
# Nginx immediately closes the TCP connection without giving any response body
# This step saves our bandwidth and hides our server's existence from port scanners
return 444;
}
Git Version Control-Based Configuration Management #
Because Nginx configuration is purely text code, we must treat it like application code by storing it in a Git version control system. This gives us a change audit history, neat team collaboration, and the ability to do instant rollback when errors occur.
1. Initializing Git in the Nginx Folder #
cd /etc/nginx
sudo git init
# Create a .gitignore file to ignore sensitive certificate files
sudo bash -c 'echo "ssl/
*.key
*.pem" > .gitignore'
# Do the first commit
sudo git add .
sudo git commit -m "Initial commit: Basic Nginx configuration"
2. Configuration Change Procedure #
Make it a habit to commit every time we make a successful small modification:
# Make the change
sudo nano conf.d/app.conf
# Syntax validation
sudo nginx -t
# If valid, do the commit
sudo git add -A
sudo git commit -m "Refactor: Update proxy timeout for /api/upload"
# Activate the configuration
sudo systemctl reload nginx
3. Doing an Instant Rollback When Obstacles Occur #
If a new configuration we deployed triggers a hidden bug in production, we can restore the configuration to the previous revision instantly using Git:
# See the list of recent commits
sudo git log --oneline
# Restore the configuration file to the previous commit
sudo git checkout HEAD~1 -- conf.d/app.conf
# Validate and reactivate
sudo nginx -t && sudo systemctl reload nginx
Always Use Graceful Reload #
Never use the systemctl restart nginx or nginx -s stop command in a production environment if our goal is only updating the configuration. A hard restart cuts all active client TCP connections currently downloading files or processing transactions, and triggers Connection refused errors for a few seconds.
Always use the graceful reload directive:
sudo systemctl reload nginx
# Or
sudo nginx -s reload
How Graceful Reload Works: #
- The Nginx master process revalidates the configuration file.
- If valid, the master process launches a set of new worker processes with the newly updated configuration.
- The master process sends a signal to old worker processes to stop accepting new connections and only focus on finishing the active request processes they’re currently handling.
- After all old requests finish being served, the old worker processes die orderly. Not a single client request is cut or fails throughout this process.
CI/CD Pipeline Integration for Automated Configuration Testing #
For modern developer teams, letting humans manually run nginx -t on production servers is risky if they forget to do it. The best practice combined with Git usage is building an automated CI/CD Pipeline (like using GitHub Actions or GitLab CI) that validates every configuration change before merging into the main branch.
We can implement automated configuration testing using the following two methods:
1. Static Linting Using Gixy #
Gixy is a static analyzer tool specifically for Nginx configuration. Gixy automatically detects security holes (like SSRF, HTTP splitting, or alias path traversal misconfigurations):
# Example GitHub Actions Workflow (.github/workflows/nginx-lint.yml)
name: Nginx Configuration Lint
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v3
- name: Run Gixy Linter
uses: yandex/gixy-action@master
with:
config_path: nginx.conf
2. Syntax Testing Using a Docker Container #
We can also create a simple integration test by launching the official Nginx Docker container to dynamically validate our configuration file syntax:
validate-syntax:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v3
- name: Test Syntax in Docker
run: |
docker run --rm \
-v ${{ github.workspace }}:/etc/nginx \
nginx:alpine nginx -t
By integrating both tests into our CI/CD pipeline, every configuration change is guaranteed to be syntactically safe and free of security holes before touching our physical production servers.
Building High Availability (Nginx Cluster) #
In high-level production environments, a single Nginx server alone can become a Single Point of Failure (SPOF). If our Nginx VPS machine dies due to data center hardware failure, our entire site goes offline even though our backend cluster is healthy.
To achieve High Availability (HA), we must duplicate the Nginx server (e.g., into two nodes: Nginx-Active and Nginx-Passive) and combine them using Keepalived based on the VRRP protocol (Virtual Router Redundancy Protocol).
1. How a Virtual IP (VIP) Works #
Keepalived provides a single virtual IP address (Virtual IP / VIP) held together by both Nginx servers. Our site domain (e.g., app.unisbadri.com) points to this VIP address in DNS, not to the server’s physical IP.
2. Automatic Failover Scheme #
Keepalived constantly sends heartbeat packets (ping) between Nginx servers.
- Normal Condition: All traffic from the VIP flows to the
Nginx-Activeserver. - Failover Condition: If the
Nginx-Activeserver suddenly dies, Keepalived on theNginx-Passiveserver detects the loss of heartbeat packets within milliseconds, then automatically takes over that VIP address (IP takeover). - Client traffic is instantly redirected to the second server without any downtime noticed by outside users.
3. Keepalived Monitor Script Configuration #
We configure Keepalived in the /etc/keepalived/keepalived.conf file to monitor the local Nginx process health:
vrrp_script chk_nginx {
script "/usr/bin/killall -0 nginx" # Check whether the nginx process is active
interval 2 # Check every 2 seconds
weight 2
}
If the Nginx process is detected dead, that node’s priority is lowered, triggering automatic failover to the backup passive node.
Minimum Default Security Hardening Checklist #
Make sure the following security hardening directives are installed globally in /etc/nginx/nginx.conf:
http {
# 1. Hide the Nginx version from response headers and default error pages
server_tokens off;
# 2. Limit the request body size to fend off wild giant file uploads (default 1MB)
client_max_body_size 10m;
# 3. Timeout tuning to mitigate Slowloris attacks (slow header/body sending)
client_header_timeout 15s;
client_body_timeout 15s;
send_timeout 15s;
keepalive_timeout 65s;
# 4. Limit buffer sizes to prevent buffer overflow exploits
client_header_buffer_size 1k;
large_client_header_buffers 4 8k;
}
Preventive Monitoring & Alerting #
Managing a server in production demands us to be proactive. We must know if an obstacle occurs on the server before our users report it.
1. Secure the Nginx Process to Auto-Restart on Crash #
Make sure systemd is configured to automatically bring Nginx back up if its process suddenly dies:
# Check the Nginx service file
sudo systemctl edit nginx
Add the following parameters in the override file:
[Service]
Restart=on-failure
RestartSec=5s
2. Install Disk Capacity Alerting (Log Exhaustion Prevention) #
Swollen Nginx logs can consume all the remaining storage capacity of our server SSD (disk exhaustion). If the disk is 100% full, the OS can’t write temporary files and Nginx will crash. Create a simple cron job to periodically monitor remaining disk capacity:
#!/bin/bash
# A simple disk space monitoring script (alert if usage > 90%)
USAGE=$(df -h / | awk 'NR==2 {print $5}' | cut -d'%' -f1)
if [ "$USAGE" -gt 90 ]; then
echo "Warning: Server disk capacity critical! Current usage: $USAGE%" | mail -s "Disk Alert Server" [email protected]
fi
Production Readiness Checklist #
Before releasing our Nginx server to the public, strictly verify every checklist point below:
Configuration & Infrastructure #
- The
sudo nginx -tcommand runs successfully without warnings. - Configuration files are stored in a Git repository.
- The default server block (catch-all) is active on ports 80 and 443 with a
444response. - The Nginx service is enabled to run automatically at boot (
systemctl enable nginx).
Security #
- The
server_tokens off;directive is active at the http level. - The SSL certificate uses
fullchain.pemand has more than 30 days of remaining validity. - SSL protocols only allow
TLSv1.2andTLSv1.3. - Basic security headers (
X-Frame-Options,X-Content-Type-Options,Referrer-Policy) are active. - The HSTS header (
Strict-Transport-Security) is installed on production HTTPS virtual hosts. - Hidden file protection rules (
.git,.env) are configured with thedeny allstatus.
Performance & Tuning #
- The
worker_processes auto;directive is configured. - The file descriptor limit (
worker_rlimit_nofile) is aligned with the Linux OS limit. - Gzip/Brotli compression is active with an efficient compression level.
- Static asset caching strategies (
expires/Cache-Control) are installed correctly. - Upstream connection pooling (Keepalive) is configured for dynamic backends.
Observability #
- Log file rotation (
logrotate) runs daily to prevent disk leaks. - Custom log formats (like JSON formats) are installed for log aggregator parsing.
- The monitoring metrics endpoint (
stub_status) is enabled for the Prometheus scrape target.
Closing #
Nginx is extraordinary software — very fast, stable, memory-efficient, and flexible. It’s the backbone for most of the most active websites on the internet today. By understanding how it works deeply, we can make it a robust and reliable server infrastructure foundation for any scenario.
Our Nginx learning journey doesn’t end here. The official Nginx documentation at nginx.org/en/docs presents a very complete directive reference. Doing hands-on practice, experimenting on test servers, independently inspecting error logs, and systematically fixing failures are the best methods to strengthen our technical understanding.
Happy configuring, and may our servers always be stable!
Quick Checklist Before Every Configuration Change:
- Syntax Validation: Always run
sudo nginx -tto test the syntax.- History Commit: Do a
git committo record a safe recovery point.- Graceful Reload: Use
sudo systemctl reload nginxto minimize traffic impact.- Path Verification: Test URLs using
curland monitor the access log output directly.- Metric Monitoring: Monitor the server error rate level for 5 to 10 minutes after deployment.
← Previous: Diagnostic Tools