The first time I needed to put Apache in front of a Node.js application, I assumed I’d need some exotic third-party tool. Turns out Apache already had everything I needed built right in, via mod_proxy. Since then, I’ve used it to front Node apps, Python services, Docker containers, and even other Apache instances. It’s become one of my go-to tools for tying together mixed-technology backends behind a single, clean domain.
What Is a Reverse Proxy, and Why Use One?
A reverse proxy sits in front of one or more backend servers and forwards client requests to them, then returns the backend’s response back to the client — all while the client only ever talks to the proxy, never directly to the backend.
This is different from a “forward proxy,” which sits in front of clients (like a corporate proxy filtering outbound traffic). A reverse proxy sits in front of servers.
Common reasons I reach for mod_proxy:
- Unifying multiple backend services under one domain:
example.com/apiproxies to a Node.js service, whileexample.com/serves a static site directly from Apache. - SSL termination: Handle HTTPS at the Apache layer while backend services run plain HTTP internally.
- Load balancing: Distribute requests across multiple backend instances.
- Hiding backend infrastructure details: Clients never see internal ports or server names.
- Adding a caching or security layer in front of an application server that doesn’t have those features built in.
Prerequisites
- Apache installed with root/sudo access.
- A backend service running somewhere reachable from your Apache server (could be
localhoston a different port, or a completely separate server). - Basic familiarity with editing Apache virtual host configuration files.
Step 1: Enable the Required Modules
mod_proxy itself only provides the core functionality — you also need protocol-specific modules depending on what you’re proxying to.
sudo a2enmod proxy
sudo a2enmod proxy_http
sudo a2enmod proxy_balancer
sudo a2enmod lbmethod_byrequests
If you’re proxying to a WebSocket-based application (common with real-time apps built on Node.js), also enable:
sudo a2enmod proxy_wstunnel
Restart Apache after enabling modules:
sudo systemctl restart apache2
Step 2: Basic Reverse Proxy Configuration
Let’s say you have a Node.js app running on localhost:3000, and you want example.com to proxy to it entirely.
<VirtualHost *:80>
ServerName example.com
ProxyPreserveHost On
ProxyPass / http://localhost:3000/
ProxyPassReverse / http://localhost:3000/
ErrorLog ${APACHE_LOG_DIR}/example.com-error.log
CustomLog ${APACHE_LOG_DIR}/example.com-access.log combined
</VirtualHost>
Let me break down these directives:
- ProxyPreserveHost On: Passes the original
Hostheader through to the backend, rather than replacing it withlocalhost:3000. Important for apps that generate absolute URLs based on the host header. - ProxyPass: Defines the actual proxying rule — requests to
/get forwarded to the backend URL. - ProxyPassReverse: Rewrites
Location,Content-Location, andURIheaders in the backend’s response so redirects work correctly from the client’s perspective.
Step 3: Path-Based Proxying
More often, I need to proxy only a specific path — say, /api — while everything else is served normally by Apache:
<VirtualHost *:80>
ServerName example.com
DocumentRoot /var/www/example.com/public_html
ProxyPreserveHost On
ProxyPass /api http://localhost:3000/api
ProxyPassReverse /api http://localhost:3000/api
<Directory /var/www/example.com/public_html>
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
This is incredibly useful for setups where a static frontend (served directly by Apache) needs to talk to a backend API running on a completely different technology stack.
Step 4: Proxying WebSockets
If your backend uses WebSockets (common for chat apps, live dashboards, or anything real-time), you need a slightly different configuration using mod_proxy_wstunnel:
<VirtualHost *:80>
ServerName example.com
RewriteEngine On
RewriteCond %{HTTP:Upgrade} =websocket [NC]
RewriteRule /(.*) ws://localhost:3000/$1 [P,L]
RewriteCond %{HTTP:Upgrade} !=websocket [NC]
RewriteRule /(.*) http://localhost:3000/$1 [P,L]
ProxyPassReverse / http://localhost:3000/
</VirtualHost>
This checks the Upgrade header to determine whether the request is a WebSocket handshake, routing it to ws:// accordingly while normal HTTP traffic goes through the standard proxy path.
Step 5: Load Balancing Across Multiple Backends
If you’re running multiple instances of your backend application (for redundancy or scaling), mod_proxy_balancer lets you distribute traffic across them:
<Proxy "balancer://mycluster">
BalancerMember http://localhost:3001
BalancerMember http://localhost:3002
BalancerMember http://localhost:3003
</Proxy>
<VirtualHost *:80>
ServerName example.com
ProxyPreserveHost On
ProxyPass / balancer://mycluster/
ProxyPassReverse / balancer://mycluster/
</VirtualHost>
By default this uses a simple round-robin request distribution, but you can adjust the load-balancing method (byrequests, bytraffic, bybusyness) by enabling the corresponding module.
Step 6: Adding SSL Termination
In most production setups, Apache handles HTTPS while the backend runs plain HTTP internally:
<VirtualHost *:443>
ServerName example.com
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
ProxyPreserveHost On
ProxyPass / http://localhost:3000/
ProxyPassReverse / http://localhost:3000/
RequestHeader set X-Forwarded-Proto "https"
</VirtualHost>
That last line, RequestHeader set X-Forwarded-Proto "https", is important — it tells your backend application the original request was HTTPS, even though the proxy-to-backend connection itself is plain HTTP. Many frameworks use this header to correctly generate HTTPS links.
Real-World Use Cases
- Fronting Node.js or Python apps with Apache handling SSL, static files, and caching, while the app server focuses purely on application logic.
- Microservices architectures: Routing different URL paths to different backend services under one unified domain.
- Legacy system modernization: Gradually migrating parts of an old monolith to new services, using path-based proxying to route only specific endpoints to the new system.
- Internal tool exposure: Safely exposing internal dashboards or admin panels through Apache with additional authentication layers, without exposing the backend port directly.
Troubleshooting Common Issues
Problem: 502 Bad Gateway errors. This almost always means Apache can’t reach the backend at all. Verify the backend service is actually running (curl http://localhost:3000 directly from the server) and that the port matches your ProxyPass configuration.
Problem: Redirects from the backend point to the wrong URL. Check that ProxyPassReverse matches your ProxyPass directive exactly, and confirm ProxyPreserveHost On is set if your backend generates URLs based on the Host header.
Problem: WebSocket connections fail immediately. Confirm mod_proxy_wstunnel is enabled and that your rewrite rules correctly detect the Upgrade header.
Problem: Backend receives requests as coming from 127.0.0.1 instead of the real client IP. Add RemoteIPHeader and mod_remoteip, or simpler still, pass X-Forwarded-For and configure your backend framework to trust and read it:
ProxyAddHeaders On
Common Mistakes to Avoid
- Forgetting to enable
proxy_httpalongsideproxy— the base module alone won’t handle standard HTTP proxying. - Mismatched
ProxyPassandProxyPassReversepaths, breaking redirect handling silently. - Proxying WebSocket traffic through standard HTTP proxy directives without the WebSocket-specific rewrite rules or
mod_proxy_wstunnel. - Not setting
X-Forwarded-Proto, causing backend applications to generate brokenhttp://links on an HTTPS site. - Exposing the backend port directly to the internet in addition to the proxy, defeating much of the purpose of proxying in the first place — always firewall the backend port to localhost-only access where possible.
Security Best Practices
- Bind backend services to
127.0.0.1rather than0.0.0.0so they’re only reachable through the Apache proxy, not directly from the internet. - Use
ProxyAddHeaders Onand configure your backend to trust forwarded headers only from Apache itself, not from arbitrary clients. - Apply rate limiting or authentication at the Apache layer for sensitive proxied endpoints using
mod_evasiveormod_auth_basicas appropriate. - Keep TLS termination at the Apache layer up to date with strong cipher suites, since this is the boundary the public internet actually touches.
Performance Optimization Tips
- Enable connection pooling with
ProxyPass ... connectiontimeout=5 timeout=30parameters tuned to your backend’s actual response characteristics. - Combine reverse proxying with
mod_cachefor cacheable backend responses, reducing repeated load on the application server. - Use load balancing with health checks (
BalancerMember ... retry=5) so Apache automatically routes around a backend instance that’s gone down. - Monitor backend response times separately from Apache’s own metrics to identify whether bottlenecks are in the proxy layer or the application itself.
Frequently Asked Questions
Is mod_proxy the same as a dedicated reverse proxy like Nginx or HAProxy? Functionally, it accomplishes similar goals, though dedicated reverse proxies are sometimes more performant at very high concurrency. For most small-to-medium setups, mod_proxy is more than capable and saves you from running an entirely separate piece of software.
Can I proxy to a backend on a different physical server? Yes — the ProxyPass target can be any reachable host and port, not just localhost.
Do I need mod_proxy_balancer if I only have one backend instance? No, plain ProxyPass/ProxyPassReverse is sufficient for a single backend. Balancer configuration is only needed for multiple instances.
How do I exclude certain paths from being proxied? Use ProxyPass /path ! (with a trailing exclamation mark) before your main proxy rule to explicitly exclude that path from proxying.
Summary and Key Takeaways
mod_proxy turns Apache into a capable, flexible reverse proxy without needing any additional software — which has saved me from unnecessary infrastructure complexity more times than I can count.
The key points to remember:
- Enable
mod_proxyand the protocol-specific module you need (proxy_http,proxy_wstunnel, etc.). - Use
ProxyPassandProxyPassReversetogether, and keep their paths matched exactly. - Set
ProxyPreserveHost OnandX-Forwarded-Protoso backend applications behave correctly. - Use
mod_proxy_balancerwhen you need to distribute traffic across multiple backend instances. - Keep backend services bound to localhost or internal networks only, letting Apache be the sole public-facing entry point.
Once configured properly, this setup gives you the best of both worlds: Apache’s mature HTTP handling and SSL support in front, with whatever backend technology suits your application best behind the scenes.
