Running an HTTP backend on the same machine as an Apache-hosted site usually hits one obstacle: Apache already owns ports 80 and 443. The standard workaround is to give Apache the job of reverse-proxying a sub-domain to your backend, which keeps a single public entry point and lets Apache handle TLS.

This assumes a Linux VM with a domain such as domain.com mapped to it, Apache installed and running, and a backend service you want to expose via a sub-domain like sub.domain.com.

Enabling mod_proxy

First, enable Apache's proxy module and restart the service:

$ sudo a2enmod proxy proxy_http
$ sudo systemctl restart apache2

Creating the virtual host

Sub-domains usually get their own configuration file under /etc/apache2/sites-available. Create a file there, say sub.domain.com.conf, containing:

<VirtualHost *:80>
        ProxyPreserveHost On
        ProxyPass / http://127.0.0.1:5000/
        ProxyPassReverse / http://127.0.0.1:5000/

        ServerName sub.domain.com
        ServerAdmin [email protected]

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

This tells Apache that requests to sub.domain.com should be forwarded to a service running locally on port 5000. The target can just as easily be a different port or a remote host.

Register the configuration and restart Apache:

$ sudo a2ensite sub.domain.com.conf
$ sudo systemctl restart apache2

Testing the backend

With Apache configured, run the backend service on port 5000. A simple header-debugging server works for verification:

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

From a separate terminal on the same machine, test it with curl:

$ curl 127.0.0.1:5000/headers
hello /headers

The server's terminal should show the incoming request logging:

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

From any machine, the sub-domain should now reach the backend:

$ curl http://sub.domain.com/headers
hello /headers

Apache listens on port 80 for domain.com; when it sees a request for sub.domain.com, it proxies that request to the service listening on port 5000. If something fails, check both Apache's error log and access log for clues.

TLS for the sub-domain

If the main domain already uses Let's Encrypt via certbot, adding the sub-domain is nearly automatic. Since a new Apache configuration now exists, run certbot again:

$ sudo certbot --apache

Follow the prompts. certbot will detect the new virtual host, obtain a certificate for sub.domain.com, and HTTPS access follows:

$ curl https://sub.domain.com/headers
hello /headers

The backend itself serves plain HTTP, but it's not reachable from outside the machine—port 5000 typically isn't exposed publicly. Apache terminates TLS and forwards HTTP to the backend, which is a common pattern.

One subtlety: certbot's HTTP challenge needs to serve a special file under a path like .well-known/acme-challenge to prove domain ownership. At first glance, that seems impossible when all requests to the sub-domain are proxied to the backend. However, certbot handles it transparently: during the handshake it temporarily adds a RewriteRule to sub.domain.com.conf, routing requests starting with .well-known/acme-challenge to a local directory it controls. Once verification succeeds, it removes those rules quietly.