Pangolin Tunnel Deployment¶
Overview¶
Pangolin is a self-hosted tunnel service (alternative to Cloudflare Tunnel) that provides public HTTPS access to your Changemaker Lite instance without port forwarding or firewall configuration.
Benefits: - No port forwarding needed - SSL/TLS handled by tunnel provider - Static public URLs - Self-hosted tunnel server (privacy/control) - Free/open source
Architecture:
Changemaker Integration: - Pangolin server: https://api.bnkserve.org/v1 - Tunnel endpoint: https://pangolin.bnkserve.org - Newt container: Tunnel connector (fosrl/newt image) - Admin GUI: PangolinPage.tsx setup wizard
Setup Workflow¶
1. Prerequisites¶
Required: - Pangolin API key (obtain from Pangolin admin) - Docker Compose running - Nginx container accessible from Newt
Environment Variables:
PANGOLIN_API_URL=https://api.bnkserve.org/v1
PANGOLIN_API_KEY=<your-api-key>
PANGOLIN_ORG_ID=<set-after-org-creation>
PANGOLIN_SITE_ID=<set-after-site-creation>
PANGOLIN_ENDPOINT=https://pangolin.bnkserve.org
PANGOLIN_NEWT_ID=<set-after-resource-creation>
PANGOLIN_NEWT_SECRET=<set-after-resource-creation>
2. Setup via Admin GUI¶
Easiest method: Use /app/pangolin page in admin GUI.
Steps:
1. Navigate to http://localhost:3000/app/pangolin
2. Enter PANGOLIN_API_KEY (click "Test Connection")
3. Create Organization (or select existing)
4. Create Site (linked to org)
5. Create Endpoint (tunnel URL)
6. Create Resource (Newt connector credentials)
7. Copy NEWT_ID and NEWT_SECRET to .env
8. Restart Newt container: docker compose restart newt
3. Manual Setup (CLI)¶
Organization:
curl -X POST https://api.bnkserve.org/v1/orgs \
-H "Authorization: Bearer $PANGOLIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "My Organization",
"description": "Changemaker Lite Production"
}'
# Returns:
# {"id":"org_abc123","name":"My Organization",...}
export PANGOLIN_ORG_ID=org_abc123
Site:
curl -X POST https://api.bnkserve.org/v1/sites \
-H "Authorization: Bearer $PANGOLIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"org_id": "'"$PANGOLIN_ORG_ID"'",
"name": "Production Site",
"description": "Main deployment"
}'
# Returns:
# {"id":"site_xyz789","name":"Production Site",...}
export PANGOLIN_SITE_ID=site_xyz789
Endpoint:
curl -X POST https://api.bnkserve.org/v1/endpoints \
-H "Authorization: Bearer $PANGOLIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"site_id": "'"$PANGOLIN_SITE_ID"'",
"subdomain": "changemaker",
"domain": "pangolin.bnkserve.org"
}'
# Returns:
# {"id":"endpoint_def456","url":"https://changemaker.pangolin.bnkserve.org",...}
export PANGOLIN_ENDPOINT=https://changemaker.pangolin.bnkserve.org
Resource (Newt Connector):
curl -X POST https://api.bnkserve.org/v1/resources \
-H "Authorization: Bearer $PANGOLIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"endpoint_id": "<endpoint-id>",
"target": "http://nginx:80",
"name": "Changemaker Services"
}'
# Returns:
# {"id":"newt_abc123","secret":"secret_xyz789",...}
export PANGOLIN_NEWT_ID=newt_abc123
export PANGOLIN_NEWT_SECRET=secret_xyz789
Update .env:
cat >> .env <<EOF
PANGOLIN_ORG_ID=$PANGOLIN_ORG_ID
PANGOLIN_SITE_ID=$PANGOLIN_SITE_ID
PANGOLIN_ENDPOINT=$PANGOLIN_ENDPOINT
PANGOLIN_NEWT_ID=$PANGOLIN_NEWT_ID
PANGOLIN_NEWT_SECRET=$PANGOLIN_NEWT_SECRET
EOF
4. Start Newt Container¶
# Restart to pick up new env vars
docker compose up -d --force-recreate newt
# Check logs
docker compose logs -f newt
# Should see:
# [INFO] Connected to Pangolin tunnel
# [INFO] Tunnel active: https://changemaker.pangolin.bnkserve.org
5. DNS Configuration¶
Option A: Direct Access (use tunnel URL): - https://changemaker.pangolin.bnkserve.org
Option B: Custom Domain (CNAME to tunnel):
app.cmlite.org. CNAME changemaker.pangolin.bnkserve.org.
api.cmlite.org. CNAME changemaker.pangolin.bnkserve.org.
Newt Container Configuration¶
docker-compose.yml:
newt:
image: fosrl/newt
container_name: newt-changemaker
restart: unless-stopped
environment:
- PANGOLIN_ENDPOINT=${PANGOLIN_ENDPOINT}
- NEWT_ID=${PANGOLIN_NEWT_ID}
- NEWT_SECRET=${PANGOLIN_NEWT_SECRET}
depends_on:
- nginx
networks:
- changemaker-lite
Key Features:
- Restart policy: unless-stopped (auto-reconnects)
- Nginx dependency: Ensures Nginx running before Newt starts
- No ports exposed: All traffic via tunnel
Target: Newt connects to http://nginx:80 (Docker internal network).
Tunnel Lifecycle¶
Start Tunnel¶
Stop Tunnel¶
Check Status¶
# Container status
docker compose ps newt
# Logs
docker compose logs --tail=50 newt
# Test tunnel
curl https://changemaker.pangolin.bnkserve.org/api/health
Restart (after .env changes)¶
Exit Nodes & Resource Routing¶
Resource: Defines how tunnel routes traffic to your services.
Target URL: http://nginx:80 (Nginx handles subdomain routing internally).
Example Flow:
1. User visits https://changemaker.pangolin.bnkserve.org/api/health
2. Pangolin tunnel receives HTTPS request
3. Tunnel forwards to Newt container
4. Newt proxies to http://nginx:80/api/health
5. Nginx routes to changemaker-v2-api:4000/api/health
6. Response flows back through tunnel
Multiple Resources: Create separate resources for different backends (advanced).
Troubleshooting¶
Tunnel Not Connecting¶
Symptoms: Newt logs show connection errors
Diagnosis:
docker compose logs newt
# Common errors:
# - "Authentication failed" (wrong NEWT_ID/SECRET)
# - "Endpoint not found" (wrong PANGOLIN_ENDPOINT)
# - "Connection refused" (Nginx not running)
Solutions:
# Verify credentials
docker compose exec newt printenv | grep PANGOLIN
# Test Nginx from Newt container
docker compose exec newt wget -O- http://nginx:80/api/health
# Restart Newt
docker compose restart newt
Tunnel Connected But Site Unreachable¶
Symptoms: Newt connected, but HTTPS requests timeout/fail
Diagnosis:
# Test tunnel endpoint
curl -I https://changemaker.pangolin.bnkserve.org
# Check Nginx logs
docker compose logs nginx | tail -50
# Verify resource target
curl -X GET https://api.bnkserve.org/v1/resources/<resource-id> \
-H "Authorization: Bearer $PANGOLIN_API_KEY"
Common Causes: - Resource target points to wrong service - Nginx not listening on port 80 - Firewall blocking Nginx → backend communication
Solution:
# Verify Nginx config
docker compose exec nginx nginx -t
# Check Nginx listening
docker compose exec nginx netstat -tulpn | grep :80
# Test backend from Nginx
docker compose exec nginx curl http://changemaker-v2-api:4000/api/health
SSL Certificate Errors¶
Symptoms: Browser shows "Certificate invalid" warning
Cause: Tunnel endpoint SSL certificate not trusted (rare).
Solution: Contact Pangolin support — tunnel provider manages SSL certificates.
Frequent Disconnects¶
Symptoms: Newt reconnects every few minutes
Diagnosis:
# Check for network issues
docker compose logs newt | grep -i disconnect
# Monitor connection
watch -n5 'docker compose logs --tail=1 newt'
Possible Causes:
- Network instability
- Container restarts (check docker compose ps)
- Resource limits (check docker stats newt-changemaker)
Solution:
# Increase restart backoff (if needed)
# Edit docker-compose.yml:
newt:
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
Migration from Cloudflare Tunnel¶
Retired Scripts (in scripts/legacy/):
- start-production.sh
- config.sh
- tunnel-config.sh
Migration Steps:
1. Stop Cloudflare tunnel: cloudflared service uninstall
2. Remove Cloudflare credentials: rm ~/.cloudflared/*.json
3. Setup Pangolin tunnel (see above)
4. Update DNS: Change CNAME from cloudflared.com to pangolin.bnkserve.org
5. Test new tunnel: curl https://changemaker.pangolin.bnkserve.org/api/health
6. Remove old scripts: rm scripts/legacy/*
Why Pangolin? - Self-hosted (privacy/control) - No Cloudflare dependency - Free/open source - API-driven management
Advanced Configuration¶
Custom Tunnel Domain¶
Requirement: Own domain with DNS control.
Steps:
1. Create endpoint with custom domain
2. Add DNS record: tunnel.cmlite.org CNAME pangolin.bnkserve.org.
3. Update PANGOLIN_ENDPOINT=https://tunnel.cmlite.org
4. Restart Newt
Multiple Sites¶
Use case: Staging + production on same tunnel.
Setup:
# Create second site
curl -X POST https://api.bnkserve.org/v1/sites \
-d '{"org_id":"...","name":"Staging"}'
# Create endpoint for staging
curl -X POST https://api.bnkserve.org/v1/endpoints \
-d '{"site_id":"...","subdomain":"staging-changemaker"}'
# Create resource pointing to staging Nginx
curl -X POST https://api.bnkserve.org/v1/resources \
-d '{"endpoint_id":"...","target":"http://nginx-staging:80"}'
Monitoring¶
Health Checks¶
Tunnel status:
# Container health
docker compose ps newt
# Connection logs
docker compose logs --tail=50 newt | grep -i connected
# Test public endpoint
curl -I https://changemaker.pangolin.bnkserve.org
Prometheus metrics (if enabled):
# API uptime through tunnel
curl https://changemaker.pangolin.bnkserve.org/api/metrics | grep cm_api_uptime
Related Documentation¶
- Docker Compose — Newt container configuration
- Nginx Configuration — Reverse proxy setup
- SSL/TLS — Certificate management (handled by tunnel)
- Environment Variables — Pangolin env vars