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 onlyproduction.
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/healthworks through the customer hostname./,/normal, and/premiumroute to configured production presentations.- Browser URL remains on the customer hostname.
Rollout Strategy
- Configure one test customer hostname.
- Start with a single route:
/ -> one project slug. - Verify DNS, Apache, HTTPS, API, assets, and first page load.
- Add
/normaland/premium. - Verify images, video, audio, and service worker behavior.
- Repeat for additional customer hostnames.
Out of Scope
- Cloudflare custom hostnames or DNS changes in the
flatlogic.appzone. - Customer-side HTTP redirects.
- Stage, constructor, or admin access through customer domains.
- UI for managing custom-domain routes in the first version.