Nginx Config Generator: From Manual Editing to Automation
Nginx Config Generator: From Manual Editing to Automation#
Last week I configured Nginx for a new project and hit the same old pitfalls. Wrong SSL certificate paths, missing gzip types, forgotten security headers… Every time I copy-paste old configs, change the domain and port, then pray nothing breaks. So I built a config generator to encode these best practices.
Core Structure of Nginx Config#
A complete Nginx configuration typically includes:
# Global settings
gzip on;
gzip_types text/plain text/css application/json;
# Upstream backend definition
upstream app_server {
server 127.0.0.1:3000;
}
# HTTP to HTTPS redirect
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
# Main server block
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://app_server;
}
}
Looks simple, but the devil is in the details. Here are the technical challenges I encountered while building the generator.
Challenge 1: Dynamic String Concatenation#
Nginx config is strict about formatting. Indentation, semicolons, braces must be correct. Manual string concatenation is error-prone:
function generateConfig(options: ConfigOptions): string {
const lines: string[] = []
const indent = ' ' // 4-space indent
// Wrong: direct concatenation, easy to miss semicolons
// lines.push('server {')
// lines.push('listen 80') // Missing semicolon!
// Correct: unified formatting
lines.push('server {')
lines.push(`${indent}listen ${options.port};`)
lines.push(`${indent}server_name ${options.domain};`)
lines.push('}')
return lines.join('\n')
}
My approach: store each line in an array, then join('\n'). Easier to control formatting and conditionally insert:
if (enableSsl) {
lines.push(`${indent}listen 443 ssl http2;`)
lines.push(`${indent}ssl_certificate ${sslCert};`)
lines.push(`${indent}ssl_certificate_key ${sslKey};`)
} else {
lines.push(`${indent}listen ${listenPort};`)
}
Challenge 2: Upstream URL Parsing#
Reverse proxy configs need upstream definitions, but users input full URLs (e.g., http://127.0.0.1:3000), while upstream only needs host:port:
# Wrong: upstream doesn't support http:// prefix
upstream app_server {
server http://127.0.0.1:3000; # Error!
}
# Correct
upstream app_server {
server 127.0.0.1:3000;
}
Strip the protocol prefix:
const proxyPass = 'http://127.0.0.1:3000'
const upstreamAddr = proxyPass.replace(/^https?:\/\//, '')
lines.push('upstream app_server {')
lines.push(`${indent}server ${upstreamAddr};`)
lines.push('}')
Challenge 3: Security Header Order and Duplication#
Nginx’s add_header has a pitfall: child locations override parent headers. Example:
server {
add_header X-Frame-Options "SAMEORIGIN";
location /api {
add_header Content-Type "application/json";
# X-Frame-Options doesn't work here!
}
}
Solution: repeat headers in each location, or use include for common config:
# /etc/nginx/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;
# Main config
server {
include /etc/nginx/security-headers.conf;
location /api {
include /etc/nginx/security-headers.conf;
add_header Content-Type "application/json";
}
}
In the generator, I add at server level with always parameter to ensure headers work even for error responses:
if (enableSecurityHeaders) {
lines.push(`${indent}add_header X-Frame-Options "SAMEORIGIN" always;`)
lines.push(`${indent}add_header X-Content-Type-Options "nosniff" always;`)
lines.push(`${indent}add_header X-XSS-Protection "1; mode=block" always;`)
lines.push(`${indent}add_header Referrer-Policy "strict-origin-when-cross-origin" always;`)
if (enableSsl) {
lines.push(`${indent}add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;`)
}
}
Challenge 4: PHP-FPM Socket Path Differences#
PHP-FPM socket paths vary across systems:
# Ubuntu/Debian
fastcgi_pass unix:/run/php/php-fpm.sock;
# CentOS/RHEL
fastcgi_pass unix:/run/php-fpm/php-fpm.sock;
# macOS Homebrew
fastcgi_pass unix:/opt/homebrew/var/run/php-fpm.sock;
The generator defaults to Ubuntu path, but should allow customization:
const phpSocket = '/run/php/php-fpm.sock' // Configurable
lines.push(`${indent}location ~ \\.php$ {`)
lines.push(`${indent}${indent}fastcgi_pass unix:${phpSocket};`)
lines.push(`${indent}${indent}fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;`)
lines.push(`${indent}${indent}include fastcgi_params;`)
lines.push(`${indent}}`)
Challenge 5: Gzip Type Selection#
Gzip compression significantly reduces transfer size, but not all types benefit. Images and videos are already compressed; re-compressing wastes CPU:
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6; # Compression level 1-9, 6 balances performance and ratio
gzip_min_length 256; # Don't compress files smaller than 256 bytes
# Only compress text-based resources
gzip_types
text/plain
text/css
application/json
application/javascript
text/xml
application/xml
application/xml+rss
text/javascript;
Note: text/html is compressed by default, no need to declare in gzip_types.
Challenge 6: Layered Caching Strategy#
Static resources should have long-term caching, but HTML files shouldn’t (users won’t see updates):
# Static resources: long-term cache
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public, no-transform";
}
# HTML: no cache
location ~* \.html$ {
expires -1;
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
no-transform prevents ISPs from modifying content (like injecting ads).
In Practice: Complete Generator Implementation#
Based on these experiences, I built an online tool: Nginx Config Generator
Supports 5 server types:
- Static Site: Pure HTML/CSS/JS, ideal for frontend apps
- Reverse Proxy: Proxy backend APIs, WebSocket support
- PHP-FPM: WordPress, Laravel, and other PHP apps
- Node.js: Express, Koa, Next.js, etc.
- Python: Django, Flask, FastAPI, etc.
Core features:
- Auto-generate SSL config (HTTP to HTTPS redirect)
- Optional gzip compression, security headers, static caching
- One-click copy or download config file
Deployment Notes#
After generating config, keep these in mind:
1. Test Config Syntax#
sudo nginx -t
This checks config syntax, preventing service failures after restart.
2. Graceful Reload#
sudo nginx -s reload
Doesn’t interrupt existing connections; new config applies to new requests.
3. Certificate Auto-Renewal#
Let’s Encrypt certificates expire in 90 days. Configure auto-renewal:
# Add cron job
sudo crontab -e
# Renew at 2 AM on the 1st of each month
0 2 1 * * /usr/bin/certbot renew --quiet --post-hook "systemctl reload nginx"
4. Log Rotation#
Nginx logs don’t auto-clean and can fill disk:
# /etc/logrotate.d/nginx
/var/log/nginx/*.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
create 0640 www-data adm
sharedscripts
postrotate
[ -f /var/run/nginx.pid ] && kill -USR1 `cat /var/run/nginx.pid`
endscript
}
Conclusion#
Nginx config seems simple but involves many details. Manual writing is prone to omissions. A generator ensures:
- Correct SSL config (TLS 1.2+, HSTS)
- Complete security headers (XSS, clickjacking protection)
- Proper performance optimization (gzip, caching)
- Full logging and monitoring setup
Next time you deploy a new project, try this tool to save copy-paste and debugging time.
Related: Docker Compose Generator | Cron Expression Parser