changemaker.lite/PANGOLIN_NGINX_FIX_SUMMARY.md

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-changemakermini-qr
  • Port: 80898080

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

  1. configs/pangolin/resources.yml - Added 2 resources, fixed 1 container name/port
  2. nginx/conf.d/services.conf.template - Updated 6 server blocks for dual-domain support

Next Steps

  1. 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
  2. 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
  3. 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)