Why Cloudflare Tunnel?
In traditional self-hosting setups, exposing a local service to the internet typically requires:
- A public IP address (many residential ISPs and basic VPS plans don’t provide one)
- Port forwarding on your router
- Manual SSL certificate management
- Direct exposure to internet threats (CC attacks, DDoS, port scanning)
Cloudflare Tunnel (formerly Argo Tunnel) changes all of this:
Your server never needs to expose any open ports. Cloudflare establishes an encrypted tunnel from its edge network to your server, with all traffic routed through Cloudflare’s global CDN.
Key Advantages
| Feature | Traditional Port Forwarding | Cloudflare Tunnel |
|---|---|---|
| Public IP Required | ✅ Yes | ❌ No |
| Ports Exposed | All forwarded ports | Zero ports |
| SSL Certificates | Manual management | Free & automatic |
| DDoS Protection | Requires separate solution | Free Cloudflare CDN |
| Firewall | Complex configuration | Zero Trust policies |
| Cost | Port forwarding is free | Free tier available |
Architecture Overview
┌─────────────┐ HTTPS ┌──────────────────┐ Encrypted Tunnel ┌──────────────────┐
│ User Browser│ ──────────────→│ Cloudflare CDN │ ────────────────────────→│ Cloudflared Agent│
│ │ Edge │ (WAF/CDN/Security)│ │ (on your server) │
└─────────────┘ └──────────────────┘ └────────┬─────────┘
│
▼
┌──────────────────┐
│ Nginx (local) │
│ :80 / :443 │
└──────────────────┘
Prerequisites
- A Cloudflare account (free tier is sufficient)
- A domain managed through Cloudflare
- A VPS or local server (Ubuntu 22.04+ used in examples)
- Docker installed (optional; binary installation also covered)
Step 1: Create a Cloudflare Tunnel
1. Create Tunnel in Cloudflare Dashboard
Log in to the Cloudflare Zero Trust Dashboard, navigate to Networks → Tunnels, and click Create a tunnel.
Select cloudflared as the connector type. You’ll receive a Tunnel Token — save it!
# Save this token — you'll need it in the next step
TUNNEL_TOKEN="eyJhIjoiY2YtMTIz...truncated..."
2. Register the Tunnel with the Token
# Install cloudflared
curl -fsSL https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared
chmod +x /usr/local/bin/cloudflared
# Create a tunnel connection using the token
cloudflared tunnel --no-autoupdate create selfvps-tunnel
# Get the Tunnel ID
TUNNEL_ID=$(cat ~/.cloudflared/selfvps-tunnel.json | jq -r .TunnelID)
echo "Tunnel ID: $TUNNEL_ID"
Step 2: Configure Nginx Local Service
1. Install Nginx
sudo apt update && sudo apt install -y nginx
sudo systemctl enable --now nginx
2. Configure Nginx Server Block
Create /etc/nginx/sites-available/selfvps.net:
server {
listen 80;
server_name selfvps.net www.selfvps.net;
root /var/www/selfvps.net;
index index.html;
# Basic security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
location / {
try_files $uri $uri/ =404;
}
# Static resource caching
location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff2)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
# Deny access to hidden files
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
}
Enable the site:
sudo ln -s /etc/nginx/sites-available/selfvps.net /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
3. Verify Local Nginx is Working
curl -I http://localhost
# Should return 200 OK
Step 3: Configure Tunnel Routing
1. Create DNS Records
Add a CNAME record in Cloudflare DNS:
Hostname: www.selfvps.net
Type: CNAME
Target: <TUNNEL_ID>.cfargotunnel.com
Or manage DNS through Cloudflared directly:
cloudflared tunnel --no-autoupdate route dns selfvps-tunnel www.selfvps.net
cloudflared tunnel --no-autoupdate route dns selfvps-tunnel selfvps.net
2. Create Tunnel Configuration File
sudo mkdir -p /etc/cloudflared
sudo tee /etc/cloudflared/config.yaml > /dev/null << 'EOF'
tunnel: selfvps-tunnel
credentials-file: /etc/cloudflared/<TUNNEL_ID>.json
protocol: http2
# Don't auto-update
no-autoupdate: true
ingress:
# Main site
- hostname: www.selfvps.net
service: http://localhost:80
# Blog subdomain
- hostname: blog.selfvps.net
service: http://localhost:80
# Admin panel (source-restricted)
- hostname: admin.selfvps.net
service: http://localhost:8080
originRequest:
noTLSVerify: false
originServerName: admin.selfvps.net
# Default 404
- service: http_status:404
EOF
Security Note: Ensure the
<TUNNEL_ID>.jsonfile has strict permissions:sudo chmod 600 /etc/cloudflared/<TUNNEL_ID>.json sudo chown root:root /etc/cloudflared/<TUNNEL_ID>.json
3. Use Native Cloudflare DNS Management
If you prefer not to let Cloudflared manage DNS records, add CNAME records manually in the Cloudflare dashboard. This is the recommended approach for production environments.
Step 4: Start the Tunnel Service
Using Systemd (Recommended)
# Create systemd service
sudo tee /etc/systemd/system/cloudflared.service > /dev/null << 'EOF'
[Unit]
Description=Cloudflare Tunnel
After=network-online.target
Wants=network-online.target
[Service]
Type=notify
ExecStart=/usr/local/bin/cloudflared --no-autoupdate tunnel --config /etc/cloudflared/config.yaml run
Restart=on-failure
RestartSec=5
KillMode=mixed
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now cloudflared
sudo systemctl status cloudflared
Or Using Docker
docker run -d --name cloudflared-tunnel \
--restart unless-stopped \
-v /etc/cloudflared:/etc/cloudflared \
cloudflare/cloudflared:latest \
tunnel --no-autoupdate run
Step 5: Configure Cloudflare Security Policies
1. Enable WAF (Web Application Firewall)
In the Cloudflare dashboard, go to Security → WAF:
- Enable Super Bot Fight Mode (free)
- Create custom rules for critical pages
# Example: Restrict API paths to specific HTTP methods
http.request.uri contains "/api" and not http.request.method in {"GET", "POST"} → Block
# Example: Challenge on login pages
http.request.uri contains "/admin/login" → Challenge
2. Configure Caching Rules
In Caching → Configuration:
Cache Level: Basic
Cache Everything: Off (for dynamic content)
Browser TTL: 2 hours
Edge TTL: 4 hours
For static assets:
Cache Rule:
Path: /*.{js,css,png,jpg,jpeg,gif,svg,ico,woff,woff2}
Cache Level: Cache Everything
Browser TTL: 1 month
Edge TTL: 1 week
Override Host: {origin}
3. Set Up Firewall Rules
Security → WAF → Managed Headers — Enable all options
Security → WAF → Managed Filters — Enable:
- Cloudflare Essential Rules
- Cloudflare OWASP Core Ruleset
4. Enable Always Use HTTPS
In SSL/TLS → Overview:
- Encryption mode: Full (since cloudflared to origin is HTTP)
- Always Use HTTPS: On
Step 6: Advanced Hardening
1. Bind Nginx to Loopback Only
# Only listen on 127.0.0.1, reject external connections
server {
listen 127.0.0.1:80;
server_name _;
# ... rest of config
}
sudo nginx -t && sudo systemctl reload nginx
2. Configure UFW Firewall
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw allow 7844/tcp # cloudflared outbound (usually unnecessary)
sudo ufw enable
3. Enable Cloudflare Access Rules
In Zero Trust → Access → Applications:
- Add a Private Application
- Enter your subdomain (e.g.,
admin.selfvps.net) - Configure Access Policy:
- Require → One or more of these → specify allowed email domains
- Or require Cloudflare Access authentication
Only authorized users can now access the admin panel.
4. Configure Rate Limiting
# Max 100 requests per 10 seconds
http.request.uri path eq "/" and http.request.method eq "GET"
Rate limit: 100 requests per 10 seconds
Action: Challenge
Troubleshooting
Tunnel Disconnected
# Check cloudflared logs
journalctl -u cloudflared -f --no-pager
# Common issues:
# 1. DNS resolution failure → check network connectivity
# 2. Token expired → regenerate tunnel token
# 3. Firewall blocking → check outbound ports 7844/443
502 Bad Gateway
Usually indicates Nginx is not running or misconfigured:
sudo systemctl status nginx
sudo nginx -t
sudo journalctl -u nginx -n 50
SSL Certificate Issues
Cloudflare manages certificates automatically. If the certificate isn’t生效:
- Verify DNS records correctly point to the Tunnel
- SSL mode should be set to Full
- Clear browser cache and retry
Monitoring and Alerting
Health Checks
Add an origin health check in the Cloudflare dashboard:
- URL:
http://127.0.0.1:80/health - Interval: 60 seconds
- Timeout: 10 seconds
Monitor cloudflared Status
# Simple health check script
cat > /usr/local/bin/check-tunnel.sh << 'EOF'
#!/bin/bash
if ! systemctl is-active --quiet cloudflared; then
curl -s -X POST "https://hooks.slack.com/services/YOUR/WEBHOOK" \
-d '{"text":"🚨 Cloudflared tunnel is DOWN!"}'
sudo systemctl restart cloudflared
fi
EOF
chmod +x /usr/local/bin/check-tunnel.sh
# Add to crontab, check every minute
(crontab -l 2>/dev/null; echo "* * * * * /usr/local/bin/check-tunnel.sh") | crontab -
Summary
With Cloudflare Tunnel, you can:
- ✅ Zero port exposure — No ports open to the internet on your server
- ✅ Automatic HTTPS — Cloudflare manages SSL certificates for free
- ✅ CDN acceleration — Global edge caching for static resources
- ✅ DDoS protection — Free Cloudflare CDN layer protection
- ✅ Zero-trust access — Admin panel accessible only to authorized users
- ✅ No public IP needed — Deploy from any network environment
This setup is ideal for: blogs, documentation sites, admin panels, internal tools, CI/CD runners, and more.
This guide covers the complete journey from zero to production-ready. If your scenario has special requirements (WebSocket support, load balancing, multi-region deployment), feel free to discuss in the comments.
