Suppose you have a domain that hosts your website:, and the website is served with the venerable Apache HTTP server. Suppose, also, that you want to run some backend application on the same domain, perhaps using a sub-domain like Running an application on a non-standard port (not 80 or 443) is not a problem, but what it you need it to run on port 80? Apache occupies port 80 in order to serve, so at least on the surface this seems like a problem.

Logo of the Apache HTTP server project

This post talks about how to make it work using the reverse-proxying capabilities of Apache. It assumes you control a virtual machine that has a top-level domain like mapped to it, and that the machine runs Linux.

Setting up Apache as a proxy with mod_proxy

If you need to brush up on proxy concepts, consider reading this series of posts first.

Assuming Apache is already installed and running on the server, you'll first have to enable the proxy module and restart the service:

$ sudo a2enmod proxy proxy_http
$ sudo systemctl restart apache2

Sub-domains typically have their own configuration file in /etc/apache2/sites-available. Create a new configuration file in that directory, named or some such; here's what should be in it (adjust as needed):

<VirtualHost *:80>
        ProxyPreserveHost On
        ProxyPass /
        ProxyPassReverse /


        ErrorLog ${APACHE_LOG_DIR}/error.log
        CustomLog ${APACHE_LOG_DIR}/access.log combined

This tells Apache that the route should be proxied to a service running locally on port 5000; naturally, the service address can have a different port or run on a different domain altogether.

Next you'll want to register that configuration with Apache and restart it again:

$ sudo a2ensite
$ sudo systemctl restart apache2

Running the backend service

Now that Apache is all set up, it's time to run the actual backend service at port 5000. As an example, you can run this simple header debugging server:

$ go run http-server-debug-request-headers.go -addr
2023/01/17 01:01:20 Starting server on

To test that it runs properly, in a separate terminal (on the same machine!) let's run curl:

$ curl
hello /headers

And looking at the terminal where the server is running, you should see some useful logging:

2023/01/17 01:02:50   GET     /headers        Host:
User-Agent: curl/7.81.0
Accept: */*

If you've followed all the steps in this and the previous session, it should work via the sub-domain now (from any machine):

$ curl
hello /headers

Apache listens on port 80 for, and when it sees requests to, it proxies them to the server running on port 5000 on the same machine.

If this doesn't work for you, take a careful look at the Apache logs - both the error log and the access log may be useful.

Bonus: TLS with Let's Encrypt

If your server is set up to serve via TLS using Let's Encrypt, I have good news for you -- it will just work for as well!

Presumably you've set up Let's Encrypt certificates using certbot. Since we've now added an additional Apache configuration (, we should run certbot again:

$ sudo certbot --apache

And carefully follow the on-screen instructions. certbot should detect there's a new sub-domain to get a certificate for; if everything goes as expected, it succeeds and from that point on you should be able to access the backend server via HTTPS:

$ curl
hello /headers

Note that the backend Go server serves HTTP; the reverse proxy (Apache) terminates the TLS connection and passes HTTP to the backend server. This is a fairly common way to structure backends. While the backend server serves unencrypted traffic, it's not actually accessible from outside the machine (port 5000 is unlikely to be exposed). The only way to access it is via the reverse-proxy on, which can use TLS if needed.

I was wondering how this works. certbot uses the HTTP challenge with Let's Encrypt, wherein it's asked to serve a special file on a special path (typically something like .well-known/acme-challenge) to prove to Let's Encrypt that it controls the domain. But here all requests get forwarded to the backend server...

After scratching my head for a minute I found the answer in certbot's logs, where it honestly explains its tricky ways. It turns out it adds a RewriteRule to our file for the duration of the Let's Encrypt handshake, sending any requests starting with .well-known/acme-challenge to a known disk location it controls. After all is done, it quietly removes these rules from the configuration file.