Deploying Behind a Reverse Proxy
Configuration guide for deploying Multi Host behind CDNs, load balancers, and reverse proxies including header handling and SSL termination.
Reverse proxies, CDNs, and load balancers sit between users and your application, providing caching, SSL termination, and traffic distribution. This guide covers configuring Multi Host to work correctly in these environments.
Understanding Reverse Proxy Architecture
In a typical proxied deployment:
User → CDN/Proxy → Origin Server (Multi Host)
The proxy receives user requests, may serve cached responses, and forwards other requests to your origin. This affects:
- Client IP detection: Origin sees proxy IP, not user IP
- Protocol detection: Origin may receive HTTP even when user connected via HTTPS
- Host detection: Request may arrive with different Host header
- Caching: Static content serves from proxy edge
Proper configuration ensures the application behaves correctly despite this intermediary layer.
Trusting Proxy Headers
Proxies communicate original request details through headers. Configure Multi Host to trust these headers from your proxy:
$config['trust_proxy'] = true;
$config['trusted_proxies'] = [
'10.0.0.0/8', // Internal network
'172.16.0.0/12', // Internal network
'192.168.0.0/16', // Internal network
'173.245.48.0/20', // Cloudflare
'103.21.244.0/22', // Cloudflare
// Add your CDN's IP ranges
];
Security warning: Never trust proxy headers from untrusted sources. An attacker could send fake headers to spoof their IP or protocol.
Important Headers
X-Forwarded-For: Chain of IP addresses from user through proxies
X-Forwarded-For: 203.0.113.50, 198.51.100.178
The leftmost IP is typically the original client (but can be spoofed if earlier proxies aren't trusted).
X-Forwarded-Proto: Original protocol (http or https)
X-Forwarded-Proto: https
X-Forwarded-Host: Original Host header
X-Forwarded-Host: example.com
X-Real-IP: Some proxies send a single client IP header
X-Real-IP: 203.0.113.50
SSL/TLS Termination
When the proxy handles SSL, traffic between proxy and origin may be plain HTTP:
User → HTTPS → Proxy → HTTP → Origin
Configure the application to recognise the connection was secure:
$config['force_https'] = true;
$config['trust_proto_header'] = true;
This ensures:
- Generated URLs use HTTPS
- Secure cookie flags work correctly
- HTTPS enforcement doesn't create redirect loops
Configuring Nginx as Origin
server {
listen 80;
server_name example.com;
# Trust proxy protocol header
set_real_ip_from 10.0.0.0/8;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
location / {
# Pass headers to PHP
fastcgi_param HTTPS on;
fastcgi_param HTTP_X_FORWARDED_PROTO $http_x_forwarded_proto;
# ... other fastcgi config
}
}
Configuring Apache as Origin
<VirtualHost *:80>
ServerName example.com
# Reconstruct HTTPS variable
SetEnvIf X-Forwarded-Proto https HTTPS=on
# Trust proxy for IP
RemoteIPHeader X-Forwarded-For
RemoteIPTrustedProxy 10.0.0.0/8
</VirtualHost>
CDN Configuration
Cloudflare
Cloudflare provides CDN, DDoS protection, and edge features:
// Cloudflare IP ranges - update periodically from their published list
$config['trusted_proxies'] = [
'173.245.48.0/20',
'103.21.244.0/22',
'103.22.200.0/22',
// ... complete list from cloudflare.com/ips/
];
Page Rules for caching:
# Cache static assets
*.example.com/images/* - Cache Level: Standard
*.example.com/static/* - Cache Level: Standard
# Bypass cache for dynamic content
*.example.com/api/* - Cache Level: Bypass
*.example.com/upload/* - Cache Level: Bypass
Cache headers from origin:
// Static assets
header('Cache-Control: public, max-age=31536000, immutable');
// Dynamic content
header('Cache-Control: private, no-cache');
AWS CloudFront
CloudFront configuration for image hosting:
Origin settings:
- Protocol: HTTPS only (or match viewer)
- HTTP port: 80
- HTTPS port: 443
Cache behaviour:
- Forward headers: Host, Accept (for content negotiation)
- Query strings: Forward all (if used for cache busting)
- TTL: 86400 default, respect origin headers
Custom headers to origin:
X-Forwarded-Proto: ${CloudFront-Forwarded-Proto}
Load Balancer Configuration
Multiple origin servers require session handling consideration:
Sticky Sessions
Route user sessions to the same backend:
# HAProxy example
backend app_servers
cookie SERVERID insert indirect nocache
server app1 10.0.0.1:80 check cookie app1
server app2 10.0.0.2:80 check cookie app2
Sticky sessions simplify session handling but reduce load distribution effectiveness.
Shared Session Storage
Better approach: store sessions in shared storage accessible from all backends:
$config['session_driver'] = 'redis';
$config['session_redis_host'] = '10.0.0.100';
$config['session_redis_port'] = 6379;
This enables true round-robin load balancing without session affinity.
Health Checks
Configure health check endpoints:
// /health endpoint
if ($request->path() === '/health') {
// Check critical dependencies
$db_ok = $this->checkDatabase();
$storage_ok = $this->checkStorage();
if ($db_ok && $storage_ok) {
http_response_code(200);
echo 'OK';
} else {
http_response_code(503);
echo 'Service Unavailable';
}
exit;
}
Load balancer configuration:
# HAProxy
backend app_servers
option httpchk GET /health
http-check expect status 200
server app1 10.0.0.1:80 check
server app2 10.0.0.2:80 check
Caching Strategies
Static Assets
Images, CSS, JavaScript with content-based URLs:
Cache-Control: public, max-age=31536000, immutable
Use fingerprinted URLs so content changes create new URLs:
/static/app.abc123.js
/images/photo.jpg?v=1609459200
Dynamic Content
API responses and personalised pages:
Cache-Control: private, no-cache, no-store
Or for cacheable but personalised content:
Cache-Control: private, max-age=60
Vary: Cookie
Conditional Caching
Use ETags for validation:
$etag = md5($content);
header("ETag: \"{$etag}\"");
if (isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
if (trim($_SERVER['HTTP_IF_NONE_MATCH'], '"') === $etag) {
http_response_code(304);
exit;
}
}
Handling WebSocket Connections
If using WebSockets for real-time features:
Nginx proxy configuration:
location /ws {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 3600s;
}
Cloudflare: Enable WebSocket support in dashboard settings.
Troubleshooting
Redirect Loops
Symptom: Infinite redirects, especially HTTP→HTTPS
Cause: Application doesn't recognise HTTPS from proxy
Solution: Configure trusted proxies and protocol header trust
Wrong Client IP
Symptom: All requests appear from same IP, rate limiting affects everyone
Cause: Application using proxy IP instead of forwarded IP
Solution: Configure X-Forwarded-For header trust with correct proxy ranges
Cache Serving Stale Content
Symptom: Updates not visible, old content served
Cause: Aggressive caching without proper cache control
Solution: Review cache headers, implement cache purging, use versioned URLs
Mixed Content Warnings
Symptom: Browser warns about insecure content on HTTPS pages
Cause: Application generating HTTP URLs despite HTTPS connection
Solution: Configure protocol detection and force HTTPS URL generation
Frequently Asked Questions
CDN providers publish their IP ranges. Cloudflare: cloudflare.com/ips. CloudFront: aws.amazon.com/ip-ranges (filter for CloudFront). Update these periodically as providers add new ranges.