What is Cloudflare Tunnel?
Cloudflare Tunnel (formerly Argo Tunnel) is a free service from Cloudflare that lets you securely expose locally running services (web apps, SSH, databases, etc.) to the internet without needing:
- A public IP address
- Router port forwarding configuration
- Firewall inbound rules
- Exposure of your server’s real IP
The core principle runs a lightweight proxy process (cloudflared) on your server that establishes an outbound encrypted TLS connection to Cloudflare’s global edge network. When users visit your domain, requests go through Cloudflare edge nodes and are forwarded through the tunnel to your local service.
User → Cloudflare Edge Node → TLS Tunnel → cloudflared → Local Service
Why Choose Cloudflare Tunnel?
Comparison with Traditional Port Forwarding
| Feature | Port Forwarding | Cloudflare Tunnel |
|---|---|---|
| Requires Public IP | ✅ Yes | ❌ No |
| Router Configuration | ✅ Complex | ❌ Zero config |
| Real IP Exposed | ✅ Yes | ❌ Cloudflare IPs only |
| DDoS Protection | ❌ None | ✅ Automatic |
| HTTPS/SSL | ❌ Manual setup | ✅ Automatic |
| Cost | May have extra fees | Completely free |
| Security Model | Firewall rules | Zero-trust architecture |
Key Advantages
- Improved Security: Your server IP is completely hidden from the internet; Cloudflare’s DDoS protection kicks in automatically
- Zero-Config HTTPS: Cloudflare automatically issues and manages SSL certificates for your domain
- Works Across Networks: Works behind NAT, corporate firewalls, and residential broadband
- Multi-Service Support: One Tunnel can route multiple domains and sub-paths to different local services
- Global Acceleration: Leverages 100+ Cloudflare edge nodes for faster worldwide access
Prerequisites
Requirements
- A Cloudflare account (free tier available)
- A domain managed by Cloudflare DNS
- A Linux VPS or home server
- At least one local service to expose publicly (web app, NAS, Home Assistant, etc.)
Verify Domain on Cloudflare
Log into the Cloudflare Dashboard and confirm your domain is added and using Cloudflare nameservers.
Installing cloudflared
Ubuntu / Debian
# Add Cloudflare GPG key and repository
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg \
| sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] \
https://pkg.cloudflare.com/cloudflared $(lsb_release -cs) main" \
| sudo tee /etc/apt/sources.list.d/cloudflared.list
sudo apt update && sudo apt install -y cloudflared
CentOS / RHEL / Rocky Linux
sudo rpm -ivh https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-x86_64.rpm
Using Docker
# docker-compose.yml
version: "3.8"
services:
cloudflared:
image: cloudflare/cloudflared:latest
restart: unless-stopped
command: tunnel --no-autoupdate run
environment:
- TUNNEL_TOKEN=***
networks:
- tunnel-net
networks:
tunnel-net:
driver: bridge
Verify Installation
cloudflared --version
# Output: cloudflared version 2025.x.x (built ...)
Method 1: Using Cloudflare Zero Trust Dashboard (Recommended)
This is the simplest approach for most users.
Step 1: Create a Tunnel
- Log in to the Cloudflare Zero Trust Dashboard
- Navigate to Networks → Tunnels
- Click Create a tunnel
- Select Cloudflared as the connector type
- Enter a Tunnel name (e.g.,
my-server) and click Save tunnel
Step 2: Get the Installation Command
The Dashboard displays an install command based on your OS:
# Ubuntu / Debian
sudo cloudflared service install <TOKEN>
Where <TOKEN> is the long token string generated by the Dashboard.
Step 3: Configure Routing Rules
After installation, configure Public Hostnames in the Dashboard:
| Subdomain | Domain | Type | URL |
|---|---|---|---|
| blog | example.com | HTTP | http://localhost:8080 |
| admin | example.com | HTTP | http://localhost:3000 |
| files | example.com | HTTP | http://localhost:8090 |
Each rule maps a subdomain to a service running on your local ports.
Step 4: Verify
# Check Tunnel status
sudo systemctl status cloudflared
# View logs
sudo journalctl -u cloudflared -f
Method 2: Using YAML Config File (Advanced Users)
Ideal for version-controlled configurations and automated deployments.
Step 1: Authenticate and Create Tunnel
# Login with API Token
cloudflared tunnel login
# Create Tunnel
cloudflared tunnel create my-server
# Output: Tunnel credentials written to /root/.cloudflared/<UUID>.json
# Save the Tunnel UUID
Step 2: Configure YAML
# /etc/cloudflared/config.yaml
tunnel: <YOUR-TUNNEL-UUID>
credentials-file: /root/.cloudflared/<UUID>.json
ingress:
# Main website
- hostname: blog.example.com
service: http://localhost:8080
# Admin panel
- hostname: admin.example.com
service: http://localhost:3000
# File server
- hostname: files.example.com
service: http://localhost:8090
# Default rule (required)
- service: http_status:404
Step 3: Register DNS Records
# Add CNAME records for each subdomain
cloudflared tunnel route dns my-server blog.example.com
cloudflared tunnel route dns my-server admin.example.com
cloudflared tunnel route dns my-server files.example.com
Step 4: Start and Enable Auto-Start
# Test configuration
cloudflared tunnel --config /etc/cloudflared/config.yaml run
# Install as system service
sudo cloudflared service install
# Enable auto-start
sudo systemctl enable cloudflared
sudo systemctl start cloudflared
Advanced Configuration
Multi-Service Management with Docker Compose
# docker-compose.yml
version: "3.8"
services:
# Local services
blog:
image: wordpress:latest
ports:
- "8080:80"
volumes:
- ./wordpress:/var/www/html
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: secure_password_here
WORDPRESS_DB_NAME: wordpress
restart: unless-stopped
admin-panel:
image: portainer/portainer-ce:latest
ports:
- "3000:9000"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- portainer_data:/data
restart: unless-stopped
file-server:
image: filebrowser/filebrowser:latest
ports:
- "8090:80"
volumes:
- ./data:/srv
- filebrowser_db:/data/filebrowser.db
restart: unless-stopped
# Cloudflare Tunnel
cloudflared:
image: cloudflare/cloudflared:latest
restart: unless-stopped
command: tunnel --no-autoupdate run --token ${TUNNEL_TOKEN}
depends_on:
- blog
- admin-panel
- file-server
environment:
- TUNNEL_TOKEN=<FROM_DASHBOARD>
volumes:
portainer_data:
filebrowser_db:
Creating a Tunnel for SSH
# Add ingress rule
ingress:
- hostname: ssh.example.com
service: ssh://localhost:22
originRequest:
noTLSVerify: true
Access SSH through the Tunnel:
ssh -o ProxyCommand="cloudflared access ssh --hostname ssh.example.com" user@example.com
Health Checks and Monitoring
# Add monitoring endpoint
ingress:
- hostname: monitor.example.com
service: http://localhost:9090 # Prometheus/Grafana
Using CNAME Instead of Subdomains
If you don’t want new subdomains, bind the Tunnel to an existing domain’s CNAME:
# DNS record
_cname.cfargotunnel.com. CNAME <tunnel-uuid>.cfargotunnel.com.
Then match routes with:
ingress:
- hostname: "*.example.com"
service: http://localhost:8080
Security Best Practices
1. Enable Access Policies
Add access policies to your Tunnel via Cloudflare Zero Trust:
- Admin pages: Require corporate email or MFA verification
- Internal tools: Restrict to specific IP ranges or device compliance
- Public services: Allow unauthenticated access
Path: Zero Trust → Access → Applications → Add an Application
2. Minimize Attack Surface
Only expose services that truly need public access:
ingress:
# ✅ Needs public access
- hostname: app.example.com
service: http://localhost:8080
# ❌ Don't expose databases, Redis, etc.
# Even if they're on localhost
3. Rotate Tokens Regularly
# Regenerate Tunnel Token in Dashboard
# Then update config and restart
sudo systemctl restart cloudflared
4. Monitor Tunnel Status
# Check Tunnel connection status
cloudflared tunnel info my-server
# View real-time logs
journalctl -u cloudflared -f --output=json | jq '.'
Troubleshooting
Issue 1: Unstable Tunnel Connection
# Increase connection grace period
cloudflared tunnel run --grace-period 120s
# Check network quality
mtr -r -c 100 cloudflaretunnel.com
Issue 2: DNS Resolution Failure
# Verify DNS records
dig blog.example.com CNAME
# Check if Tunnel has registered DNS
cloudflared tunnel info my-server
Issue 3: 502 Bad Gateway
# Check if local service is running
curl http://localhost:8080
# Check cloudflared logs
sudo journalctl -u cloudflared -n 50
Common causes:
- Local service not started
- Wrong port configuration
- Firewall blocking localhost communication (rare)
Issue 4: Certificate Issues
Cloudflare Tunnel manages certificates automatically. If problems arise:
# Clear cached certificates
rm -rf /root/.cloudflared/*.json
# Re-authenticate
cloudflared tunnel login
Performance Optimization
Enable HTTP/2 and HTTP/3
ingress:
- hostname: app.example.com
service: https://localhost:8443
originRequest:
http2Origin: true
Enable Caching
Configure cache rules in the Cloudflare Dashboard for Tunnel domains:
Rules → Transform Rules → Cache Rules
Example Cache Rule:
- Match: URL path ends with .css, .js, .png, .jpg
- Cache Level: Cache Everything
- Edge TTL: 1 month
Bandwidth Optimization
originRequest:
connectTimeout: 30s
noHappyEyeballs: false
keepAliveConnections: 100
keepAliveTimeout: 90s
Summary
Cloudflare Tunnel is currently the most elegant solution for exposing local services, especially suited for:
- Home lab / Homelab enthusiasts
- Users without public IP addresses
- Scenarios requiring quick deployment and teardown
- Self-hosted projects prioritizing security
With Tunnel, you can serve any local service to a global audience without compromising security.
Read more: Cloudflare Tunnel Official Documentation
