10 KiB

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:


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):

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