DevOpsil
Nginx
87%
Needs Review

Apache mod_proxy: Reverse Proxy, Load Balancing, and WebSocket Support

Muhammad HassanMuhammad Hassan4 min read

Apache as a Reverse Proxy

Apache httpd isn't just a file server — with mod_proxy and its companions, it's a capable reverse proxy. You can use it to proxy requests to Node.js, PHP-FPM, Tomcat, or any backend service, with built-in load balancing, session stickiness, and health checking.


Enabling Required Modules

# Ubuntu/Debian
sudo a2enmod proxy proxy_http proxy_balancer lbmethod_byrequests headers
sudo systemctl reload apache2

# RHEL/Rocky — modules are loaded in /etc/httpd/conf.modules.d/00-proxy.conf
# Verify they're uncommented:
grep -v "^#" /etc/httpd/conf.modules.d/00-proxy.conf | grep proxy

# For WebSockets
sudo a2enmod proxy_wstunnel   # Ubuntu
# Add: LoadModule proxy_wstunnel_module modules/mod_proxy_wstunnel.so  # RHEL

Basic Reverse Proxy

Proxy all traffic to a backend application (e.g., Node.js on port 3000):

<VirtualHost *:80>
    ServerName app.example.com

    ProxyPreserveHost On
    ProxyPass        / http://127.0.0.1:3000/
    ProxyPassReverse / http://127.0.0.1:3000/

    # Pass real client IP to backend
    RequestHeader set X-Forwarded-For %{REMOTE_ADDR}s
    RequestHeader set X-Forwarded-Proto %{REQUEST_SCHEME}s
    RequestHeader set X-Real-IP %{REMOTE_ADDR}s

    ErrorLog ${APACHE_LOG_DIR}/app-error.log
    CustomLog ${APACHE_LOG_DIR}/app-access.log combined
</VirtualHost>

Proxying a Subpath

Proxy only /api/ to a backend, serve everything else from the filesystem:

<VirtualHost *:443>
    ServerName example.com
    DocumentRoot /var/www/html

    # Proxy /api/ to backend
    ProxyPass        /api/ http://127.0.0.1:8080/api/
    ProxyPassReverse /api/ http://127.0.0.1:8080/api/

    # Serve static files directly
    <Directory /var/www/html>
        Options -Indexes
        AllowOverride None
        Require all granted
    </Directory>
</VirtualHost>

Load Balancing

Distribute traffic across multiple backends using mod_proxy_balancer:

<VirtualHost *:80>
    ServerName app.example.com

    <Proxy balancer://appcluster>
        BalancerMember http://10.0.1.10:3000 loadfactor=1
        BalancerMember http://10.0.1.11:3000 loadfactor=1
        BalancerMember http://10.0.1.12:3000 loadfactor=2  # gets 2x traffic

        # Load balancing method
        ProxySet lbmethod=byrequests

        # Timeout and retry settings
        ProxySet timeout=30
        ProxySet retry=1
    </Proxy>

    ProxyPreserveHost On
    ProxyPass        / balancer://appcluster/
    ProxyPassReverse / balancer://appcluster/
</VirtualHost>

Load Balancing Methods

MethodModuleBehavior
byrequestslbmethod_byrequestsRound-robin by request count
bytrafficlbmethod_bytrafficRound-robin by bytes transferred
bybusynesslbmethod_bybusynessRoute to least-busy worker
heartbeatlbmethod_heartbeatUse heartbeat monitor data
# Enable multiple methods (Ubuntu)
sudo a2enmod lbmethod_byrequests lbmethod_bytraffic lbmethod_bybusyness

Session Stickiness

Route the same user to the same backend:

<Proxy balancer://appcluster>
    BalancerMember http://10.0.1.10:3000 route=node1
    BalancerMember http://10.0.1.11:3000 route=node2
    ProxySet lbmethod=byrequests
    ProxySet stickysession=ROUTEID
</Proxy>

ProxyPass        / balancer://appcluster/ stickysession=ROUTEID
ProxyPassReverse / balancer://appcluster/

# Inject route cookie if not set
Header add Set-Cookie "ROUTEID=.%{BALANCER_WORKER_ROUTE}e; path=/" env=BALANCER_ROUTE_CHANGED

Hot Spare (Failover)

<Proxy balancer://appcluster>
    BalancerMember http://10.0.1.10:3000
    BalancerMember http://10.0.1.11:3000
    BalancerMember http://10.0.1.12:3000 status=+H  # hot standby
</Proxy>

The hot spare (+H) only receives traffic when all primary members are unavailable.


Balancer Manager

Apache provides a web UI for managing the balancer at runtime:

<Location /balancer-manager>
    SetHandler balancer-manager

    # Restrict to trusted IPs
    Require ip 10.0.0.0/8
    Require ip 127.0.0.1
</Location>

Access at http://your-server/balancer-manager to enable/disable backends, adjust load factors, and drain nodes without restarting Apache.


WebSocket Proxying

WebSocket connections require mod_proxy_wstunnel:

<VirtualHost *:443>
    ServerName ws.example.com

    # HTTP upgrade to WebSocket
    RewriteEngine On
    RewriteCond %{HTTP:Upgrade} websocket [NC]
    RewriteCond %{HTTP:Connection} upgrade [NC]
    RewriteRule ^/ws/(.*)$ ws://127.0.0.1:3000/ws/$1 [P,L]

    # Regular HTTP fallback
    ProxyPass        / http://127.0.0.1:3000/
    ProxyPassReverse / http://127.0.0.1:3000/

    ProxyPreserveHost On
</VirtualHost>

Proxying to PHP-FPM

For PHP applications with FastCGI:

# Enable fcgi module
sudo a2enmod proxy_fcgi setenvif
<VirtualHost *:443>
    ServerName php-app.example.com
    DocumentRoot /var/www/php-app

    <FilesMatch "\.php$">
        SetHandler "proxy:unix:/run/php/php8.3-fpm.sock|fcgi://localhost/"
    </FilesMatch>

    <Directory /var/www/php-app>
        Options -Indexes
        AllowOverride All
        Require all granted
    </Directory>
</VirtualHost>

Proxying to Tomcat (AJP)

sudo a2enmod proxy_ajp
<VirtualHost *:80>
    ServerName java-app.example.com

    ProxyPass        / ajp://127.0.0.1:8009/
    ProxyPassReverse / ajp://127.0.0.1:8009/
</VirtualHost>

Health Checks

Enable the heartbeat monitor for active health checking:

<Proxy balancer://appcluster>
    BalancerMember http://10.0.1.10:3000 hcmethod=GET hcuri=/health hcinterval=10 hcpasses=2 hcfails=3
    BalancerMember http://10.0.1.11:3000 hcmethod=GET hcuri=/health hcinterval=10 hcpasses=2 hcfails=3
</Proxy>
  • hcmethod=GET — use HTTP GET to check health
  • hcuri=/health — the health check endpoint
  • hcinterval=10 — check every 10 seconds
  • hcpasses=2 — require 2 consecutive passes to mark healthy
  • hcfails=3 — require 3 consecutive failures to mark unhealthy

Timeouts and Error Handling

# Global proxy timeout settings
ProxyTimeout 120

# Per-balancer
<Proxy balancer://appcluster>
    ProxySet timeout=30
    ProxySet connectiontimeout=5
    ProxySet retry=1
</Proxy>

# Custom error page when all backends are down
ErrorDocument 503 /maintenance.html

Useful Debug Headers

Add response headers to identify which backend served the request:

Header add X-Backend-Server %{BALANCER_WORKER_NAME}e
Header add X-Balance-Method %{BALANCER_ALGORITHM}e

These headers help trace which node handled a request during debugging — remove in production or restrict to internal IPs.

# Quick test
curl -I https://app.example.com/
# Look for X-Backend-Server header
Share:

Was this article helpful?

Muhammad Hassan
Muhammad Hassan

Network & Traffic Engineer

Packets don't lie. I design and troubleshoot the network layer that everything else depends on — Nginx, Envoy, HAProxy, DNS, CDNs, and everything in between. If it touches a socket, it's my problem.

Related Articles

More in Nginx

View all →

Discussion