π Table of Contents
- What is Cloudflare Tunnel?
- Why Use Cloudflare Tunnel?
- Prerequisites
- Installation
- Quick Start
- Configuration
- Advanced Usage
- Use Cases
- Troubleshooting
- Best Practices
- FAQ
What is Cloudflare Tunnel?
Cloudflare Tunnel (formerly Argo Tunnel) creates a secure, encrypted tunnel between your local machine and Cloudflare's global network. It allows you to expose your localhost to the internet without:
- Opening ports on your router
- Exposing your home IP address
- Complex firewall configurations
- Port forwarding setup
How it works:
[Your Local App] β [Cloudflared Client] β [Cloudflare Edge] β [Users Worldwide]
All connections are outbound-only from your machine, making it inherently more secure than traditional hosting methods.
Why Use Cloudflare Tunnel?
π Security Benefits
- β No exposed home IP address
- β Automatic SSL/TLS encryption
- β Built-in DDoS protection
- β No inbound firewall ports needed
- β Zero-trust network access
π Practical Advantages
- β Share local work instantly with clients/teams
- β Host personal projects without traditional hosting costs
- β Test webhooks requiring public URLs
- β Deploy from behind restrictive firewalls/NAT
- β Access from anywhere without VPN
π° Cost Effective
- β Free for personal use
- β No VPS or hosting fees required
- β Enterprise-grade infrastructure at no cost
Prerequisites
Before you begin, ensure you have:
- [ ] A Cloudflare account (Sign up free)
- [ ] A domain name added to Cloudflare
- [ ] A local application running (any port)
- [ ] Basic command line knowledge
- [ ] Administrator/sudo access on your machine
Installation
macOS
Using Homebrew:
brew install cloudflare/cloudflare/cloudflared
Linux
Debian/Ubuntu:
wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
sudo dpkg -i cloudflared-linux-amd64.deb
RHEL/CentOS:
wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-x86_64.rpm
sudo rpm -i cloudflared-linux-x86_64.rpm
Arch Linux:
yay -S cloudflared
Windows
- Download the latest
.exefrom GitHub Releases - Place it in a directory in your PATH (e.g.,
C:\Windows\System32)
Verify Installation
cloudflared --version
Expected output: cloudflared version 20XX.X.X
Quick Start
Step 1: Authenticate
cloudflared tunnel login
This opens your browser to authorize cloudflared with your Cloudflare account. Select your domain when prompted.
Step 2: Create a Tunnel
cloudflared tunnel create my-tunnel
Output:
Tunnel credentials written to /Users/username/.cloudflared/a7b3c4d5-e6f7-8901-2345-6789abcdef01.json
Created tunnel my-tunnel with id a7b3c4d5-e6f7-8901-2345-6789abcdef01
π Save the tunnel ID - you'll need it for configuration.
Example paths by OS:
- macOS:
/Users/john/.cloudflared/a7b3c4d5-e6f7-8901-2345-6789abcdef01.json - Linux:
/home/john/.cloudflared/a7b3c4d5-e6f7-8901-2345-6789abcdef01.json - Windows:
C:\Users\John\.cloudflared\a7b3c4d5-e6f7-8901-2345-6789abcdef01.json
Step 3: Create Configuration File
Create ~/.cloudflared/config.yml:
tunnel: my-tunnel
credentials-file: /home/username/.cloudflared/a7b3c4d5-e6f7-8901-2345-6789abcdef01.json
ingress:
- hostname: app.yourdomain.com
service: http://localhost:3000
- service: http_status:404
Real-world example:
tunnel: my-blog-tunnel
credentials-file: /home/john/.cloudflared/a7b3c4d5-e6f7-8901-2345-6789abcdef01.json
ingress:
- hostname: blog.jobayer.me
service: http://localhost:4000
- service: http_status:404
Replace:
-
my-tunnelβ your tunnel name (e.g.,my-blog-tunnel) -
a7b3c4d5-e6f7-8901-2345-6789abcdef01.jsonβ your actual tunnel credentials file -
app.yourdomain.comβ your desired subdomain (e.g.,blog.jobayer.me) -
localhost:3000β your local app address (e.g.,localhost:4000for Hugo)
Configuration file locations:
- macOS/Linux:
~/.cloudflared/config.yml - Windows:
C:\Users\YourUsername\.cloudflared\config.yml
Step 4: Configure DNS
cloudflared tunnel route dns my-tunnel app.yourdomain.com
This creates a CNAME record pointing to your tunnel.
Real-world example:
cloudflared tunnel route dns my-blog-tunnel blog.jobayer.me
Output:
Created CNAME route for blog.jobayer.me
Route created successfully
What this does:
- Creates a CNAME record in your Cloudflare DNS
- Points
blog.jobayer.meto your tunnel endpoint (e.g.,a7b3c4d5-e6f7-8901-2345-6789abcdef01.cfargotunnel.com) - Makes your tunnel accessible via the friendly domain name
Verify in Cloudflare Dashboard:
- Go to your domain in Cloudflare Dashboard
- Click "DNS" in the sidebar
- You should see a CNAME record like:
- Name:
blog - Target:
a7b3c4d5-e6f7-8901-2345-6789abcdef01.cfargotunnel.com
- Name:
Step 5: Start the Tunnel
cloudflared tunnel run my-tunnel
Output:
2024-12-05T10:30:15Z INF Starting tunnel tunnelID=a7b3c4d5-e6f7-8901-2345-6789abcdef01
2024-12-05T10:30:16Z INF Connection registered connIndex=0 location=IAD
2024-12-05T10:30:17Z INF Connection registered connIndex=1 location=ATL
2024-12-05T10:30:18Z INF Connection registered connIndex=2 location=ORD
2024-12-05T10:30:19Z INF Connection registered connIndex=3 location=DFW
What this means:
- β Tunnel is running successfully
- β Connected to 4 Cloudflare edge locations (IAD, ATL, ORD, DFW)
- β Your local app is now accessible globally
π Your app is now live at https://blog.jobayer.me!
Testing your tunnel:
# From another terminal or device:
curl https://blog.jobayer.me
# Or open in browser:
# https://blog.jobayer.me
Real-world example:
If you're running a Next.js dev server on port 3000 locally, anyone can now access it at https://blog.jobayer.me with automatic HTTPS!
Keep it running:
- Keep the terminal window open, or
- Press
Ctrl+Cto stop, or - Set it up as a system service (see Advanced Usage section)
Configuration
Basic Configuration Structure
tunnel: <tunnel-name>
credentials-file: <path-to-credentials>
ingress:
- hostname: <your-hostname>
service: <local-service>
- service: http_status:404 # Catch-all rule (required)
Multiple Services
Route different subdomains to different local services:
tunnel: my-tunnel
credentials-file: /home/user/.cloudflared/a7b3c4d5-e6f7-8901-2345-6789abcdef01.json
ingress:
# Frontend application
- hostname: app.yourdomain.com
service: http://localhost:3000
# Backend API
- hostname: api.yourdomain.com
service: http://localhost:8080
# Blog
- hostname: blog.yourdomain.com
service: http://localhost:4000
# Admin panel
- hostname: admin.yourdomain.com
service: http://localhost:5000
# Catch-all (required)
- service: http_status:404
Real-world example - Full Stack Application:
tunnel: fullstack-dev
credentials-file: /home/john/.cloudflared/a7b3c4d5-e6f7-8901-2345-6789abcdef01.json
ingress:
# React frontend (running on Vite dev server)
- hostname: app.jobayer.me
service: http://localhost:5173
# Node.js/Express API
- hostname: api.jobayer.me
service: http://localhost:3000
# PostgreSQL admin (pgAdmin)
- hostname: db.jobayer.me
service: http://localhost:5050
# Storybook component library
- hostname: storybook.jobayer.me
service: http://localhost:6006
- service: http_status:404
DNS Setup:
After creating this config, run these commands:
cloudflared tunnel route dns fullstack-dev app.jobayer.me
cloudflared tunnel route dns fullstack-dev api.jobayer.me
cloudflared tunnel route dns fullstack-dev db.jobayer.me
cloudflared tunnel route dns fullstack-dev storybook.jobayer.me
Result:
-
https://app.jobayer.meβ React app on localhost:5173 -
https://api.jobayer.meβ Express API on localhost:3000 -
https://db.jobayer.meβ pgAdmin on localhost:5050 -
https://storybook.jobayer.meβ Storybook on localhost:6006
Path-Based Routing
Route different URL paths to different services:
ingress:
# API routes
- hostname: yourdomain.com
path: /api/*
service: http://localhost:8080
# Admin routes
- hostname: yourdomain.com
path: /admin/*
service: http://localhost:5000
# Default routes
- hostname: yourdomain.com
service: http://localhost:3000
- service: http_status:404
Real-world example - Microservices on same domain:
tunnel: microservices
credentials-file: /home/john/.cloudflared/a7b3c4d5-e6f7-8901-2345-6789abcdef01.json
ingress:
# Authentication service
- hostname: jobayer.me
path: /auth/*
service: http://localhost:4000
# Payment processing
- hostname: jobayer.me
path: /payments/*
service: http://localhost:4001
# User management
- hostname: jobayer.me
path: /users/*
service: http://localhost:4002
# Analytics dashboard
- hostname: jobayer.me
path: /analytics/*
service: http://localhost:4003
# Main frontend application (catch-all for other paths)
- hostname: jobayer.me
service: http://localhost:3000
- service: http_status:404
URL routing result:
-
https://jobayer.me/β Frontend (localhost:3000) -
https://jobayer.me/auth/loginβ Auth service (localhost:4000) -
https://jobayer.me/payments/checkoutβ Payment service (localhost:4001) -
https://jobayer.me/users/profileβ User service (localhost:4002) -
https://jobayer.me/analytics/dashboardβ Analytics (localhost:4003) -
https://jobayer.me/aboutβ Frontend (localhost:3000)
Custom Headers
Add custom headers to origin requests:
ingress:
- hostname: app.yourdomain.com
service: http://localhost:3000
originRequest:
httpHostHeader: localhost
customHeaders:
X-Custom-Header: "value"
Advanced Usage
Running as a System Service
Keep your tunnel running persistently, even after reboots.
Linux (systemd)
sudo cloudflared service install
sudo systemctl start cloudflared
sudo systemctl enable cloudflared
Check status:
sudo systemctl status cloudflared
View logs:
sudo journalctl -u cloudflared -f
macOS (launchd)
sudo cloudflared service install
sudo launchctl load /Library/LaunchDaemons/com.cloudflare.cloudflared.plist
Windows (Service)
Run as Administrator:
cloudflared service install
sc start cloudflared
Load Balancing
Distribute traffic across multiple instances:
ingress:
- hostname: app.yourdomain.com
service: http://localhost:3000
originRequest:
connectTimeout: 10s
- hostname: app.yourdomain.com
service: http://localhost:3001
- service: http_status:404
Logging Configuration
Enable detailed logging:
tunnel: my-tunnel
credentials-file: /path/to/credentials.json
loglevel: debug
logfile: /var/log/cloudflared.log
ingress:
- hostname: app.yourdomain.com
service: http://localhost:3000
- service: http_status:404
Log levels: debug, info, warn, error, fatal
Use Cases
1. Personal Blog/Portfolio
Scenario: Host a Hugo, Jekyll, or Next.js site from your laptop.
ingress:
- hostname: blog.yourdomain.com
service: http://localhost:1313 # Hugo default port
- service: http_status:404
Real-world example - Hugo Blog:
tunnel: my-blog
credentials-file: /home/sarah/.cloudflared/a7b3c4d5-e6f7-8901-2345-6789abcdef01.json
ingress:
- hostname: blog.jobayer.me
service: http://localhost:1313
- service: http_status:404
Setup steps:
# 1. Start your Hugo dev server
hugo server -D
# 2. In another terminal, create tunnel route
cloudflared tunnel route dns my-blog blog.jobayer.me
# 3. Run the tunnel
cloudflared tunnel run my-blog
Result: Your Hugo blog running on http://localhost:1313 is now accessible at https://blog.jobayer.me
Other static site generators:
# Jekyll (port 4000)
- hostname: blog.yourdomain.com
service: http://localhost:4000
# Next.js (port 3000)
- hostname: portfolio.yourdomain.com
service: http://localhost:3000
# Gatsby (port 8000)
- hostname: site.yourdomain.com
service: http://localhost:8000
Benefits:
- β Zero hosting costs
- β Instant content updates
- β Full control over your content
- β No deployment pipeline needed
2. Development & Staging
Scenario: Share work-in-progress with clients or team members.
ingress:
- hostname: dev.yourdomain.com
service: http://localhost:3000
- hostname: staging.yourdomain.com
service: http://localhost:4000
- service: http_status:404
Real-world example - Agency workflow:
tunnel: client-demos
credentials-file: /home/alex/.cloudflared/a7b3c4d5-e6f7-8901-2345-6789abcdef01.json
ingress:
# Client A's project
- hostname: clienta-dev.myagency.com
service: http://localhost:3001
# Client B's project
- hostname: clientb-dev.myagency.com
service: http://localhost:3002
# Client C's project
- hostname: clientc-dev.myagency.com
service: http://localhost:3003
- service: http_status:404
Setup multiple client demos:
# Route all domains
cloudflared tunnel route dns client-demos clienta-dev.myagency.com
cloudflared tunnel route dns client-demos clientb-dev.myagency.com
cloudflared tunnel route dns client-demos clientc-dev.myagency.com
# Start all dev servers
cd ~/projects/clienta && npm run dev & # port 3001
cd ~/projects/clientb && npm run dev & # port 3002
cd ~/projects/clientc && npm run dev & # port 3003
# Run tunnel
cloudflared tunnel run client-demos
Share with clients:
Hi Client A,
Here's the latest version of your website:
https://clienta-dev.myagency.com
Let me know your feedback!
Update in real-time:
When you save changes in your code editor, clients see updates immediately after page refresh!
Benefits:
- β No deployment to staging servers needed
- β Real-time collaboration
- β Instant feedback loop
- β Multiple projects running simultaneously
- β Professional presentation with custom domains
3. Webhook Testing
Scenario: Test Stripe, GitHub, or other webhook integrations.
ingress:
- hostname: webhooks.yourdomain.com
service: http://localhost:8080
- service: http_status:404
Real-world example - Stripe payment testing:
tunnel: stripe-webhooks
credentials-file: /home/mike/.cloudflared/a7b3c4d5-e6f7-8901-2345-6789abcdef01.json
ingress:
- hostname: webhooks.mikestore.com
service: http://localhost:4000
- service: http_status:404
Express.js webhook handler:
// server.js
const express = require('express');
const app = express();
app.post('/webhooks/stripe', express.raw({type: 'application/json'}), (req, res) => {
const sig = req.headers['stripe-signature'];
const event = req.body;
console.log('Received Stripe webhook:', event.type);
switch (event.type) {
case 'payment_intent.succeeded':
console.log('Payment succeeded:', event.data.object);
break;
case 'payment_intent.payment_failed':
console.log('Payment failed:', event.data.object);
break;
}
res.json({received: true});
});
app.listen(4000);
Setup steps:
# 1. Create DNS route
cloudflared tunnel route dns stripe-webhooks webhooks.mikestore.com
# 2. Start your server
node server.js
# 3. Run tunnel
cloudflared tunnel run stripe-webhooks
# 4. Configure in Stripe Dashboard
# Webhook URL: https://webhooks.mikestore.com/webhooks/stripe
Test it:
# Create a test payment in Stripe Dashboard
# Watch your terminal for webhook logs:
Received Stripe webhook: payment_intent.succeeded
Payment succeeded: { id: 'pi_123abc', amount: 1000, ... }
Other webhook examples:
ingress:
# GitHub webhooks
- hostname: github.yourdomain.com
path: /webhook
service: http://localhost:3000
# Twilio webhooks
- hostname: twilio.yourdomain.com
service: http://localhost:4000
# PayPal IPN
- hostname: paypal.yourdomain.com
service: http://localhost:5000
- service: http_status:404
Benefits:
- β Public URL for webhook callbacks
- β Test real payments without deploying
- β Debug webhook payloads locally
- β Instant iteration on webhook logic
4. Home Automation
Scenario: Access Home Assistant, Plex, or other home services remotely.
ingress:
- hostname: home.yourdomain.com
service: http://localhost:8123 # Home Assistant
- hostname: media.yourdomain.com
service: http://localhost:32400 # Plex
- service: http_status:404
Real-world example - Complete smart home setup:
tunnel: smart-home
credentials-file: /home/chris/.cloudflared/a7b3c4d5-e6f7-8901-2345-6789abcdef01.json
ingress:
# Home Assistant
- hostname: home.jobayer.me
service: http://localhost:8123
# Plex Media Server
- hostname: plex.jobayer.me
service: http://localhost:32400
# Pi-hole (ad blocking)
- hostname: pihole.jobayer.me
service: http://localhost:80
# Synology NAS
- hostname: nas.jobayer.me
service: http://localhost:5000
# Security cameras (Frigate)
- hostname: cameras.jobayer.me
service: http://localhost:5001
- service: http_status:404
Setup DNS routes:
cloudflared tunnel route dns smart-home home.jobayer.me
cloudflared tunnel route dns smart-home plex.jobayer.me
cloudflared tunnel route dns smart-home pihole.jobayer.me
cloudflared tunnel route dns smart-home nas.jobayer.me
cloudflared tunnel route dns smart-home cameras.jobayer.me
Run as system service (Raspberry Pi):
sudo cloudflared service install
sudo systemctl start cloudflared
sudo systemctl enable cloudflared
Access from anywhere:
- Control lights:
https://home.jobayer.me - Watch movies:
https://plex.jobayer.me - Check cameras:
https://cameras.jobayer.me - Manage files:
https://nas.jobayer.me
Mobile app integration:
Configure Home Assistant mobile app to use https://home.jobayer.me - now you can control your home from anywhere!
Example - Control lights from work:
1. Open https://home.jobayer.me on your phone
2. Click "Living Room Lights"
3. Turn off (you forgot to turn them off!)
Benefits:
- β Secure remote access without VPN
- β No port forwarding security risks
- β Automatic HTTPS encryption
- β Access all your home services
- β Works on any device, anywhere
5. API Development
Scenario: Expose local API for mobile app development.
ingress:
- hostname: api.yourdomain.com
service: http://localhost:8080
originRequest:
connectTimeout: 30s
- service: http_status:404
Real-world example - Mobile app backend:
tunnel: mobile-api
credentials-file: /home/dev/.cloudflared/a7b3c4d5-e6f7-8901-2345-6789abcdef01.json
ingress:
# REST API
- hostname: api.jobayer.me
service: http://localhost:3000
originRequest:
connectTimeout: 30s
# GraphQL API
- hostname: graphql.jobayer.me
service: http://localhost:4000
# API documentation (Swagger)
- hostname: docs.jobayer.me
service: http://localhost:8080
- service: http_status:404
Example Node.js/Express API:
// server.js
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors());
app.use(express.json());
// API endpoints
app.get('/api/users', (req, res) => {
res.json([
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Smith' }
]);
});
app.post('/api/users', (req, res) => {
console.log('Creating user:', req.body);
res.json({ id: 3, ...req.body });
});
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', timestamp: Date.now() });
});
app.listen(3000, () => {
console.log('API running on port 3000');
});
Setup:
# 1. Start API server
node server.js
# 2. Create DNS route
cloudflared tunnel route dns mobile-api api.jobayer.me
# 3. Run tunnel
cloudflared tunnel run mobile-api
Mobile app configuration (React Native):
// config.js
const API_BASE_URL = 'https://api.jobayer.me';
export const fetchUsers = async () => {
const response = await fetch(`${API_BASE_URL}/api/users`);
return response.json();
};
export const createUser = async (userData) => {
const response = await fetch(`${API_BASE_URL}/api/users`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
});
return response.json();
};
Test from mobile device:
# Your phone can now access:
https://api.jobayer.me/api/users
https://api.jobayer.me/api/health
Development workflow:
- Make changes to API code
- Save file (nodemon auto-restarts)
- Test immediately on mobile device
- See changes in real-time!
Example - Testing on physical iPhone:
// In React Native app
fetch('https://api.jobayer.me/api/users')
.then(res => res.json())
.then(users => console.log('Users:', users));
// Output in debugger:
// Users: [{id: 1, name: 'John Doe'}, {id: 2, name: 'Jane Smith'}]
Benefits:
- β Test API with real mobile devices
- β Share API with frontend developers
- β Test in production-like environment
- β No need to deploy for every change
- β Automatic HTTPS for secure testing
Troubleshooting
Tunnel Won't Start
Problem: Error starting tunnel
Solutions:
- Check config syntax:
cloudflared tunnel info my-tunnel
- Verify credentials file:
ls ~/.cloudflared/
- Test local service:
curl http://localhost:3000
- Check for port conflicts:
lsof -i :3000 # macOS/Linux
netstat -ano | findstr :3000 # Windows
Connection Timeouts
Problem: Tunnel connects but requests timeout
Solutions:
- Verify service is running:
netstat -tuln | grep 3000
- Check firewall settings:
sudo ufw status # Linux
- Test with curl:
curl -v http://localhost:3000
- Increase timeout:
originRequest:
connectTimeout: 60s
DNS Not Resolving
Problem: Domain doesn't resolve to tunnel
Solutions:
-
Check DNS records:
- Go to Cloudflare Dashboard β DNS
- Verify CNAME record exists
Wait for propagation:
dig app.yourdomain.com
nslookup app.yourdomain.com
-
Force HTTPS:
- Access via
https://nothttp://
- Access via
Clear DNS cache:
# macOS
sudo dscacheutil -flushcache
# Linux
sudo systemd-resolve --flush-caches
# Windows
ipconfig /flushdns
Certificate Errors
Problem: SSL/TLS certificate errors
Solutions:
Wait for certificate provisioning (can take 5-10 minutes)
-
Verify Cloudflare SSL mode:
- Cloudflare Dashboard β SSL/TLS β Overview
- Set to "Full" or "Full (strict)"
For self-signed certs:
originRequest:
noTLSVerify: true
High Latency
Problem: Slow response times
Solutions:
- Check Cloudflare region:
cloudflared tunnel info my-tunnel
- Monitor local resources:
top # or htop
- Enable compression:
originRequest:
disableChunkedEncoding: false
- Optimize application performance
Tunnel Keeps Disconnecting
Problem: Tunnel drops connection frequently
Solutions:
- Check logs:
cloudflared tunnel run my-tunnel --loglevel debug
Verify network stability
Increase grace period:
grace-period: 30s
- Check for updates:
cloudflared update
Best Practices
Security
β
Use strong authentication
originRequest:
access:
required: true
β
Enable audit logging
logfile: /var/log/cloudflared.log
loglevel: info
β
Regularly update cloudflared
cloudflared update
β Implement rate limiting in your application
β Use environment variables for sensitive data
Performance
β‘ Enable HTTP/2
originRequest:
http2Origin: true
β‘ Optimize application before exposing
β‘ Use caching where appropriate
β‘ Monitor resource usage
β‘ Consider CDN settings in Cloudflare Dashboard
Reliability
π Run as system service for automatic restart
π Monitor uptime with health checks
π Keep backups of configuration files
π Document your setup for team members
π Test failover scenarios
Organization
π Use descriptive tunnel names
cloudflared tunnel create prod-web-server
π Maintain separate configs for different environments
π Version control your config files
π Document DNS records and their purposes
π Use consistent naming conventions
Managing Tunnels
List All Tunnels
cloudflared tunnel list
View Tunnel Details
cloudflared tunnel info my-tunnel
Delete a Tunnel
# Stop the tunnel first
cloudflared tunnel cleanup my-tunnel
# Delete the tunnel
cloudflared tunnel delete my-tunnel
# Remove DNS record
# (manually in Cloudflare Dashboard)
Rotate Credentials
cloudflared tunnel token my-tunnel
Update Tunnel Configuration
- Edit
~/.cloudflared/config.yml - Restart the tunnel:
sudo systemctl restart cloudflared
Additional Resources
- π Official Cloudflare Tunnel Documentation
- π¬ Cloudflare Community Forums
- π Report Issues on GitHub
- πΊ Video Tutorials
- π Cloudflare Learning Center
Originally published at https://jobayer.me/blog/turn-your-local-machine-into-a-web-server-with-cloudflare-tunnel/
Top comments (0)