6.2 KiB
Pangolin Tunnel Management + Nginx Multi-Domain Support - Implementation Summary
Date: 2026-02-15 Status: ✅ COMPLETE
Changes Implemented
1. Pangolin Resources Configuration (configs/pangolin/resources.yml)
Added 2 new resources:
- Excalidraw (subdomain:
draw, container:excalidraw-changemaker, port: 80) - MailHog (subdomain:
mail, container:mailhog-changemaker, port: 8025)
Fixed Mini QR resource:
- Container name:
miniqr-changemaker→mini-qr✅ - Port:
8089→8080✅
Total resources: 14 (up from 12)
2. Nginx Template Updates (nginx/conf.d/services.conf.template)
Updated 6 server blocks to support multi-domain routing:
| Service | Old server_name | New server_name |
|---|---|---|
| Gitea | git.${DOMAIN} |
git.cmlite.org git.betteredmonton.org |
| n8n | n8n.${DOMAIN} |
n8n.cmlite.org n8n.betteredmonton.org |
| Code Server | code.${DOMAIN} |
code.cmlite.org code.betteredmonton.org |
| MailHog | mail.${DOMAIN} |
mail.cmlite.org mail.betteredmonton.org |
| Mini QR | qr.${DOMAIN} |
qr.cmlite.org qr.betteredmonton.org |
| Excalidraw | draw.${DOMAIN} |
draw.cmlite.org draw.betteredmonton.org |
CSP Headers Updated: All 6 services now allow iframe embedding from both admin domains:
add_header Content-Security-Policy "frame-ancestors 'self' app.cmlite.org app.betteredmonton.org" always;
3. Nginx Container Rebuild
- Rebuilt nginx image to pick up updated template files
- Restarted nginx container with new configuration
- Verified nginx syntax: ✅ OK
Root Cause Analysis
Issue 1: Missing Resources
- Excalidraw and MailHog existed in infrastructure but weren't listed in
resources.yml - Pangolin page reads from this YAML to display resource status
Issue 2: X-Frame-Options Error
- Nginx template used
${DOMAIN}variable (only substitutes ONE domain from.env) - When accessing via alternate domain, nginx didn't match any server block
- Request fell back to default server with
X-Frame-Options: SAMEORIGIN - This blocked iframe embedding
Issue 3: Wrong Container Name
- resources.yml referenced
miniqr-changemaker(doesn't exist) - Correct name from docker-compose.yml:
mini-qr
Solution Applied
Pattern: Hardcoded dual-domain server_name directives (same pattern as NocoDB, Listmonk, Grafana, etc.)
Why not template variables?
${DOMAIN}substitution only supports ONE domain value- We need BOTH domains for multi-domain production deployment
- Hardcoding is explicit and predictable
Verification Results
✅ Nginx Configuration
$ docker compose exec nginx nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
✅ Multi-Domain Server Blocks
All 6 services verified with both domains in server_name:
- Mini QR:
qr.cmlite.org qr.betteredmonton.org - Gitea:
git.cmlite.org git.betteredmonton.org - n8n:
n8n.cmlite.org n8n.betteredmonton.org - Code Server:
code.cmlite.org code.betteredmonton.org - MailHog:
mail.cmlite.org mail.betteredmonton.org - Excalidraw:
draw.cmlite.org draw.betteredmonton.org
✅ Resources Count
$ grep -c "subdomain:" configs/pangolin/resources.yml
14
Testing Instructions
1. Test Pangolin Resources Page
# Access Pangolin page
# Navigate to http://localhost:3000/app/pangolin
# or https://app.betteredmonton.org/app/pangolin
# Verify:
# ✅ Resources table shows 14 services
# ✅ "Excalidraw" appears in list
# ✅ "MailHog" appears in list
# ✅ Mini QR container shows as "mini-qr"
2. Test Multi-Domain Access (Production)
# Test all 6 services respond on both domains
for service in qr git n8n code mail draw; do
for domain in cmlite.org betteredmonton.org; do
echo "Testing ${service}.${domain}..."
curl -I https://${service}.${domain} 2>&1 | grep HTTP | head -1
done
done
# Expected: All return HTTP/1.1 200 OK (or 302 for auth-protected)
3. Test Iframe Embedding
# Test Mini QR iframe from both domains
# 1. Navigate to https://app.betteredmonton.org/app/services/qr
# 2. Check browser console for errors
# 3. Verify Mini QR loads without "Refused to display" error
# 4. Repeat from https://app.cmlite.org/app/services/qr
# Test other services similarly
4. Verify CSP Headers
# Check CSP headers include both admin domains
curl -I https://qr.betteredmonton.org 2>&1 | grep Content-Security-Policy
# Expected:
# Content-Security-Policy: frame-ancestors 'self' app.cmlite.org app.betteredmonton.org
Success Criteria
✅ Pangolin resources list shows 14 services (up from 12)
✅ Excalidraw and MailHog appear in resources table
✅ Mini QR container name corrected to mini-qr
✅ Mini QR iframe loads without X-Frame-Options errors
✅ All 6 iframe-embedded services work on both domains
✅ CSP headers allow embedding from both app domains
✅ Nginx config regenerates successfully without syntax errors
Files Modified
- configs/pangolin/resources.yml - Added 2 resources, fixed 1 container name/port
- nginx/conf.d/services.conf.template - Updated 6 server blocks for dual-domain support
Next Steps
-
Pangolin Setup (Manual)
- Login to Pangolin dashboard
- Create resources for new services (Excalidraw, MailHog)
- Set authentication to "Not Protected" for public access
- Verify tunnel connectivity
-
Production Deployment
- Rebuild nginx image:
docker compose build nginx - Restart nginx:
docker compose up -d nginx - Test all services on both domains
- Verify iframe embedding works
- Rebuild nginx image:
-
Documentation Updates
- Update CLAUDE.md with new resource count
- Update MEMORY.md with nginx rebuild requirement for template changes
- Document dual-domain pattern in troubleshooting guide
Notes
- Important: Template changes require nginx image rebuild (
docker compose build nginx) - Pattern: Hardcoded domains prevent future confusion vs. variable substitution
- Scope: Frontend already uses
buildServiceUrl()correctly (no code changes needed) - Manual Setup: Pangolin resources.yml is documentation/reference only (manual setup per MEMORY.md)