Websites & apps

Use Apache as a reverse proxy with mod_proxy

Put Apache in front of a Node.js, Python or Docker app with mod_proxy. Set ProxyPass and ProxyPassReverse, keep the original host, proxy WebSockets and serve the app over HTTPS.

IntermediateLinux server4 min read

Many apps, such as Node.js, Python, Go or a Docker container, listen on a local port like 3000. You do not want visitors typing that port, and you want HTTPS handled in one place. Apache can sit in front as a reverse proxy: it accepts requests on ports 80 and 443 and forwards them to the app. This guide sets that up with mod_proxy.

Before you start

  • Apache 2.4 on Ubuntu, Debian, AlmaLinux or Rocky Linux.
  • An app listening on the server, for example on 127.0.0.1:3000. Test it with curl http://127.0.0.1:3000.
  • A domain such as app.example.com pointing at your server IP.
  • The app should listen on 127.0.0.1, not 0.0.0.0, so it cannot be reached directly from the internet.

Enable the proxy modules

Ubuntu and Debian

Bash
sudo a2enmod proxy proxy_http proxy_wstunnel headers rewrite
sudo systemctl restart apache2

AlmaLinux and Rocky Linux

The proxy modules are loaded by default from /etc/httpd/conf.modules.d/00-proxy.conf. Check:

Bash
httpd -M | grep proxy

SELinux blocks Apache from opening network connections by default. Allow it permanently:

Bash
sudo setsebool -P httpd_can_network_connect 1

Without this you get 503 Service Unavailable and a permission denied line in the error log.

Create the proxy virtual host

Create /etc/apache2/sites-available/app.example.com.conf (Ubuntu and Debian) or /etc/httpd/conf.d/app.example.com.conf (AlmaLinux and Rocky):

Apache
<VirtualHost *:80>
    ServerName app.example.com

    ProxyPreserveHost On
    ProxyRequests Off

    ProxyPass        / http://127.0.0.1:3000/
    ProxyPassReverse / http://127.0.0.1:3000/

    ErrorLog ${APACHE_LOG_DIR}/app.example.com-error.log
    CustomLog ${APACHE_LOG_DIR}/app.example.com-access.log combined
</VirtualHost>

On AlmaLinux and Rocky, write the log paths in full under /var/log/httpd/.

What each line does:

  • ProxyPass forwards requests for / to the app. Keep the trailing slash on both sides, or none on either.
  • ProxyPassReverse rewrites Location headers in the app's redirects, so a redirect to http://127.0.0.1:3000/login becomes http://app.example.com/login.
  • ProxyPreserveHost On passes the visitor's Host header to the app. Many apps need it to build correct links.
  • ProxyRequests Off keeps Apache from acting as an open forward proxy. Never turn it on for a public server.

Enable the site on Ubuntu and Debian with sudo a2ensite app.example.com.conf, then test and reload:

Bash
sudo apachectl configtest
sudo systemctl reload apache2    # or httpd

Proxy only part of a site

To serve a normal website and send only /api to the app:

Apache
ProxyPass        /api/ http://127.0.0.1:3000/
ProxyPassReverse /api/ http://127.0.0.1:3000/

A request for /api/users reaches the app as /users. If the app expects the /api prefix, use http://127.0.0.1:3000/api/ as the target instead.

To exclude a folder from a site-wide proxy, add an exclusion before the main rule:

Apache
ProxyPass /static/ !
ProxyPass / http://127.0.0.1:3000/

Proxy WebSockets

Apps that use WebSockets (chat, live dashboards, Socket.IO) need the upgrade passed through. On Apache 2.4.47 and later, mod_proxy_http can do it with one option:

Apache
ProxyPass        / http://127.0.0.1:3000/ upgrade=websocket
ProxyPassReverse / http://127.0.0.1:3000/

On older Apache versions, use rewrite rules with mod_proxy_wstunnel:

Apache
RewriteEngine On
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule ^/?(.*) ws://127.0.0.1:3000/$1 [P,L]

Check your version with apachectl -v.

Add HTTPS in front

Get a certificate for the proxy host with certbot, exactly as for a normal site:

Bash
sudo certbot --apache -d app.example.com

Certbot copies the proxy lines into the new HTTPS virtual host. Then tell the app the original request was HTTPS, so it builds https:// links. Add this inside the *:443 vhost:

Apache
RequestHeader set X-Forwarded-Proto "https"

Apache already sends X-Forwarded-For with the visitor's IP. Configure your app to trust these headers from 127.0.0.1 only.

Check it works

Bash
curl -I https://app.example.com

You should get the app's normal response, such as 200 OK, with Server: Apache in the headers. Then confirm the app port is not reachable from outside. From your own computer:

Bash
curl -m 5 http://203.0.113.10:3000

This should time out or be refused. If it answers, bind the app to 127.0.0.1 or block the port in your firewall.

Troubleshooting

  • 503 Service Unavailable: the app is not running, is on another port, or (on AlmaLinux and Rocky) SELinux blocks the connection. Check curl http://127.0.0.1:3000 and the httpd_can_network_connect boolean.
  • Redirects go to 127.0.0.1:3000: ProxyPassReverse is missing or does not match the ProxyPass target exactly.
  • CSS and links break under a sub-path like /api: the app thinks it lives at /. Set its base path setting, or give it its own subdomain.
  • WebSocket connection fails with 400 or keeps reconnecting: the upgrade is not passed. Use upgrade=websocket or the rewrite rules above.
  • Infinite redirect after adding HTTPS: the app does not see HTTPS and redirects again. Add the X-Forwarded-Proto header and enable proxy trust in the app.

Next steps

Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.