Apache as a Reverse Proxy
A reverse proxy is a server that sits in front of your application and forwards incoming requests to it, then sends the application’s response back to the client. Apache (a very popular open-source web server) can act as a reverse proxy using a module called mod_proxy. This is useful when your app (for example a Node.js, Python, or Java service) listens on an internal port like localhost:3000, and you want the public internet to reach it on the normal web ports (80 for HTTP and 443 for HTTPS) through a clean domain name. This page mirrors the Nginx reverse-proxy page so you can compare the two side by side.
Why put Apache in front of your app
Most application servers (the program that actually runs your code, like a Node.js process) are not designed to face the public internet directly. They are slow at serving static files, weak at handling many slow connections, and they usually run on a high port that users should never see.
A reverse proxy fixes this. Apache listens on port 80/443, handles the messy parts (connections, TLS encryption, logging), and quietly hands the real work to your backend (the app running on a local port).
When to use this: you have an app on localhost:<port> and want a real domain, ports 80/443, central logging, and a single place to add TLS later.
When NOT to use this: if you only serve plain static HTML/CSS files, you don’t need a proxy at all — just serve the files directly with a virtual host.
Step 1 — install Apache
If Apache is not installed yet, install it on Ubuntu 22.04 or 24.04 LTS with apt (the Ubuntu package manager):
sudo apt update
sudo apt install apache2 -y
Output:
Reading package lists... Done
Building dependency tree... Done
The following NEW packages will be installed:
apache2 apache2-bin apache2-data apache2-utils
...
Setting up apache2 (2.4.58-1ubuntu8.5) ...
Step 2 — enable the proxy modules
A module in Apache is an optional feature you turn on. To act as a reverse proxy you need two modules: proxy (the core proxy engine) and proxy_http (which knows how to proxy HTTP traffic). Ubuntu ships them but leaves them disabled.
Enable them with a2enmod (Apache’s “enable module” helper), then restart Apache so the change takes effect:
sudo a2enmod proxy
sudo a2enmod proxy_http
sudo systemctl restart apache2
Output:
Enabling module proxy.
Enabling module proxy_http.
To activate the new configuration, you need to run:
systemctl restart apache2
Only enable the modules you actually need. Apache also ships
mod_proxy_balancer,mod_proxy_ftpand others. Enabling extra modules widens the attack surface (the number of ways an attacker could try to break in) for no benefit.
Step 3 — write the reverse-proxy virtual host
A virtual host is a block of config that tells Apache how to handle requests for one domain. On Ubuntu these live in /etc/apache2/sites-available/. Create one for your app:
sudo nano /etc/apache2/sites-available/myapp.conf
Paste the following. This example assumes your backend app is running on http://127.0.0.1:3000:
<VirtualHost *:80>
ServerName myapp.example.com
# Forward all requests to the backend app on localhost:3000
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:3000/
ProxyPassReverse / http://127.0.0.1:3000/
# Pass the real client info to the backend
RequestHeader set X-Forwarded-Proto "http"
RemoteIPHeader X-Forwarded-For
ErrorLog ${APACHE_LOG_DIR}/myapp-error.log
CustomLog ${APACHE_LOG_DIR}/myapp-access.log combined
</VirtualHost>
What each directive does
| Directive | What it does |
|---|---|
ProxyPass / http://127.0.0.1:3000/ | Sends every incoming path (/) to the backend app. |
ProxyPassReverse / http://127.0.0.1:3000/ | Rewrites Location headers in redirects so the client sees your domain, not 127.0.0.1:3000. |
ProxyPreserveHost On | Forwards the original Host header (your domain) to the backend instead of 127.0.0.1. Many apps need this to build correct URLs. |
RequestHeader set X-Forwarded-Proto | Tells the backend whether the original request was HTTP or HTTPS. |
RemoteIPHeader X-Forwarded-For | Lets the backend see the real visitor IP, not Apache’s. |
Always use
127.0.0.1(loopback) for the backend, not0.0.0.0. Your app should bind to localhost so it is unreachable from the internet directly — only Apache can talk to it. Then control public access withufw(Ubuntu’s firewall).
Step 4 — enable the site and reload
Apache won’t read a file in sites-available until you enable it with a2ensite, which creates a symlink (a shortcut) in sites-enabled. Then test the config and reload.
sudo a2ensite myapp.conf
sudo apache2ctl configtest
sudo systemctl reload apache2
Output:
Enabling site myapp.
Syntax OK
Syntax OK means your config is valid. Always run configtest before reloading — a bad config can take the whole server down.
Step 5 — open the firewall and verify
Allow web traffic through ufw and test the proxy with curl:
sudo ufw allow 'Apache Full'
curl -I http://myapp.example.com
Output:
HTTP/1.1 200 OK
Date: Mon, 16 Jun 2026 10:22:14 GMT
Server: Apache/2.4.58 (Ubuntu)
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
The 200 OK with X-Powered-By: Express confirms Apache reached your backend app and returned its response.
Apache vs Nginx as a reverse proxy
| Point | Apache (mod_proxy) | Nginx |
|---|---|---|
| Config style | XML-like <VirtualHost> blocks | Block syntax with location |
| Proxy directive | ProxyPass / ProxyPassReverse | proxy_pass |
| Preserve host | ProxyPreserveHost On | proxy_set_header Host $host; |
| Strength | .htaccess, huge module ecosystem | High concurrency, low memory |
| When to pick | You already run Apache or need its modules | You want maximum performance under load |
Best practices
- Bind your backend app to
127.0.0.1only, never a public interface. - Run
sudo apache2ctl configtestbefore everyreloadorrestart. - Keep
ProxyPreserveHost Onso the backend builds correct absolute URLs. - Set
X-Forwarded-ProtoandX-Forwarded-Forso your app knows the real protocol and client IP. - Disable
ProxyRequests(it is off by default) — leaving it on creates an open forward proxy, a serious security hole. - Add TLS with Let’s Encrypt and
mod_sslso traffic between the client and Apache is encrypted. - Give each app its own
.conffile and log files for easy debugging.