Putting Apache in Front of Your App: A Practical Guide to mod_proxy Reverse Proxying
How to put Apache 2.4 in front of a backend app server with ProxyPass and ProxyPassReverse — including trailing-slash pitfalls, timeout tuning, forwarded headers, and the limits of mod_proxy_balancer.
22 Mar 2026, 22:45 UTC

You have an application server listening on port 8080 — maybe a Node service, a Java app, or PHP-FPM — and Apache 2.4 is already terminating TLS on port 443. The question is how to get requests from Apache to the backend cleanly. The short answer: use mod_proxy with ProxyPass and ProxyPassReverse, not a pile of rewrite rules. It is explicit, easier to debug, and handles the redirect-rewriting problem for you.
Why ProxyPass beats mod_rewrite for this job
Apache can forward requests with mod_rewrite and the [P] flag, and it works. But rewrite rules are evaluated in order, interact with every other rule in the file, and hide the fact that proxying is happening. A ProxyPass directive inside a <Location> block states the intent directly: everything under this path goes to that backend. When a colleague (or future you) opens the config at 2 a.m., that clarity matters. Reserve mod_rewrite for URL manipulation that is genuinely about rewriting, not routing.
A minimal, correct configuration
Enable mod_proxy and mod_proxy_http (on Debian/Ubuntu: a2enmod proxy proxy_http, then restart Apache; on RHEL-family systems the modules usually ship enabled). Then, in your virtual host:
<VirtualHost *:443>
ServerName example.com
# ... TLS config ...
ProxyPreserveHost On
ProxyPass /app/ http://127.0.0.1:8080/app/ retry=0 timeout=30
ProxyPassReverse /app/ http://127.0.0.1:8080/app/
</VirtualHost>
Three details carry most of the weight here:
- Trailing slashes must match.
/app/on the left,/app/on the right. If one side has a slash and the other does not, paths get concatenated wrong and you will chase phantom 404s on the backend. - ProxyPassReverse fixes redirects. When the backend responds with a
Location: http://127.0.0.1:8080/app/loginheader, Apache rewrites it to the public address. Without it, clients get redirected to an address they cannot reach. - ProxyPreserveHost On passes the original
Hostheader through, which many frameworks need to generate correct absolute URLs.
Timeouts, retries, and the real client IP
The worker parameters on ProxyPass control how Apache behaves when the backend is slow or restarting. timeout=30 bounds how long Apache waits for a response; retry=0 means a failed worker is not taken out of rotation for a cooldown period, which is what you usually want for a single local backend — the default retry interval can leave Apache returning 503s for a minute after the backend recovers. There is also a global ProxyTimeout if you prefer setting it server-wide.
By default, mod_proxy_http adds X-Forwarded-For, X-Forwarded-Host, and X-Forwarded-Server headers, so the backend can recover the original client address. On the Apache side, though, access logs will show the proxy's own connection details unless you also enable mod_remoteip and declare trusted proxies — worth doing if any access control or log analysis depends on client IPs.
When one backend becomes several
If you outgrow a single backend, mod_proxy_balancer lets you define a balancer:// cluster with BalancerMember entries, weighted with loadfactor, and point ProxyPass at the cluster instead of a single host. It supports basic stickyness via cookies or URL parameters. The significant caveat: the balancer does not do active health checks by default. A dead member only stops receiving traffic after failed requests mark it in error, so the first few requests to a crashed backend still fail. If you need proactive health checking, that is a job for a dedicated load balancer or a service mesh, not Apache.
Verifying the setup
Do not trust the config — test the behavior. From a client machine, run curl -i https://example.com/app/ and check two things: the response comes from the backend, and any redirect Location header shows the public hostname, not 127.0.0.1:8080. Then hit a backend endpoint that echoes request headers and confirm X-Forwarded-For carries your real IP. Finally, stop the backend and watch what clients see with your chosen retry and timeout values, and again after restarting it — this is where retry=0 earns its keep.
The honest trade-off
Apache as a reverse proxy is a solid, boring-in-a-good-way choice when it is already in your stack, and it pairs naturally with the event or worker MPM since proxying is thread-friendly. But it is not a full-featured edge proxy: no active health checks, limited load-balancing algorithms, and directive behavior that varies slightly across 2.4 minor releases. Check the docs for your exact version, keep the config small, and you will have a front door that rarely needs attention.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.