How to Use Apache’s mod_proxy for Reverse Proxying

How to use Apache's mod_proxy for reverse proxying

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/api proxies to a Node.js service, while example.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 localhost on 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 Host header through to the backend, rather than replacing it with localhost: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, and URI headers 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_http alongside proxy — the base module alone won’t handle standard HTTP proxying.
  • Mismatched ProxyPass and ProxyPassReverse paths, 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 broken http:// 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.1 rather than 0.0.0.0 so they’re only reachable through the Apache proxy, not directly from the internet.
  • Use ProxyAddHeaders On and 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_evasive or mod_auth_basic as 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=30 parameters tuned to your backend’s actual response characteristics.
  • Combine reverse proxying with mod_cache for 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:

  1. Enable mod_proxy and the protocol-specific module you need (proxy_http, proxy_wstunnel, etc.).
  2. Use ProxyPass and ProxyPassReverse together, and keep their paths matched exactly.
  3. Set ProxyPreserveHost On and X-Forwarded-Proto so backend applications behave correctly.
  4. Use mod_proxy_balancer when you need to distribute traffic across multiple backend instances.
  5. 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.

References

Total
1
Shares

Leave a Reply

Previous Post
How to use Apache's mod_rewrite for URL rewriting

How to Use Apache’s mod_rewrite for URL Rewriting

Next Post
How to configure Apache to use a Content Delivery Network (CDN)

How to Configure Apache to Use a Content Delivery Network (CDN)

Related Posts