If Apache already serves your websites, you don’t need a second web server to publish a Node.js, Python or Dockerised app on its own domain or path. mod_proxy turns Apache into a capable reverse proxy, with HTTPS, WebSocket support and load balancing.
1. Enable the modules
sudo a2enmod proxy proxy_http proxy_wstunnel headers ssl rewrite
sudo systemctl restart apache2
proxy,proxy_http— HTTP proxying.proxy_wstunnel— WebSockets.headers— set forwarded headers.
2. A virtual host for your app
Assume the app listens on 127.0.0.1:3000:
<VirtualHost *:80>
ServerName app.example.com
ProxyPreserveHost On
ProxyRequests Off
ProxyPass / http://127.0.0.1:3000/
ProxyPassReverse / http://127.0.0.1:3000/
RequestHeader set X-Forwarded-Proto "http"
</VirtualHost>
ProxyPreserveHost On— pass the originalHostheader (most apps need it to build URLs).ProxyRequests Off— essential: forward proxying off, so your server isn’t an open proxy.ProxyPassReverse— rewritesLocationheaders in redirects from the app.- Apache adds
X-Forwarded-For,X-Forwarded-HostandX-Forwarded-Serverautomatically.
sudo a2ensite app.example.com.conf
sudo apache2ctl configtest && sudo systemctl reload apache2
3. HTTPS
sudo certbot --apache -d app.example.com
Certbot creates the SSL vhost. Make sure it contains your proxy lines and tells the app the original scheme:
RequestHeader set X-Forwarded-Proto "https"
Without it, frameworks may generate http:// links or loop redirecting to HTTPS. Configure the app to trust proxy headers (Express app.set('trust proxy', 1), Django SECURE_PROXY_SSL_HEADER, etc.).
4. WebSockets
Since Apache 2.4.47, mod_proxy_http can upgrade connections itself:
ProxyPass / http://127.0.0.1:3000/ upgrade=websocket
ProxyPassReverse / http://127.0.0.1:3000/
On older versions, route the WebSocket path through ws:// with mod_proxy_wstunnel:
RewriteEngine On
RewriteCond %{HTTP:Upgrade} =websocket [NC]
RewriteRule ^/(.*) ws://127.0.0.1:3000/$1 [P,L]
ProxyPass / http://127.0.0.1:3000/
ProxyPassReverse / http://127.0.0.1:3000/
Long-lived sockets may need a higher ProxyTimeout (default 60 s via Timeout).
5. Path-based routing
Serve a PHP site normally and send /api/ to an app:
<VirtualHost *:443>
ServerName example.com
DocumentRoot /var/www/example.com
ProxyPass /api/ http://127.0.0.1:8000/
ProxyPassReverse /api/ http://127.0.0.1:8000/
</VirtualHost>
Order matters: more specific ProxyPass lines must come before general ones. Exclude a path from proxying with ProxyPass /static/ !.
6. Load balancing
sudo a2enmod proxy_balancer lbmethod_byrequests
<Proxy "balancer://appcluster">
BalancerMember http://10.0.0.11:3000
BalancerMember http://10.0.0.12:3000
ProxySet lbmethod=byrequests
</Proxy>
ProxyPass / balancer://appcluster/
ProxyPassReverse / balancer://appcluster/
Methods: byrequests, bytraffic, bybusyness (lbmethod_bybusyness), heartbeat. Add stickysession=ROUTEID for session affinity. For advanced health checks, mod_proxy_hcheck is available.
7. Timeouts and uploads
ProxyTimeout 300
LimitRequestBody 104857600 # 100 MB
Common errors
- 503 Service Unavailable — the backend isn’t running, or (on RHEL family) SELinux blocks Apache from connecting:
sudo setsebool -P httpd_can_network_connect 1. - “No protocol handler was valid” —
proxy_http(orproxy_wstunnel) not enabled. - Redirects to
localhost:3000— missingProxyPassReverseorProxyPreserveHost. - Mixed content / http links — missing
X-Forwarded-Proto httpsor the app doesn’t trust it.
Apache, Nginx or Caddy as proxy?
All three proxy well. If Apache already runs your sites, mod_proxy avoids a second web server. For a dedicated proxy in front of many apps, Nginx or Caddy are more common choices. See also reverse proxy on Linux.