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.
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.
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
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.
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.