Pangolin vs Cloudflare Tunnel:
Self-Hosted Tunnelling on Your Own Terms

Cloudflare Tunnel makes exposing self-hosted services to the internet almost trivially easy - no open ports, no static IPs, free SSL. The catch: your traffic flows through Cloudflare's infrastructure. Pangolin is the open-source, self-hosted alternative that gives you the same experience on a server you control. Here's how they compare, and a complete installation guide to get Pangolin running with Docker Compose.

What Is Cloudflare Tunnel?

Cloudflare Tunnel (formerly Argo Tunnel) lets you expose services running on a private server to the internet without opening any inbound firewall ports. A lightweight daemon called cloudflared runs on your server and establishes an outbound encrypted connection to Cloudflare's edge network. Traffic destined for your domain enters Cloudflare's network and is routed through that persistent connection to your service.

The appeal is real: no public IP required, no NAT traversal headaches, automatic SSL certificates, DDoS protection built in, and a generous free tier. For homelab users and small teams, it removes a significant amount of infrastructure complexity.

The trade-off is equally real: Cloudflare sits between your users and your services. All traffic - including HTTPS content - is decrypted at Cloudflare's edge before being re-encrypted to your origin. You are trusting a third party with your traffic, your domain, and your availability. For many workloads that's perfectly acceptable. For others - sensitive internal tools, regulated data, or anyone who simply values sovereignty - it is not.

What Is Pangolin?

Pangolin (github.com/fosrl/pangolin) is an open-source, self-hosted tunnelling platform that replicates the Cloudflare Tunnel experience on infrastructure you own. Instead of routing traffic through Cloudflare's network, you run your own control plane on a VPS with a public IP. Remote services - your homelab, an office server, a device behind CGNAT - connect back to that VPS via WireGuard tunnels. Traefik handles TLS termination and reverse proxying on the VPS side.

The stack has three components:

  • Pangolin - the control plane. Manages sites, resources, users, and exposes an API that Traefik polls for routing configuration.
  • Gerbil - the WireGuard gateway. Runs on the VPS and maintains the WireGuard tunnels to your remote nodes.
  • Traefik - the reverse proxy and TLS terminator. Pulls dynamic configuration from Pangolin's API and handles Let's Encrypt certificate issuance.

On the remote side, a lightweight agent (also called Newt) connects outbound to Gerbil over WireGuard, establishing the tunnel. No inbound ports need to be opened on the remote machine - the same model as Cloudflare Tunnel, but with your VPS as the edge.

ℹ️
Note on naming. The project is spelled Pangolin (the scaly anteater), not "pengolin". The GitHub organisation is fosrl and the image is docker.io/fosrl/pangolin.

Architecture Comparison

Both tools solve the same problem - securely exposing a private service to the internet - but with fundamentally different trust models.

Aspect Cloudflare Tunnel Pangolin
Traffic routing Via Cloudflare's global edge Via your own VPS
TLS termination At Cloudflare's edge On your VPS (Traefik + Let's Encrypt)
Tunnel protocol QUIC / HTTP/2 (proprietary) WireGuard
Requires public IP No (on origin) Yes (on VPS), not on origin
Open source No (cloudflared client only) Yes (fully open source)
Self-hostable No Yes
Free tier Yes (generous) Yes (self-hosted, VPS cost only)
DDoS protection Yes (Cloudflare's network) Partial (depends on VPS provider)
Web dashboard Yes (Zero Trust dashboard) Yes (Pangolin UI)
Access policies (SSO, MFA) Yes (Cloudflare Access) Yes (built into Pangolin)

Key Differences

Data Sovereignty and Privacy

This is the deciding factor for most teams evaluating the two options. With Cloudflare Tunnel, all traffic - even traffic marked as end-to-end encrypted at the application layer - is decrypted at Cloudflare's edge so that Cloudflare can inspect headers, apply WAF rules, and make routing decisions. Cloudflare's privacy practices are generally well-regarded in the industry, but the fundamental fact remains: a third party has access to your plaintext HTTP traffic.

With Pangolin, TLS terminates on a server you control. If you're exposing internal tools, a self-hosted Bitwarden instance, a home automation dashboard, or anything containing personal data, the traffic never leaves infrastructure you own.

Cost Model

Cloudflare Tunnel is free for most use cases. The Zero Trust free tier covers up to 50 users, unlimited tunnels, and unlimited bandwidth. Advanced features (browser isolation, CASB, DLP) are paid. For purely exposing services, Cloudflare is effectively free indefinitely.

Pangolin's cost is your VPS. A small instance - 1 vCPU, 1 GB RAM - from Hetzner, DigitalOcean, or Contabo is sufficient for dozens of tunnelled services and costs roughly €3–5/month. If you're already running a VPS (and if you're reading this blog, you probably are), the marginal cost of adding Pangolin is essentially zero.

Performance and Latency

Cloudflare's edge network has over 300 PoPs worldwide. Traffic from a user in Tokyo to your service in Germany will typically route through the nearest Cloudflare PoP in Asia, then across Cloudflare's private backbone to the German PoP, then back to your server via the tunnel. This can actually be faster than a direct connection if Cloudflare's backbone outperforms the public internet path.

Pangolin routes through a single VPS. The VPS location matters: choose a data centre close to the majority of your users. For personal or internal use where you and your users are geographically concentrated, this is rarely a disadvantage in practice.

Protocol Support

Cloudflare Tunnel supports HTTP/HTTPS traffic natively, plus TCP and UDP via the WARP client (with restrictions). Raw TCP tunnelling for arbitrary services requires the Cloudflare WARP app on the client side.

Pangolin tunnels are WireGuard-based, so any TCP or UDP traffic can be forwarded. HTTPS is the primary use case via Traefik, but the underlying WireGuard mesh can carry any protocol. This makes Pangolin more flexible for non-HTTP services like game servers, RTSP streams, or custom TCP services.

Setup Complexity

Cloudflare Tunnel wins on simplicity. Install cloudflared, run one command to authenticate, define a YAML config, and your service is live within minutes. The managed control plane means zero infrastructure to maintain.

Pangolin requires a VPS, a domain, and a willingness to run a Docker Compose stack. The initial setup takes 20–30 minutes. Ongoing maintenance is low - the stack updates like any Docker service - but it is infrastructure you are responsible for. The Pangolin dashboard is polished and makes adding new tunnelled resources straightforward after initial setup.

When to Choose Cloudflare Tunnel

  • You want zero infrastructure overhead and are comfortable with Cloudflare as a trusted intermediary
  • You need a global CDN and DDoS protection as part of the same package
  • You're exposing public-facing services where Cloudflare's WAF and bot management are valuable
  • You don't have or want a VPS
  • Speed of setup matters more than data sovereignty

When to Choose Pangolin

  • You need full control over your traffic and TLS termination
  • You're exposing sensitive internal tools (password managers, home automation, internal dashboards)
  • You operate in a regulated environment where third-party traffic inspection is not acceptable
  • You want to tunnel non-HTTP protocols without client-side workarounds
  • You already have a VPS and want to consolidate infrastructure
  • You prefer open-source software with a community you can inspect and contribute to

Installing Pangolin with Docker Compose

The following is a complete, production-ready Pangolin installation using Docker Compose. You need a Linux VPS with Docker and Docker Compose installed, a domain name pointing to your VPS's IP, and root access.

⚠️
DNS first. Point your domain (e.g. tunnel.example.com) to your VPS's public IP before starting the stack. Let's Encrypt's HTTP challenge requires DNS to resolve correctly.

Step 1 - Create the Directory Structure

Create the required directory structure. Pangolin expects this layout - don't change it.

mkdir -p /opt/pangolin/config/traefik \
         /opt/pangolin/config/db \
         /opt/pangolin/config/letsencrypt \
         /opt/pangolin/config/logs

cd /opt/pangolin

Your working directory will contain:

/opt/pangolin/
├── config/
│   ├── config.yml          ← create manually
│   ├── db/
│   │   └── db.sqlite       ← auto-generated
│   ├── key                 ← auto-generated (WireGuard key)
│   ├── letsencrypt/
│   │   └── acme.json       ← auto-generated
│   ├── logs/
│   └── traefik/
│       ├── traefik_config.yml   ← create manually
│       └── dynamic_config.yml  ← create manually
└── docker-compose.yml      ← create manually

Step 2 - docker-compose.yml

Create /opt/pangolin/docker-compose.yml. The stack has three services: Pangolin (control plane), Gerbil (WireGuard gateway), and Traefik (reverse proxy). Gerbil owns all the exposed ports so that Traefik can share its network namespace.

name: pangolin
services:
  pangolin:
    image: docker.io/fosrl/pangolin:latest
    container_name: pangolin
    restart: unless-stopped
    volumes:
      - ./config:/app/config
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"]
      interval: "10s"
      timeout: "10s"
      retries: 15

  gerbil:
    image: docker.io/fosrl/gerbil:latest
    container_name: gerbil
    restart: unless-stopped
    depends_on:
      pangolin:
        condition: service_healthy
    command:
      - --reachableAt=http://gerbil:3004
      - --generateAndSaveKeyTo=/var/config/key
      - --remoteConfig=http://pangolin:3001/api/v1/
    volumes:
      - ./config/:/var/config
    cap_add:
      - NET_ADMIN
      - SYS_MODULE
    ports:
      - 51820:51820/udp
      - 21820:21820/udp
      - 443:443
      - 80:80

  traefik:
    image: docker.io/traefik:v3.6
    container_name: traefik
    restart: unless-stopped
    network_mode: service:gerbil
    depends_on:
      pangolin:
        condition: service_healthy
    command:
      - --configFile=/etc/traefik/traefik_config.yml
    volumes:
      - ./config/traefik:/etc/traefik:ro
      - ./config/letsencrypt:/letsencrypt
      - ./config/traefik/logs:/var/log/traefik

networks:
  default:
    driver: bridge
    name: pangolin
ℹ️
network_mode: service:gerbil means Traefik shares Gerbil's network namespace. Ports 80 and 443 appear on the Gerbil container so that WireGuard traffic and HTTPS traffic are handled by the same network interface. This is intentional - don't change it.

Step 3 - Traefik Static Configuration

Create config/traefik/traefik_config.yml. Replace [email protected] with your real email address for Let's Encrypt notifications.

api:
  insecure: true
  dashboard: true

providers:
  http:
    endpoint: "http://pangolin:3001/api/v1/traefik-config"
    pollInterval: "5s"
  file:
    filename: "/etc/traefik/dynamic_config.yml"

experimental:
  plugins:
    badger:
      moduleName: "github.com/fosrl/badger"
      version: "v1.3.1"

log:
  level: "INFO"
  format: "common"
  maxSize: 100
  maxBackups: 3
  maxAge: 3
  compress: true

certificatesResolvers:
  letsencrypt:
    acme:
      httpChallenge:
        entryPoint: web
      email: "[email protected]"   # REPLACE WITH YOUR EMAIL
      storage: "/letsencrypt/acme.json"
      caServer: "https://acme-v02.api.letsencrypt.org/directory"

entryPoints:
  web:
    address: ":80"
  websecure:
    address: ":443"
    transport:
      respondingTimeouts:
        readTimeout: "30m"
    http:
      tls:
        certResolver: "letsencrypt"
      encodedCharacters:
        allowEncodedSlash: true
        allowEncodedQuestionMark: true

serversTransport:
  insecureSkipVerify: true

ping:
  entryPoint: "web"

Step 4 - Traefik Dynamic Configuration

Create config/traefik/dynamic_config.yml. Replace pangolin.example.com with your actual domain in all four places.

http:
  middlewares:
    badger:
      plugin:
        badger:
          disableForwardAuth: true
    redirect-to-https:
      redirectScheme:
        scheme: https

  routers:
    main-app-router-redirect:
      rule: "Host(`pangolin.example.com`)"   # REPLACE
      service: next-service
      entryPoints:
        - web
      middlewares:
        - redirect-to-https
        - badger

    next-router:
      rule: "Host(`pangolin.example.com`) && !PathPrefix(`/api/v1`)"   # REPLACE
      service: next-service
      entryPoints:
        - websecure
      middlewares:
        - badger
      tls:
        certResolver: letsencrypt

    api-router:
      rule: "Host(`pangolin.example.com`) && PathPrefix(`/api/v1`)"   # REPLACE
      service: api-service
      entryPoints:
        - websecure
      middlewares:
        - badger
      tls:
        certResolver: letsencrypt

    ws-router:
      rule: "Host(`pangolin.example.com`)"   # REPLACE
      service: api-service
      entryPoints:
        - websecure
      middlewares:
        - badger
      tls:
        certResolver: letsencrypt

  services:
    next-service:
      loadBalancer:
        servers:
          - url: "http://pangolin:3002"   # Next.js dashboard

    api-service:
      loadBalancer:
        servers:
          - url: "http://pangolin:3000"   # API / WebSocket

tcp:
  serversTransports:
    pp-transport-v1:
      proxyProtocol:
        version: 1
    pp-transport-v2:
      proxyProtocol:
        version: 2

Step 5 - Pangolin Application Config

Create config/config.yml. At minimum, set your domain and an SMTP server for user invitations. Consult the official Pangolin configuration guide for the full set of options - the defaults are sensible for most deployments.

# config/config.yml - minimal example
app:
  dashboard_url: "https://pangolin.example.com"   # REPLACE
  log_level: "info"

server:
  external_port: 443
  internal_port: 3001
  next_port: 3002
  internal_hostname: "pangolin"
  session_cookie_name: "p_session_token"
  resource_access_token_param: "p_token"
  resource_access_token_headers:
    - name: "P-Access-Token"
      value: "{token}"

gerbil:
  start_port: 51820
  base_endpoint: "pangolin.example.com"   # REPLACE
  remote_address: "gerbil:3004"
  make_accessible_on_same_subnet: false

traefik:
  http_entrypoint: "web"
  https_entrypoint: "websecure"

rate_limits:
  global:
    window_minutes: 1
    max_requests: 500

Step 6 - Start the Stack

sudo docker compose up -d

# Watch the logs - Gerbil will generate the WireGuard key,
# Traefik will obtain the Let's Encrypt certificate.
sudo docker compose logs -f

# Verify all containers are healthy
sudo docker compose ps

After a minute or two, all three services should report Up status. Let's Encrypt certificate issuance takes 30–60 seconds on first startup.

Step 7 - Initial Setup

Navigate to https://pangolin.example.com/auth/initial-setup in your browser. The setup wizard creates your first admin user. After completing the wizard, log in to the Pangolin dashboard to create your first organisation, site, and tunnelled resource.

Adding remote nodes. From the Pangolin dashboard, create a new Site and download the Newt agent for your remote machine. Run the Newt agent on the remote host - it connects outbound to Gerbil over WireGuard. No inbound ports need to be opened on the remote machine.

Recommendation

Choose Cloudflare Tunnel if you want zero infrastructure overhead, need a global CDN as part of the same package, and are comfortable with Cloudflare as a trusted intermediary. For most public-facing homelab and small-team use cases, it's an excellent, genuinely free option.

Choose Pangolin if data sovereignty matters, you're exposing internal or sensitive services, you want full control over TLS termination, or you need to tunnel non-HTTP protocols. The initial setup requires more effort than Cloudflare Tunnel, but once running it's lightweight, self-contained, and fully under your control. Running on a €4/month VPS, the ongoing cost is trivially low compared to equivalent managed services.

The two tools are not mutually exclusive - a common pattern is to use Cloudflare Tunnel for public marketing sites and Pangolin for internal tooling, combining the CDN benefits of Cloudflare's edge with the privacy of a self-hosted tunnel for sensitive workloads.

Services Technologies Process Blog Get in Touch