This guide explains the production-ready nginx configuration used in the Liberu Control Panel helm chart.
- Overview
- Production Features
- Configuration Details
- Performance Optimizations
- Security Headers
- Customization
- Troubleshooting
The control panel uses nginx as a reverse proxy to the PHP-FPM application server. The nginx configuration has been optimized for production use with security headers, compression, and caching.
Location: helm/control-panel/templates/nginx-configmap.yaml
The nginx configuration includes the following production-ready features:
- Gzip Compression: Reduces bandwidth by 60-80%
- Sendfile: Enabled for 2-3x faster file serving
- TCP Optimizations:
tcp_nopushandtcp_nodelayenabled - Connection Pooling: Keepalive with 65s timeout
- Static Asset Caching: 1-year cache for images, CSS, JS, fonts
- Security Headers: OWASP-recommended headers
- Hidden File Protection: Denies access to
.files - Content Security Policy: XSS and injection protection
- Frame Options: Clickjacking protection
- Clean URLs: Laravel routing with
try_files - PHP-FPM: Optimized FastCGI configuration
- Health Checks: Kubernetes-compatible
/healthendpoint
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml text/javascript
application/json application/javascript
application/xml+rss application/rss+xml
font/truetype font/opentype
application/vnd.ms-fontobject image/svg+xml;
gzip_disable "msie6";Benefits:
- Reduces response size by 60-80% for text-based content
- Compression level 6 provides good balance between CPU usage and compression ratio
- Excludes binary files (images, PDFs) that don't compress well
- IE6 compatibility disabled (modern browsers only)
keepalive_timeout 65;
keepalive_requests 100;Benefits:
- Reuses TCP connections for multiple requests
- Reduces latency and server overhead
- Allows up to 100 requests per connection
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;Header Explanations:
| Header | Purpose | Protection Against |
|---|---|---|
| X-Frame-Options | Prevents page from being loaded in iframe | Clickjacking attacks |
| X-Content-Type-Options | Prevents MIME type sniffing | Drive-by downloads, content injection |
| X-XSS-Protection | Enables browser XSS filter | Cross-site scripting (legacy browsers) |
| Referrer-Policy | Controls referrer information | Privacy leaks |
| Content-Security-Policy | Controls resource loading | XSS, data injection, code injection |
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}Benefits:
- Browsers cache static assets for 1 year
- Reduces server load and bandwidth
immutabledirective tells browsers the file never changes- Disables access logs for static files to reduce I/O
sendfile on;
tcp_nopush on;
tcp_nodelay on;Benefits:
sendfile: Uses kernel sendfile() for 2-3x faster file transferstcp_nopush: Optimizes packet sending (reduces packets)tcp_nodelay: Disables Nagle's algorithm for low latency
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_intercept_errors off;
fastcgi_buffer_size 16k;
fastcgi_buffers 4 16k;
fastcgi_connect_timeout 300;
fastcgi_send_timeout 300;
fastcgi_read_timeout 300;
}Key Settings:
- Buffer size: 16k × 4 = 64k total buffering
- Timeouts: 300s (5 minutes) for long-running requests
- Path info: Proper handling for Laravel routing
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}Purpose:
- Used by Kubernetes liveness and readiness probes
- Fast response without hitting PHP-FPM
- Doesn't generate access logs
| Optimization | Impact | Benefit |
|---|---|---|
| Gzip Compression | 60-80% smaller responses | Faster page loads, reduced bandwidth costs |
| Sendfile | 2-3x faster file serving | Lower CPU usage, faster static file delivery |
| Static Caching | 100% cache hit rate | Reduced server requests, faster repeat visits |
| Keepalive | Reduced latency | Faster subsequent requests from same client |
| Security Headers | Minimal overhead | Protection with no performance cost |
Without compression:
- HTML page: 50 KB
- CSS files: 100 KB
- JS files: 200 KB
- Total: 350 KB
With gzip (comp_level 6):
- HTML page: 10 KB (80% reduction)
- CSS files: 20 KB (80% reduction)
- JS files: 50 KB (75% reduction)
- Total: 80 KB (77% reduction)
Annual savings for 1M pageviews:
- Data transfer: 270 GB saved
- AWS S3 egress savings: ~$24/year
- User experience: Faster load times
To change gzip compression level, edit nginx-configmap.yaml:
# Lower compression (faster, less compression)
gzip_comp_level 3; # Light compression
# Higher compression (slower, more compression)
gzip_comp_level 9; # Maximum compressionRecommendations:
- Level 1-3: Low CPU usage, good for high-traffic sites
- Level 4-6: Balanced (recommended for most sites)
- Level 7-9: High compression, high CPU usage
To add custom headers, edit the server block:
add_header X-Custom-Header "value" always;
add_header Strict-Transport-Security "max-age=31536000" always; # HSTSTo change static asset cache duration:
# Short cache (1 week)
expires 7d;
# Medium cache (1 month)
expires 30d;
# Long cache (1 year) - default
expires 1y;Default is 100MB. To increase:
client_max_body_size 500m; # Allow 500MB uploadsNote: Also update PHP settings to match.
For longer-running scripts:
fastcgi_connect_timeout 600; # 10 minutes
fastcgi_send_timeout 600;
fastcgi_read_timeout 600;Cause: File upload exceeds client_max_body_size
Solution:
client_max_body_size 200m; # Increase limitAlso check PHP settings:
upload_max_filesize = 200M
post_max_size = 200MCause: PHP-FPM request takes longer than timeout
Solutions:
- Increase nginx timeouts:
fastcgi_read_timeout 600; # 10 minutes- Increase PHP max_execution_time:
max_execution_time = 600- Optimize slow code (preferred solution)
Cause: Browser not respecting cache headers
Debug:
# Check response headers
curl -I https://your-domain.com/css/app.css
# Should show:
# Cache-Control: public, immutable
# Expires: (date 1 year in future)Solution: Verify cache-busting in asset URLs (Laravel Mix adds hashes automatically)
Cause: Content type not in gzip_types list
Debug:
# Check if gzip is enabled
curl -H "Accept-Encoding: gzip" -I https://your-domain.com
# Should show:
# Content-Encoding: gzipSolution: Add content type to gzip_types list
Cause: Content Security Policy too restrictive
Solution: Adjust CSP header to allow required resources:
# Allow specific domains
add_header Content-Security-Policy "default-src 'self' https://cdn.example.com; script-src 'self' 'unsafe-inline' https://cdn.example.com" always;Tools:
- Use browser developer tools to see CSP violations
- Test CSP at: https://csp-evaluator.withgoogle.com/
The nginx configuration serves HTTP on port 80, but the Kubernetes Ingress should enforce HTTPS:
# In values.yaml
ingress:
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;Warning: Only enable HSTS after confirming HTTPS works properly!
For production, tighten the CSP policy:
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:;" always;Add to nginx config:
server_tokens off;For API endpoints or login pages:
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
location /api/ {
limit_req zone=api burst=20 nodelay;
}- Response Time: Should be <200ms for cached assets
- Compression Ratio: Should be 60-80% for HTML/CSS/JS
- Cache Hit Rate: Should be >90% for static assets
- Error Rate: Monitor 4xx and 5xx errors
Add to ConfigMap for structured logging:
log_format json_combined escape=json
'{'
'"time_local":"$time_local",'
'"remote_addr":"$remote_addr",'
'"request":"$request",'
'"status": "$status",'
'"body_bytes_sent":"$body_bytes_sent",'
'"request_time":"$request_time",'
'"http_referrer":"$http_referer",'
'"http_user_agent":"$http_user_agent"'
'}';
access_log /var/log/nginx/access.log json_combined;Install nginx-prometheus-exporter for metrics:
kubectl apply -f https://raw.githubusercontent.com/nginxinc/nginx-prometheus-exporter/main/deployments/deployment.yamlHTTP/2 is typically enabled at the Ingress Controller level, not in the nginx sidecar.
Verify HTTP/2 is enabled:
curl -I --http2 https://your-domain.com
# Should show: HTTP/2 200Enable in NGINX Ingress Controller:
ingress:
annotations:
nginx.ingress.kubernetes.io/http2-push-preload: "true"