39948-vm/documentation/custom-domains-apache.md
2026-07-03 16:11:24 +02:00

7.4 KiB

Custom Domains via Apache on the VM

Operational plan for serving public production presentations on customer-owned hostnames without changing Cloudflare configuration.

Current VM Facts

The checked VM uses this public IPv4 address:

185.8.107.221

The current runtime ports are:

Component Port Notes
Apache 80 Public HTTP reverse proxy
Frontend 3001 Next.js production server
Backend 3000 Express API

Apache currently listens on :80; :443 is not enabled on the VM until a certificate is issued and an SSL virtual host is created.

The existing Apache proxy pattern is:

/api/* -> http://127.0.0.1:3000
/*     -> http://127.0.0.1:3001

Cloudflare settings are out of scope for this workflow. Do not rely on a customer CNAME to tbp.flatlogic.app, because that hostname may be proxied by Cloudflare and reject unknown customer hostnames before the request reaches the VM.

Customer DNS Setup

For each customer hostname, ask the customer to create an A record pointing directly to the VM public IP:

Type: A
Name: presentation
Value: 185.8.107.221
TTL: 300 or Auto
Proxy/CDN: DNS only / Off

For the full hostname:

presentation.customer.com A 185.8.107.221

Customer-side notes:

  • DNS records cannot contain paths such as /p/presentation.
  • Do not configure an HTTP redirect to tbp.flatlogic.app/p/....
  • If the customer uses Cloudflare or another CDN, start with DNS-only mode.
  • The customer can use multiple hostnames; each hostname should point to the same VM IP and will be routed by our host/path mapping.

Required Route Data

Before VM and app setup, collect these values:

CUSTOM_DOMAIN=presentation.customer.com
CERTBOT_EMAIL=admin@flatlogic.com
ROUTES:
  /        -> presentation
  /normal  -> presentation-normal
  /premium -> presentation-premium

Routes are resolved by hostname + path, not by DNS. The same customer can have several hostnames and several paths per hostname.

Example mapping:

presentation.customer.com | /        | presentation
presentation.customer.com | /normal  | presentation-normal
presentation.customer.com | /premium | presentation-premium
hotel-a.customer.com      | /        | hotel-a-tour
hotel-b.customer.com      | /        | hotel-b-tour

DNS Verification

After the customer creates the DNS record, verify resolution from the VM:

dig +short presentation.customer.com

Expected output:

185.8.107.221

Verify HTTP reaches the VM:

curl -I http://presentation.customer.com
curl -I -H 'Host: presentation.customer.com' http://127.0.0.1/

Before HTTPS is configured, only HTTP is expected to work.

Apache Virtual Host

Create a customer-specific Apache site:

sudo nano /etc/apache2/sites-available/custom-presentation.customer.com.conf

Use this HTTP virtual host as the starting point:

<VirtualHost *:80>
  ServerName presentation.customer.com

  ProxyPreserveHost On
  ProxyRequests Off

  RewriteEngine On

  RewriteRule ^/api(/.*)?$ http://127.0.0.1:3000$0 [P,L]

  RewriteCond %{HTTP:Upgrade} =websocket [NC]
  RewriteRule /(.*) ws://127.0.0.1:3001/$1 [P,L]

  RewriteCond %{HTTP:Upgrade} !=websocket [NC]
  RewriteRule /(.*) http://127.0.0.1:3001/$1 [P,L]

  ProxyPassReverse /api http://127.0.0.1:3000/api
  ProxyPassReverse / http://127.0.0.1:3001/

  ErrorLog /var/log/apache2/custom-presentation.customer.com-error.log
  CustomLog /var/log/apache2/custom-presentation.customer.com-access.log combined
</VirtualHost>

Enable and reload:

sudo a2ensite custom-presentation.customer.com.conf
sudo apache2ctl configtest
sudo systemctl reload apache2

HTTPS with Certbot

Install Certbot if needed:

sudo apt update
sudo apt install -y certbot python3-certbot-apache

Issue the certificate:

sudo certbot --apache \
  -d presentation.customer.com \
  --email admin@flatlogic.com \
  --agree-tos \
  --no-eff-email

Verify Apache and certificate state:

sudo ss -ltnp | grep -E ':80|:443'
sudo certbot certificates
curl -I https://presentation.customer.com
sudo certbot renew --dry-run

After Certbot succeeds, Apache should listen on :443 and serve HTTPS for the customer hostname.

Application Changes

The application needs a host/path routing layer for public production runtime.

Backend Mapping

Add a backend entity such as custom_domain_routes with these minimum fields:

hostname      text, required
path          text, required
project_slug  text, required
environment   text, default production
is_active     boolean, default true
created_at
updated_at

Required constraint:

UNIQUE(hostname, path)

Normalization rules:

  • hostname: lowercase, no port.
  • path: starts with /; remove trailing slash except for /.
  • environment: first version should use only production.

Backend Resolve Endpoint

Add a public endpoint:

GET /api/runtime-context/custom-domain/resolve?path=/normal

Behavior:

hostname = request Host header / req.hostname
path = normalized query path
find active route by hostname + path
return { projectSlug, environment }

Failure behavior:

400 for invalid path
404 if no active mapping exists

Security requirements:

  • Do not accept hostname from query or body.
  • Use only the request host that reached Apache/backend.
  • Do not expose stage, constructor, or admin through customer hostnames.
  • Return only the data needed to render the public runtime.

Frontend Custom-Domain Route

Add a public catch-all route for customer-domain paths:

/
/normal
/premium

Runtime behavior:

1. Read window.location.pathname.
2. Detect that the current host is not a standard platform host.
3. Call /api/runtime-context/custom-domain/resolve?path=<pathname>.
4. Render <RuntimePresentation projectSlug={projectSlug} environment="production" />.

Keep existing platform routes unchanged:

/p/[projectSlug]
/p/[projectSlug]/stage
/constructor

End-to-End Checks

DNS:

dig +short presentation.customer.com

Apache:

sudo apache2ctl -S
sudo ss -ltnp | grep -E ':80|:443|:3000|:3001'

HTTP and HTTPS:

curl -I http://presentation.customer.com
curl -I https://presentation.customer.com

Routes:

curl -I https://presentation.customer.com/
curl -I https://presentation.customer.com/normal
curl -I https://presentation.customer.com/premium
curl -I https://presentation.customer.com/api/health

Direct upstream checks:

curl -I http://127.0.0.1:3001
curl -I http://127.0.0.1:3000/api/health

Expected outcome:

  • Customer hostname resolves to 185.8.107.221.
  • HTTPS certificate matches the customer hostname.
  • /api/health works through the customer hostname.
  • /, /normal, and /premium route to configured production presentations.
  • Browser URL remains on the customer hostname.

Rollout Strategy

  1. Configure one test customer hostname.
  2. Start with a single route: / -> one project slug.
  3. Verify DNS, Apache, HTTPS, API, assets, and first page load.
  4. Add /normal and /premium.
  5. Verify images, video, audio, and service worker behavior.
  6. Repeat for additional customer hostnames.

Out of Scope

  • Cloudflare custom hostnames or DNS changes in the flatlogic.app zone.
  • Customer-side HTTP redirects.
  • Stage, constructor, or admin access through customer domains.
  • UI for managing custom-domain routes in the first version.