The stack uses nginx as a reverse proxy to handle SSL/TLS termination, route traffic, and provide security features like rate limiting and security headers.
Configuration Files
Two nginx configurations are provided:
nginx.dev.conf Development configuration with HTTP on port 8080
nginx.conf Production configuration with HTTPS, rate limiting, and security headers
Development Configuration
The development configuration (nginx.dev.conf) is optimized for local testing:
HTTP only (no SSL/TLS)
Listens on port 8080
Simplified logging
No rate limiting
WebSocket support enabled
Usage
Create a development override:
cp docker-compose.override.example.yml docker-compose.override.yml
docker compose up -d
Access points:
Headscale: http://localhost:8000
Headplane UI: http://localhost:8000/admin
Health check: http://localhost:8000/health
Metrics: http://localhost:8000/metrics
Production Configuration
The production configuration (nginx.conf) includes enterprise-grade security:
HTTPS with TLS 1.2/1.3
HTTP to HTTPS redirect
Rate limiting (3-tier strategy)
Security headers (HSTS, CSP, etc.)
OCSP stapling
HTTP/2 support
DDoS protection
Worker Process Settings
Number of worker processes. Set to auto to use number of CPU cores.
Maximum number of open file descriptors per worker (production only). worker_rlimit_nofile 65535 ;
Maximum number of simultaneous connections per worker. Development : 2048Production : 4096events {
worker_connections 4096 ;
use epoll ;
multi_accept on ;
}
Upstream Configuration
Headscale Backend
upstream headscale_backend {
server headscale:8080 max_fails = 3 fail_timeout=30s;
keepalive 32 ;
keepalive_requests 100 ;
keepalive_timeout 60s ;
}
Number of failed attempts before marking backend as down.
Time to wait before retrying a failed backend.
Number of idle keepalive connections to maintain for reuse.
Headplane Backend
upstream headplane_backend {
server headplane:3000 max_fails = 3 fail_timeout=30s;
keepalive 16 ;
}
Proxy Buffering
Enable buffering of responses from proxied servers. proxy_buffering on ;
proxy_buffer_size 8k ;
proxy_buffers 16 8k ;
proxy_busy_buffers_size 16k ;
Buffering is enabled for Headscale traffic but disabled for Headplane UI for better interactivity.
Proxy Timeouts
proxy_connect_timeout 90s ;
proxy_send_timeout 90s ;
proxy_read_timeout 90s ;
SSL/TLS Configuration (Production)
Protocols and Ciphers
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305' ;
ssl_prefer_server_ciphers off ;
Only TLS 1.2 and 1.3 are enabled. TLS 1.0 and 1.1 are deprecated and insecure.
Session Cache
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m ;
ssl_session_tickets off ;
OCSP Stapling
ssl_stapling on ;
ssl_stapling_verify on ;
resolver 1.1.1.1 1.0.0.1 valid=300s;
resolver_timeout 5s ;
Certificate Paths
ssl_certificate /etc/letsencrypt/live/${ DOMAIN }/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/${ DOMAIN }/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/${ DOMAIN }/chain.pem;
Rate Limiting (Production)
Rate Limit Zones
# General traffic: 10 requests/second
limit_req_zone $ binary_remote_addr zone=general:10m rate=10r/s;
# API traffic: 30 requests/second
limit_req_zone $ binary_remote_addr zone=api:10m rate=30r/s;
# Health checks: 100 requests/second
limit_req_zone $ binary_remote_addr zone=health:10m rate=100r/s;
Connection Limits
# Limit connections per IP
limit_conn_zone $ binary_remote_addr zone=addr:10m;
server {
limit_conn addr 10 ;
}
Applying Rate Limits
location / {
limit_req zone=general burst=20 nodelay;
# ...
}
location /api/ {
limit_req zone=api burst=50 nodelay;
# ...
}
location = /health {
limit_req zone=health burst=20 nodelay;
# ...
}
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
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 "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
Security Header Explanations
WebSocket Support
WebSocket support is critical for the Tailscale protocol to function properly.
# WebSocket upgrade mapping
map $ http_upgrade $ connection_upgrade {
default upgrade;
'' close;
}
location / {
proxy_set_header Upgrade $ http_upgrade ;
proxy_set_header Connection $ connection_upgrade ;
# ...
}
Location Blocks
Health Check Endpoint
location = /health {
limit_req zone=health burst=20 nodelay;
proxy_pass http://headscale_backend/health;
proxy_http_version 1.1 ;
proxy_set_header Connection "" ;
proxy_set_header Host $ host ;
# Fast fail for health checks
proxy_connect_timeout 5s ;
proxy_read_timeout 5s ;
access_log off ;
}
Metrics Endpoint
location = /metrics {
# Restrict access to internal networks
# allow 127.0.0.1;
# allow 10.0.0.0/8;
# deny all;
limit_req zone=health burst=10 nodelay;
proxy_pass http://headscale_backend/metrics;
proxy_http_version 1.1 ;
proxy_set_header Connection "" ;
proxy_set_header Host $ host ;
add_header Cache-Control "no-cache, no-store, must-revalidate" ;
}
Uncomment the IP restrictions in production to prevent unauthorized metric access.
Headplane Admin Interface
location /admin {
limit_req zone=general burst=20 nodelay;
proxy_pass http://headplane_backend;
proxy_http_version 1.1 ;
# Standard proxy headers
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 ;
# WebSocket support
proxy_set_header Upgrade $ http_upgrade ;
proxy_set_header Connection $ connection_upgrade ;
# Disable buffering for interactive UI
proxy_buffering off ;
}
API Endpoints
location /api/ {
limit_req zone=api burst=50 nodelay;
proxy_pass http://headscale_backend;
proxy_http_version 1.1 ;
# Essential proxy headers
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 ;
# WebSocket support
proxy_set_header Upgrade $ http_upgrade ;
proxy_set_header Connection $ connection_upgrade ;
# Error handling
proxy_next_upstream error timeout invalid_header http_502 http_503 http_504;
proxy_next_upstream_tries 2 ;
proxy_next_upstream_timeout 30s ;
# Request ID for tracing
proxy_set_header X-Request-ID $ request_id ;
}
Main Headscale Proxy
location / {
limit_req zone=general burst=20 nodelay;
proxy_pass http://headscale_backend;
proxy_http_version 1.1 ;
# Essential proxy headers
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 ;
# WebSocket support (critical for Tailscale)
proxy_set_header Upgrade $ http_upgrade ;
proxy_set_header Connection $ connection_upgrade ;
# Enable buffering
proxy_buffering on ;
# Error handling
proxy_next_upstream error timeout invalid_header http_502 http_503 http_504;
proxy_next_upstream_tries 2 ;
proxy_next_upstream_timeout 30s ;
# Request ID for tracing
proxy_set_header X-Request-ID $ request_id ;
}
Compression
gzip on ;
gzip_vary on ;
gzip_proxied any;
gzip_comp_level 6 ;
gzip_min_length 1000 ;
gzip_disable "msie6" ;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml+rss;
Error Pages
error_page 502 503 504 /50x.html;
location = /50x.html {
internal ;
add_header Content-Type text/plain always;
return 503 "Service Temporarily Unavailable \n Please try again in a few moments. \n " ;
}
error_page 429 /429.html;
location = /429.html {
internal ;
add_header Content-Type text/plain always;
return 429 "Too Many Requests \n Please slow down and try again. \n " ;
}
Testing Configuration
Verify nginx configuration syntax:
docker exec nginx nginx -t
Reload configuration without downtime:
docker exec nginx nginx -s reload
For high-traffic deployments, consider:
worker_connections 8192 ;
worker_rlimit_nofile 100000 ;
keepalive_timeout 120 ;
keepalive_requests 1000 ;
proxy_buffers 32 8k ;
Monitoring
View real-time logs:
# All logs
docker compose logs -f nginx
# Access logs only
docker exec nginx tail -f /var/log/nginx/access.log
# Error logs only
docker exec nginx tail -f /var/log/nginx/error.log
Additional Resources
nginx script reference Management script for nginx operations
Production deployment Deploy nginx with SSL/TLS in production