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 withcurl http://127.0.0.1:3000. - A domain such as
app.example.compointing at your server IP. - The app should listen on
127.0.0.1, not0.0.0.0, so it cannot be reached directly from the internet.
Enable the proxy modules
Ubuntu and Debian
sudo a2enmod proxy proxy_http proxy_wstunnel headers rewrite
sudo systemctl restart apache2AlmaLinux and Rocky Linux
The proxy modules are loaded by default from /etc/httpd/conf.modules.d/00-proxy.conf. Check:
httpd -M | grep proxySELinux blocks Apache from opening network connections by default. Allow it permanently:
sudo setsebool -P httpd_can_network_connect 1Without 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):
<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:
ProxyPassforwards requests for/to the app. Keep the trailing slash on both sides, or none on either.ProxyPassReverserewritesLocationheaders in the app's redirects, so a redirect tohttp://127.0.0.1:3000/loginbecomeshttp://app.example.com/login.ProxyPreserveHost Onpasses the visitor'sHostheader to the app. Many apps need it to build correct links.ProxyRequests Offkeeps 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:
sudo apachectl configtest
sudo systemctl reload apache2 # or httpdProxy only part of a site
To serve a normal website and send only /api to the app:
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:
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:
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:
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:
sudo certbot --apache -d app.example.comCertbot 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:
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
curl -I https://app.example.comYou 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:
curl -m 5 http://203.0.113.10:3000This 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:3000and thehttpd_can_network_connectboolean. - Redirects go to
127.0.0.1:3000:ProxyPassReverseis missing or does not match theProxyPasstarget 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=websocketor the rewrite rules above. - Infinite redirect after adding HTTPS: the app does not see HTTPS and redirects again. Add the
X-Forwarded-Protoheader and enable proxy trust in the app.
Next steps
- Run your Node.js app as a service: Deploy Node.js with PM2 and Nginx explains the app side.
- Prefer Nginx for proxying? See Nginx reverse proxy setup.
