How to Use Apache’s mod_proxy for Reverse Proxying

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:

Prerequisites

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:

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

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

Security Best Practices

Performance Optimization Tips

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

Exit mobile version