Skip to content

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:

Internet → Pangolin Tunnel (pangolin.bnkserve.org) → Newt Container → Nginx → Services

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

docker compose up -d newt

Stop Tunnel

docker compose stop newt

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)

docker compose up -d --force-recreate newt

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