The OpenVPN clients at home dial into the fixed, static-IP of the server to connect to the OpenVPN server there. That external server has an IP whitelist to only allow OpenVPN connections from my IPs (static IPs and dynamic DNS that my router automatically updates every 10 minutes or so), so nothing else can connect to OpenVPN at the external server.
Because OpenVPN assigns a 10. network to the connection (and I don't use 10. at home or at the server end), the only traffic that goes out over the VPN is that coming from the server itself (reverse proxy requests down to the home servers) and their responses. Firewalling at both ends (with UFW) means that nothing else can go over that interface but HTTP(S) and ICMP ping at either end of it.
The external server is then just set to use Apache reverse proxying to access the "10." address of one of the home machines for its content, depending on the service required. Multiple machines from my home each dial in on their own separate VPN connection to the external service, and each get a unique "10." address which allows the server to contact them. So there's no complex network configuration that could be mis-read or bring unintentional remote access to the whole subnet. Each machine at home has their own certificates for OpenVPN and they each verify the server's OpenVPN certificate on connection. They initiate the connections to the server, the server assigns them their (fixed) 10. IP based on their certificate, and then the Apache sites push any traffic for that service down the VPN connection via reverse proxy to the 10. addresses.
I went a stage further and made Apache not just reverse proxy, but also "load balance" so that if the 10. service is unavailable (i.e. the VPN is down), then it goes to "localhost" and retrieves a page that says that the VPN must be down. That was actually surprisingly simple.
So homeclient1 triggers an OpenVPN connection on itself to connect to externalserver's OpenVPN port.
externalserver accepts the connection and gives homeclient1, say, 10.0.0.1.
mydomain1 points to externalserver's public static IP address.
externalserver's Apache listens on that IP and then has a "site" defined for each service.
The Apache config of that site tells Apache to reverse proxy all accesses (except for LetsEncrypt /.well-known/acme) to the 10.0.0.1 address, which proxies the requests/responses to homeclient1's web server to actually serve content.
homeclient2 also connects to the server and is given 10.0.0.2, and Apache does the same for mydomain2 to that address. And so on.
If you're interested, I can post some Github gists's with the critical parts of my config.