337 lines
7.4 KiB
Markdown
337 lines
7.4 KiB
Markdown
# 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:
|
|
|
|
```text
|
|
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:
|
|
|
|
```text
|
|
/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:
|
|
|
|
```text
|
|
Type: A
|
|
Name: presentation
|
|
Value: 185.8.107.221
|
|
TTL: 300 or Auto
|
|
Proxy/CDN: DNS only / Off
|
|
```
|
|
|
|
For the full hostname:
|
|
|
|
```text
|
|
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:
|
|
|
|
```text
|
|
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:
|
|
|
|
```text
|
|
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:
|
|
|
|
```bash
|
|
dig +short presentation.customer.com
|
|
```
|
|
|
|
Expected output:
|
|
|
|
```text
|
|
185.8.107.221
|
|
```
|
|
|
|
Verify HTTP reaches the VM:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
sudo nano /etc/apache2/sites-available/custom-presentation.customer.com.conf
|
|
```
|
|
|
|
Use this HTTP virtual host as the starting point:
|
|
|
|
```apache
|
|
<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:
|
|
|
|
```bash
|
|
sudo a2ensite custom-presentation.customer.com.conf
|
|
sudo apache2ctl configtest
|
|
sudo systemctl reload apache2
|
|
```
|
|
|
|
## HTTPS with Certbot
|
|
|
|
Install Certbot if needed:
|
|
|
|
```bash
|
|
sudo apt update
|
|
sudo apt install -y certbot python3-certbot-apache
|
|
```
|
|
|
|
Issue the certificate:
|
|
|
|
```bash
|
|
sudo certbot --apache \
|
|
-d presentation.customer.com \
|
|
--email admin@flatlogic.com \
|
|
--agree-tos \
|
|
--no-eff-email
|
|
```
|
|
|
|
Verify Apache and certificate state:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```text
|
|
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:
|
|
|
|
```text
|
|
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:
|
|
|
|
```text
|
|
GET /api/runtime-context/custom-domain/resolve?path=/normal
|
|
```
|
|
|
|
Behavior:
|
|
|
|
```text
|
|
hostname = request Host header / req.hostname
|
|
path = normalized query path
|
|
find active route by hostname + path
|
|
return { projectSlug, environment }
|
|
```
|
|
|
|
Failure behavior:
|
|
|
|
```text
|
|
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:
|
|
|
|
```text
|
|
/
|
|
/normal
|
|
/premium
|
|
```
|
|
|
|
Runtime behavior:
|
|
|
|
```text
|
|
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:
|
|
|
|
```text
|
|
/p/[projectSlug]
|
|
/p/[projectSlug]/stage
|
|
/constructor
|
|
```
|
|
|
|
## End-to-End Checks
|
|
|
|
DNS:
|
|
|
|
```bash
|
|
dig +short presentation.customer.com
|
|
```
|
|
|
|
Apache:
|
|
|
|
```bash
|
|
sudo apache2ctl -S
|
|
sudo ss -ltnp | grep -E ':80|:443|:3000|:3001'
|
|
```
|
|
|
|
HTTP and HTTPS:
|
|
|
|
```bash
|
|
curl -I http://presentation.customer.com
|
|
curl -I https://presentation.customer.com
|
|
```
|
|
|
|
Routes:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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.
|