Deploying Behind a Reverse Proxy

Configuration guide for deploying Multi Host behind CDNs, load balancers, and reverse proxies including header handling and SSL termination.

Updated September 2025

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.