Configuring Apache2 ProxyPass on Ubuntu: mod_proxy, mod_proxy_html & Gateway Routing Runbook

In October 2012, while building out infrastructure for ContactCampaigns.com on Ubuntu Linux, I experimented with Apache HTTP Server reverse proxies using ProxyPass and mod_proxy_html. The goal was to preserve Apache's ownership of port 80 while transparently routing subpaths to internal backend services. In this production runbook, we revisit those foundational steps, modernize them for current Ubuntu LTS distributions, examine HTML URL rewriting, and implement connection-pooled gateway reverse proxies.

The Historical Development Context: Port Dominance & Path Gateways

My original 2012 development notes captured the raw trial-and-error of configuring Apache on Ubuntu:

"I did some crazy stuff with ProxyPass in my development environment... So far this the best I can do when it comes to preserving apache's dominance of the port. But I haven't been able to get some naming out of the URL, because ProxyPass actually wants /something as the 'gateway' to the destination. Anyway, I am trying to get contact campaigns up online."

The core architectural puzzle was twofold:

  • Port Dominance: Allowing Apache to bind to privileged TCP port 80/443 so multiple microservices, Node.js applications, and Java servlets could co-exist on the same host behind a single entry point.
  • Path Gateway Mapping: Routing incoming requests starting with a URL prefix (e.g. /api or /campaigns/) to an upstream backend while ensuring HTML links, CSS assets, and HTTP redirects (Location headers) do not break when returning to client browsers.

Modern Ubuntu Package Installation & Module Activation

In modern Ubuntu LTS releases (20.04, 22.04, 24.04), manually loading module .so files via LoadModule inside apache2.conf is considered an operational anti-pattern. Instead, administrators utilize Ubuntu's native a2enmod (Apache 2 Enable Module) management utilities:

# 1. Update Package Index and Install Apache2 + XML Filter Libraries
sudo apt-get update
sudo apt-get install -y apache2 libapache2-mod-proxy-html libxml2-dev

# 2. Enable Core Proxy, HTTP Protocol, and Header Manipulation Modules
sudo a2enmod proxy
sudo a2enmod proxy_http
sudo a2enmod proxy_html
sudo a2enmod headers
sudo a2enmod deflate
sudo a2enmod xml2enc

# 3. Verify Configuration Syntax and Gracefully Reload Apache
sudo apache2ctl configtest
sudo systemctl reload apache2

Architectural Diagram: Reverse Proxy Gateway Routing

Apache2 Gateway Reverse Proxy Architecture Client Browser Port 80 / 443 Apache2 Front-End ProxyPass /campaigns/ ProxyHTMLURLMap Rewrite Upstream Node.js API 127.0.0.1:3000 Upstream Backend 127.0.0.1:8080 Apache terminates TLS, proxies traffic by path prefix, and rewrites response HTML/headers

Solving the URL Gateway Problem: ProxyPass, ProxyPassReverse & Trailing Slashes

The exact stumbling block I encountered in 2012—"proxy pass actually wants /something as the gateway to the destination"—is caused by mismatched URI paths between the public reverse proxy and the private upstream service. To resolve this cleanly without breaking redirects, three directives must be configured in lockstep:

<VirtualHost *:80>
    ServerName contactcampaigns.local
    ServerAdmin webmaster@contactcampaigns.local

    # Forwarding and Protocol Invariants
    ProxyRequests Off
    ProxyPreserveHost On

    # 1. Trailing Slash Enforcement
    # Always match trailing slashes on both source and target paths!
    ProxyPass /campaigns/ http://127.0.0.1:8080/campaigns/ connectiontimeout=5 timeout=60 retry=10
    
    # 2. Redirect Header Rewriting (Crucial!)
    # Intercepts HTTP 301/302 'Location:' headers from the upstream backend and
    # rewrites them to match the public URL seen by the user's browser.
    ProxyPassReverse /campaigns/ http://127.0.0.1:8080/campaigns/

    # 3. HTML Content URL Rewriting with mod_proxy_html
    # Parses outbound HTML documents and rewrites hardcoded internal links
    ProxyHTMLEnable On
    ProxyHTMLLinks a href
    ProxyHTMLLinks form action
    ProxyHTMLLinks img src
    ProxyHTMLLinks script src
    ProxyHTMLLinks link href
    ProxyHTMLURLMap http://127.0.0.1:8080/ /campaigns/

    # Forward Client Telemetry Headers
    RequestHeader set X-Forwarded-Proto "http"
    RequestHeader set X-Real-IP "%{REMOTE_ADDR}s"

    ErrorLog ${APACHE_LOG_DIR}/campaigns_error.log
    CustomLog ${APACHE_LOG_DIR}/campaigns_access.log combined
</VirtualHost>

Why ProxyPassReverse Is Mandatory

A common operational failure occurs when an administrator configures ProxyPass but omits ProxyPassReverse. Consider what happens when an upstream backend responds with an HTTP redirect:

  • The client browser requests: http://contactcampaigns.local/campaigns/login.
  • The upstream backend validates credentials and issues an HTTP redirect: Location: http://127.0.0.1:8080/campaigns/dashboard.
  • Without ProxyPassReverse: The client receives the raw internal IP header. The user's browser attempts to connect directly to 127.0.0.1:8080 (their own local computer!), failing with a connection refused error.
  • With ProxyPassReverse: Apache inspects the outgoing Location: header, replaces http://127.0.0.1:8080/campaigns/ with http://contactcampaigns.local/campaigns/, and the user transitions smoothly.

WebSocket Proxying: Upgrading mod_proxy for Real-Time Apps

Modern applications frequently establish bi-directional WebSocket connections. Standard mod_proxy_http does not handle HTTP 101 Switching Protocols. Enabling mod_proxy_wstunnel provides seamless real-time WebSocket pass-through:

# Enable WebSocket Tunneling Module
sudo a2enmod proxy_wstunnel
# Inside your VirtualHost block:
RewriteEngine On
RewriteCond %{HTTP:Upgrade} =websocket [NC]
RewriteRule ^/ws/(.*) ws://127.0.0.1:8080/ws/$1 [P,L]

ProxyPass /ws/ ws://127.0.0.1:8080/ws/
ProxyPassReverse /ws/ ws://127.0.0.1:8080/ws/

Production Buffer Tuning, KeepAlive & SSL Offloading

Under heavy concurrent workloads, default Apache proxy buffer sizes can introduce I/O bottlenecks or cause intermittent 502 Bad Gateway errors when upstream applications emit large HTTP response headers (such as extensive JWT auth cookies or detailed CORS declarations). Production deployments should explicitly configure proxy buffer boundaries:

# Tuning Proxy Buffers and Connection Re-use inside VirtualHost
<Proxy "*">
    # Buffer up to 16KB of response headers from backend
    ProxyIOBufferSize 16384
    # Max memory buffer for reading response bodies before disk spooling
    ProxyReceiveBufferSize 32768
</Proxy>

# Enable HTTP Persistent Connections between Apache and backend
SetEnv force-proxy-request-1.0 1
SetEnv proxy-nokeepalive 0

SSL/TLS Offloading Invariants: When terminating SSL at the Apache edge, downstream backends must be informed of the client's original protocol scheme. If the backend is unaware of HTTPS termination, it may inadvertently generate plaintext http:// links in pagination or canonical tags. Always forward standard proxy headers:

RequestHeader set X-Forwarded-Proto "https"
RequestHeader set X-Forwarded-Port "443"
RequestHeader set X-Forwarded-Host "%{HTTP_HOST}s"

Summary & Best Practice Checklist

  • Never edit apache2.conf for modules: Use a2enmod and a2dismod to maintain clean OS package upgrade paths.
  • Always pair ProxyPass with ProxyPassReverse: Prevent internal hostname leaks in HTTP 301/302 redirects.
  • Match trailing slashes consistently: If the source URL ends with /, the target upstream URL must also end with /.
  • Set connection timeouts: Never allow un-timed proxy connections to exhaust Apache worker threads. Use connectiontimeout=5 timeout=60.