Nginx Static Web Server Configuration
This dotfiles configuration includes a hardened Nginx setup for serving static websites with HTTPS.
Overview
- Ports: 80 (HTTP - redirects to HTTPS), 443 (HTTPS)
- Domains: wolfhard.net, wolfhard.dev, wolfhard.tech (with www subdomains)
- SSL/TLS: Let’s Encrypt certificates (automatic renewal)
- Document Root:
/var/www/wolfhard(symlink to/etc/nginx/www) - Security: Hardened with modern security headers and best practices
- HTTP to HTTPS: All HTTP traffic automatically redirected to HTTPS
Features
Security Hardening
- Security Headers:
- X-Frame-Options: Prevents clickjacking
- X-Content-Type-Options: Prevents MIME type sniffing
- X-XSS-Protection: XSS attack protection
- Content-Security-Policy: Restricts resource loading
- Referrer-Policy: Controls referrer information
- Server Hardening:
- Server version hidden (server_tokens off)
- Request size limits (10MB max)
- Optimized timeouts and buffer sizes
- Hidden files (.htaccess, .git) blocked
- Best Practices:
- Gzip compression enabled
- Static asset caching (1 year for images/fonts/css/js)
- UTF-8 charset
- Custom error pages (404, 50x)
Virtual Hosts
All three domains serve the same content:
- wolfhard.net (+ www.wolfhard.net)
- wolfhard.dev (+ www.wolfhard.dev)
- wolfhard.tech (+ www.wolfhard.tech)
Default Server
- Catches all requests to unknown domains
- Returns HTTP 444 (close connection without response)
- Security measure to prevent domain-based attacks
Directory Structure
dotfiles/
├── hosts/homelab/
│ └── nginx.nix # Nginx configuration module
└── web/
└── static/
├── index.html # Homepage
└── 404.html # 404 error page
Deployed to:
/etc/nginx/www/ # NixOS-managed static files
├── index.html
└── 404.html
/var/www/wolfhard -> /etc/nginx/www # Symlink
Configuration Files
Main Configuration
Located at: hosts/homelab/nginx.nix
Key sections: - Recommended Nginx settings enabled - Security headers
in appendHttpConfig - Virtual host definitions - Firewall
rules - Static file deployment
Website Content
Located at: web/static/
Files are automatically deployed to /etc/nginx/www/
during system rebuild.
Usage
Accessing the Website
# Local access
curl http://localhost:8080
# Via domain (requires DNS or /etc/hosts)
curl http://wolfhard.net:8080
# Test from browser
http://your-server-ip:8080Viewing Logs
# Access logs
tail -f /var/log/nginx/wolfhard.net.access.log
tail -f /var/log/nginx/wolfhard.dev.access.log
tail -f /var/log/nginx/wolfhard.tech.access.log
# Error logs
tail -f /var/log/nginx/wolfhard.net.error.log
# All nginx logs
journalctl -u nginx -fTesting Configuration
# Test nginx configuration syntax
sudo nginx -t
# Reload nginx (after manual config changes)
sudo systemctl reload nginx
# Restart nginx
sudo systemctl restart nginx
# Check nginx status
sudo systemctl status nginxUpdating Website Content
Method 1: Edit in Dotfiles (Recommended)
Edit files in
web/static/:vim ~/dotfiles/web/static/index.htmlRebuild system:
nrs # or: nixos-rebuild switch --flake ~/dotfiles#homelabChanges are automatically deployed
Method 2: Direct Edit (Temporary)
# Edit directly (changes lost on rebuild!)
sudo vim /etc/nginx/www/index.html
# Nginx automatically serves updated fileNote: Direct edits are lost on next system rebuild.
Always edit in web/static/ for permanent changes.
Adding New Pages
Create new HTML file in
web/static/:vim ~/dotfiles/web/static/about.htmlAdd to nginx.nix
environment.etc:environment.etc."nginx/www/about.html" = { source = ../../web/static/about.html; mode = "0644"; user = "nginx"; group = "nginx"; };Rebuild:
nrsAccess at:
http://domain:8080/about.html
Adding Static Assets
CSS Files
Create CSS file:
mkdir -p ~/dotfiles/web/static/css vim ~/dotfiles/web/static/css/style.cssAdd to nginx.nix:
environment.etc."nginx/www/css/style.css" = { source = ../../web/static/css/style.css; mode = "0644"; user = "nginx"; group = "nginx"; };Link in HTML:
<link rel="stylesheet" href="/css/style.css">
Images
Same process as CSS:
environment.etc."nginx/www/images/logo.png" = {
source = ../../web/static/images/logo.png;
mode = "0644";
user = "nginx";
group = "nginx";
};JavaScript
environment.etc."nginx/www/js/script.js" = {
source = ../../web/static/js/script.js;
mode = "0644";
user = "nginx";
group = "nginx";
};HTTPS/SSL Configuration (Let’s Encrypt)
Overview
This configuration uses NixOS’s built-in ACME support for automatic Let’s Encrypt certificates.
Features: - Automatic certificate issuance on first rebuild - Automatic renewal (checked twice daily) - HTTP to HTTPS redirect (all HTTP traffic → HTTPS) - Support for multiple domains and subdomains
How It Works
# In nginx.nix
security.acme = {
acceptTerms = true;
defaults.email = "mail@wolfhard.net";
};
# For each domain:
"wolfhard.net" = {
enableACME = true; # Request Let's Encrypt cert
forceSSL = true; # Redirect HTTP → HTTPS
serverAliases = [ "www.wolfhard.net" ];
};Certificate Management
Certificate location:
/var/lib/acme/<domain>/
# Check certificate expiry
openssl x509 -in /var/lib/acme/wolfhard.net/cert.pem -noout -dates
# View certificate details
openssl x509 -in /var/lib/acme/wolfhard.net/cert.pem -noout -text
# Force certificate renewal
systemctl start acme-wolfhard.net.service
# Check ACME service status
systemctl status acme-wolfhard.net.service
# View renewal logs
journalctl -u acme-wolfhard.net.service -fFirst-Time Setup
IMPORTANT: Configure DNS BEFORE rebuilding!
Set up DNS records (see DNS Configuration section below)
Verify DNS propagation:
dig wolfhard.net +short # Should return your server's public IPRebuild system:
nrsWatch certificate issuance (usually completes in < 1 minute):
journalctl -fu acme-wolfhard.net.serviceTest HTTPS:
curl -I https://wolfhard.netTest HTTP redirect:
curl -I http://wolfhard.net # Should show: Location: https://wolfhard.net/
Troubleshooting
Certificate issuance fails:
Check DNS:
dig wolfhard.net +short # Must return server IPCheck port 80 accessible from internet:
# From external machine: curl http://your-server-ipView ACME logs:
journalctl -u acme-wolfhard.net.service -n 100Use staging server (to avoid rate limits during testing):
# In nginx.nix security.acme.defaults = { email = "mail@wolfhard.net"; server = "https://acme-staging-v02.api.letsencrypt.org/directory"; };Then rebuild and test. Switch back to production when ready.
Manual renewal:
systemctl start acme-wolfhard.net.service
HTTP redirect not working:
- Check nginx is running:
systemctl status nginx - Check firewall:
ss -tulpn | grep nginx - View nginx logs:
journalctl -u nginx -f
Security Features
- TLS 1.2 and 1.3 only (older versions disabled)
- Modern cipher suites (via
recommendedTlsSettings) - HSTS (HTTP Strict Transport Security)
- Forward secrecy enabled
- OCSP stapling enabled
Testing SSL Configuration
# Check SSL with openssl
openssl s_client -connect wolfhard.net:443 -servername wolfhard.net
# Online SSL test
# Visit: https://www.ssllabs.com/ssltest/analyze.html?d=wolfhard.netDNS Configuration
To use your domains, configure DNS records:
A Records (IPv4)
wolfhard.net A YOUR_SERVER_IPv4
www.wolfhard.net A YOUR_SERVER_IPv4
wolfhard.dev A YOUR_SERVER_IPv4
www.wolfhard.dev A YOUR_SERVER_IPv4
wolfhard.tech A YOUR_SERVER_IPv4
www.wolfhard.tech A YOUR_SERVER_IPv4
AAAA Records (IPv6)
wolfhard.net AAAA YOUR_SERVER_IPv6
www.wolfhard.net AAAA YOUR_SERVER_IPv6
(repeat for other domains)
Adding HTTPS (Let’s Encrypt)
To enable HTTPS with Let’s Encrypt certificates:
1. Add ACME Configuration
Edit nginx.nix:
security.acme = {
acceptTerms = true;
defaults.email = "mail@wolfhard.net";
};2. Update Virtual Hosts
"wolfhard.net" = {
enableACME = true;
forceSSL = true;
# ... rest of config
};3. Open Port 80
ACME needs port 80 for validation:
networking.firewall.allowedTCPPorts = [ 80 8080 ];4. Rebuild
nrsCertificates will be automatically obtained and renewed.
Performance Optimization
Enable HTTP/2
Already enabled via recommendedTlsSettings when HTTPS is
configured.
Enable Caching
Static assets are already cached for 1 year: - Images: jpg, jpeg, png, gif, ico, svg - Fonts: woff, woff2, ttf, eot - Scripts: js - Styles: css
Enable Brotli Compression
Add to nginx.nix:
services.nginx = {
# ... existing config
appendHttpConfig = ''
# ... existing headers
# Brotli compression
brotli on;
brotli_comp_level 6;
brotli_types text/plain text/css application/json application/javascript text/xml application/xml;
'';
};Security Considerations
Current Security Features
✅ Security headers (XSS, clickjacking, etc.) ✅ Hidden files blocked ✅ Server version hidden ✅ Request size limits ✅ Timeout limits ✅ Default server returns 444 ✅ Dedicated nginx user ✅ File permissions set correctly
Additional Security Measures
Rate Limiting:
appendHttpConfig = '' limit_req_zone $binary_remote_addr zone=one:10m rate=10r/s; limit_req zone=one burst=20 nodelay; '';IP Whitelisting (if needed):
locations."/" = { extraConfig = '' allow 1.2.3.4; deny all; ''; };Fail2ban Integration: Monitor logs and ban IPs
Troubleshooting
Nginx Won’t Start
Symptoms: Service fails to start, systemd shows failed status
Diagnosis:
# Check nginx configuration syntax
sudo nginx -t
# View detailed error logs
journalctl -u nginx -n 50 --no-pager
# Check if nginx process is running
ps aux | grep nginx
# Check for port conflicts
sudo ss -tulpn | grep ':80\|:443'Common Causes:
Configuration Syntax Error
# Error message will show file and line number sudo nginx -t # Fix the error in your nix configuration and rebuildPort Already in Use
# Find what's using the port sudo lsof -i :80 sudo lsof -i :443 # Stop conflicting service sudo systemctl stop <conflicting-service>Missing SSL Certificates
# Check if certificates exist ls -la /var/lib/acme/wolfhard.net/ # Force certificate generation sudo systemctl start acme-wolfhard.net.service # Then restart nginx sudo systemctl restart nginxPermission Issues
# Check permissions on web root ls -la /var/www/wolfhard ls -la /etc/nginx/www # Fix ownership sudo chown -R nginx:nginx /var/www sudo chown -R nginx:nginx /etc/nginx/www
Page Not Found (404)
Symptoms: Website shows 404 error, nginx is running
Diagnosis:
# Check if files exist
ls -la /var/www/wolfhard/
# Check nginx error log
tail -f /var/log/nginx/wolfhard.net.error.log
# Verify symlink is correct
readlink /var/www/wolfhard
# Should output: /etc/nginx/www
# Check if www-setup service ran
systemctl status nginx-www-setup.serviceSolutions:
Broken Symlink
# Restart the setup service sudo systemctl restart nginx-www-setup.service # Or manually recreate sudo ln -sf /etc/nginx/www /var/www/wolfhardMissing Files
# Rebuild system to deploy files make switch # Verify files are in /etc/nginx/www ls -la /etc/nginx/www/Wrong Document Root
# Check nginx configuration sudo nginx -T | grep "root" # Should show: root /var/www/wolfhard;
HTTPS Not Working / Certificate Errors
Symptoms: SSL certificate errors, connection not secure warnings
Diagnosis:
# Check certificate status
sudo systemctl status acme-wolfhard.net.service
# View certificate details
sudo openssl x509 -in /var/lib/acme/wolfhard.net/cert.pem -noout -dates -subject
# Check ACME logs
sudo journalctl -u acme-wolfhard.net.service -n 100
# Test SSL connection
openssl s_client -connect wolfhard.net:443 -servername wolfhard.netCommon Issues:
DNS Not Configured
# Verify DNS points to your server dig wolfhard.net +short # Must return your server's IP # Check from external DNS dig @8.8.8.8 wolfhard.net +shortPort 80 Not Accessible
# ACME needs port 80 for HTTP-01 challenge # Test from external machine: curl -I http://YOUR_SERVER_IP # Check firewall sudo iptables -L -n | grep 80 # Verify nginx is listening sudo ss -tulpn | grep :80Certificate Expired
# Check expiry date sudo openssl x509 -in /var/lib/acme/wolfhard.net/cert.pem -noout -enddate # Force renewal sudo systemctl start acme-wolfhard.net.service # Check renewal timer is active sudo systemctl list-timers | grep acmeRate Limited by Let’s Encrypt
# Check logs for rate limit errors sudo journalctl -u acme-wolfhard.net.service | grep -i "rate" # Use staging server for testing (in nginx.nix): # security.acme.defaults.server = "https://acme-staging-v02.api.letsencrypt.org/directory";
HTTP Not Redirecting to HTTPS
Symptoms: HTTP URLs don’t redirect to HTTPS
Diagnosis:
# Test redirect
curl -I http://wolfhard.net
# Should show: Location: https://wolfhard.net/
# Check nginx config
sudo nginx -T | grep -A 5 "server_name wolfhard.net"Solution:
# Verify forceSSL is enabled in configuration
# Should be in hosts/homelab/services.nix:
# forceSSL = true;
# Rebuild if changed
make switchPerformance Issues / Slow Response
Symptoms: Website loads slowly, high latency
Diagnosis:
# Check nginx access logs for slow requests
tail -f /var/log/nginx/wolfhard.net.access.log
# Monitor system resources
htop
# Check nginx worker processes
ps aux | grep nginx
# Test response time
time curl -I https://wolfhard.net
# Check for errors
sudo journalctl -u nginx --since "1 hour ago" | grep -i errorSolutions:
Insufficient Resources
# Check memory and CPU free -h top # May need to increase server resourcesToo Many Connections
# Check current connections sudo ss -s # Increase worker connections in nginx config if neededSlow DNS Resolution
# Test DNS speed time dig wolfhard.net # Consider using resolver caching
Static Files Not Updating
Symptoms: Changes to HTML/CSS/JS don’t appear
Diagnosis:
# Check if files are updated in /etc/nginx/www
ls -la /etc/nginx/www/
cat /etc/nginx/www/index.html
# Check browser cache
# Use Ctrl+Shift+R for hard refresh
# Check nginx caching headers
curl -I https://wolfhard.net/index.html | grep -i cacheSolutions:
Files Not Deployed
# Rebuild to update files make switch # Verify files updated ls -la /etc/nginx/www/Browser Cache
# Clear browser cache or use incognito mode # Or add cache-busting query strings: # <link href="/style.css?v=2">CDN/Proxy Cache
# If using Cloudflare or similar, purge cache # Check cache-control headers are correct
“Connection Refused” Errors
Symptoms: Can’t connect to website at all
Diagnosis:
# Check if nginx is running
sudo systemctl status nginx
# Check if ports are listening
sudo ss -tulpn | grep nginx
# Check firewall
sudo iptables -L -n -v | grep -E '80|443'
# Test locally
curl -I http://localhost
# Test from external machine
curl -I http://YOUR_SERVER_IPSolutions:
Nginx Not Running
sudo systemctl start nginx sudo systemctl enable nginxFirewall Blocking
# Verify ports are open sudo iptables -L -n -v # Configuration should have: # networking.firewall.allowedTCPPorts = [ 80 443 ]; # Rebuild if changed make switchExternal Firewall (cloud provider, router)
# Check with your hosting provider # Ensure security groups allow ports 80/443
Mixed Content Warnings
Symptoms: HTTPS page loading HTTP resources
Diagnosis:
# Check browser console for mixed content warnings
# Review HTML for http:// URLs in:
# - <img src="http://...">
# - <script src="http://...">
# - <link href="http://...">Solutions:
# Use relative URLs: src="/images/logo.png"
# Or use protocol-relative: src="//example.com/image.png"
# Or use HTTPS: src="https://example.com/image.png"
# CSP will also block mixed content if configured correctlyTroubleshooting Checklist
When things go wrong, run through this checklist:
Monitoring
Check Nginx Status
# Service status
systemctl status nginx
# Test config
sudo nginx -t
# Check listening ports
ss -tulpn | grep nginxMonitor Access
# Real-time access log
tail -f /var/log/nginx/wolfhard.net.access.log
# Count requests
wc -l /var/log/nginx/wolfhard.net.access.log
# Top IPs
awk '{print $1}' /var/log/nginx/wolfhard.net.access.log | sort | uniq -c | sort -rn | headBackup
Website content is stored in git:
cd ~/dotfiles
git add web/
git commit -m "Update website content"
git pushLogs are in /var/log/nginx/ (consider log rotation).