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:

  1. Static Site: Pure HTML/CSS/JS, ideal for frontend apps
  2. Reverse Proxy: Proxy backend APIs, WebSocket support
  3. PHP-FPM: WordPress, Laravel, and other PHP apps
  4. Node.js: Express, Koa, Next.js, etc.
  5. 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