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 original Host header (most apps need it to build URLs).
  • ProxyRequests Off — essential: forward proxying off, so your server isn’t an open proxy.
  • ProxyPassReverse — rewrites Location headers in redirects from the app.
  • Apache adds X-Forwarded-For, X-Forwarded-Host and X-Forwarded-Server automatically.
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 (or proxy_wstunnel) not enabled.
  • Redirects to localhost:3000 — missing ProxyPassReverse or ProxyPreserveHost.
  • Mixed content / http links — missing X-Forwarded-Proto https or 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.

  • Apache
  • Reverse Proxy
  • mod_proxy