How Cloudflare Tunnel Became a Game Changer for My Self-Hosted Setup
The Problem: ISP Restrictions and Port Blocking
Cloudflare Tunnel turned out to be the game changer I needed. It creates a secure, encrypted connection from your server to Cloudflare’s edge network without requiring any open inbound ports on your firewall or router.
Here’s why it’s brilliant:
-
✅ Zero open ports: No port forwarding needed (goodbye ports 80/443!)
-
✅ Works with restrictive ISPs: Bypasses residential port blocks entirely
-
✅ Automatic SSL/TLS: Free certificates managed by Cloudflare
-
✅ Dynamic IP friendly: Your public IP can change without issues
-
✅ Built-in DDoS protection: Cloudflare filters malicious traffic
-
✅ High availability: 4 redundant connections automatically
-
✅ Completely free: Included in Cloudflare’s free tier
My setup involves:
-
Mac Mini running Ubuntu (at home): Running MinIO, OpenSearch, and Qdrant via Coolify
-
Oracle ARM Server (cloud): Running LangFlow, OpenWebUI, and Docling
The Oracle server needed to access the MinIO S3 API running on my Mac Mini. Cloudflare Tunnel made this seamless by exposing:
-
minio.example.com → MinIO Console (web UI)
-
minioapi.example.com → MinIO S3 API
Now my cloud services can use the MinIO instance at home as if it were a public cloud storage service!
Let me walk you through the exact process I used to set this up.
Before starting, you need:
-
A domain registered with Cloudflare (e.g., yourdomain.com)
-
A Linux server (Ubuntu, Debian, etc.)
-
Root or sudo access
-
A service running locally that you want to expose (e.g., MinIO on port 9001)
On Ubuntu/Debian:
# Download the latest versioncurl -L --output cloudflared.deb \ https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
# Installsudo dpkg -i cloudflared.deb
# Verify installationcloudflared --versionOn macOS:
brew install cloudflaredcloudflared tunnel loginThis command will:
-
Open your browser automatically
-
Prompt you to log in to Cloudflare (if not already logged in)
-
Ask you to select which domain to authorize (e.g., yourdomain.com)
-
Create a certificate file at ~/.cloudflared/cert.pem
# Create a tunnel with a descriptive namecloudflared tunnel create macminiExpected output:
Tunnel credentials written to /home/user/.cloudflared/a1b2c3d4-5e6f-7g8h-9i0j-k1l2m3n4o5p6.jsonCreated tunnel macmini with id a1b2c3d4-5e6f-7g8h-9i0j-k1l2m3n4o5p6⚠️ Important: Save that TUNNEL_ID – you’ll need it for the next steps!
For Docker containers, you need to find the container’s internal IP:
# List running containersdocker ps
# Inspect the container's networksdocker inspect | jq '.[0].NetworkSettings.Networks'Example output:
{ "coolify": { "IPAddress": "10.0.1.7", "Gateway": "10.0.1.1" }, "jwkgo0sgscc4ow8ggs8wskoc": { "IPAddress": "10.0.2.2", "Gateway": "10.0.2.1" }}Pro tip: Select the IP from the network that’s NOT named “coolify” (in this case 10.0.2.2).
For native services (not in Docker), you can use localhost or 127.0.0.1.
Create /etc/cloudflared/config.yml:
sudo mkdir -p /etc/cloudflaredsudo nano /etc/cloudflared/config.ymlBasic configuration (single service):
tunnel: a1b2c3d4-5e6f-7g8h-9i0j-k1l2m3n4o5p6credentials-file: /etc/cloudflared/a1b2c3d4-5e6f-7g8h-9i0j-k1l2m3n4o5p6.json
ingress: # Your service - hostname: myservice.yourdomain.com service: http://10.0.2.2:8080
# Catch-all (required, always at the end) - service: http_status:404Advanced configuration (multiple services):
tunnel: a1b2c3d4-5e6f-7g8h-9i0j-k1l2m3n4o5p6credentials-file: /etc/cloudflared/a1b2c3d4-5e6f-7g8h-9i0j-k1l2m3n4o5p6.json
ingress: # MinIO Console (UI) - hostname: minio.yourdomain.com service: http://10.0.2.2:9001
# MinIO API (S3) - hostname: minioapi.yourdomain.com service: http://10.0.2.2:9000
# OpenWebUI - hostname: openwebui.yourdomain.com service: http://10.0.3.3:8080
# Catch-all rule (always at the end) - service: http_status:404Copy credentials to the system directory:
sudo cp ~/.cloudflared/.json /etc/cloudflared/sudo chmod 600 /etc/cloudflared/.jsonsudo chown root:root /etc/cloudflared/.jsonYou have two options:
Option A: Automatic (Recommended)
# For each hostname, run:cloudflared tunnel route dns macmini minio.yourdomain.comcloudflared tunnel route dns macmini minioapi.yourdomain.comThis automatically creates CNAME records in Cloudflare.
Option B: Manual (via Cloudflare Dashboard)
-
Go to: https://dash.cloudflare.com
-
Select your domain
-
Navigate to DNS → Records
-
Click Add record
-
Configure:
Type: CNAME
-
Name: minio (without the full domain)
-
Content: .cfargotunnel.com
-
Proxy status: ✅ Proxied (orange cloud, not gray)
-
Click Save
Repeat for each subdomain.
To make the tunnel start automatically on boot:
# Install the servicesudo cloudflared service install
# Start the servicesudo systemctl start cloudflared
# Enable auto-start on bootsudo systemctl enable cloudflared
# Check statussudo systemctl status cloudflaredExpected output:
● cloudflared.service - cloudflared Loaded: loaded (/etc/systemd/system/cloudflared.service; enabled) Active: active (running) since Sun 2025-12-15 10:00:00 CST Main PID: 1234 Tasks: 8 Memory: 45.2M
Dec 15 10:00:01 macmini cloudflared[1234]: INF Registered tunnel connectionDec 15 10:00:01 macmini cloudflared[1234]: INF Registered tunnel connectionDec 15 10:00:01 macmini cloudflared[1234]: INF Registered tunnel connectionDec 15 10:00:01 macmini cloudflared[1234]: INF Registered tunnel connectionYou should see 4 registered connections – this provides high availability!
Check the logs:
# View logs in real-timesudo journalctl -u cloudflared -f
# View last 50 linessudo journalctl -u cloudflared -n 50Test connectivity:
# Test with curlcurl -I https://minio.yourdomain.com
# Should return:# HTTP/2 200# server: cloudflareOpen in browser:
-
Navigate to https://minio.yourdomain.com
-
Verify it loads correctly
-
Check for a valid SSL certificate (green padlock)
Docker assigns dynamic IPs when containers restart. I learned this the hard way when my MinIO container got a new IP after a reboot.
Solution: Use the container name instead of IP (if on the same Docker network):
# Instead of:service: http://10.0.2.2:9000
# Use:service: http://minio-container:9000Always use http:// in your config.yml, even though your public URL uses HTTPS.
Why? Cloudflare handles SSL/TLS at the edge. Your internal service should respond with plain HTTP. Cloudflare Tunnel securely encrypts the connection from your server to Cloudflare’s edge.
When using Coolify (or similar platforms), don’t add domain configurations in the app itself. Let Cloudflare Tunnel handle all routing.
If you configure URLs in both places, you’ll get redirect loops!
You must always include a catch-all rule at the end of your ingress list:
- service: http_status:404Without this, cloudflared won’t start.
One of the best parts about this setup is how easy it is to add new services:
-
Get the service IP:docker inspect | jq ’.[0].NetworkSettings.Networks’
-
Edit the config:sudo nano /etc/cloudflared/config.yml Add before the catch-all: - hostname: newservice.yourdomain.com service: http://10.0.X.X:PORT
-
Configure DNS:cloudflared tunnel route dns macmini newservice.yourdomain.com
-
Restart tunnel:sudo systemctl restart cloudflared
-
Verify:curl -I https://newservice.yourdomain.com
Done! Your new service is now publicly accessible with HTTPS.
Cause: Tunnel is connected but can’t reach your local service.
Solution:
# Verify service is runningdocker ps | grep
# Check if IP changeddocker inspect | jq '.[0].NetworkSettings.Networks'
# Update config.yml with correct IPsudo nano /etc/cloudflared/config.yml
# Restart tunnelsudo systemctl restart cloudflaredCause: CNAME records not created or propagation pending.
Solution:
# Create DNS recordcloudflared tunnel route dns macmini myservice.yourdomain.com
# Wait 1-2 minutes for propagation
# Verify with Cloudflare DNSnslookup myservice.yourdomain.com 1.1.1.1Cause: Your service is configured with HTTPS but Cloudflare Tunnel connects via HTTP.
Solution: In config.yml, use http:// not https://. Cloudflare handles SSL/TLS at the edge.
-
Go to: https://dash.cloudflare.com
-
Zero Trust → Access → Tunnels
-
Click your tunnel to see:
Status (Active/Inactive)
-
Traffic stats (requests, bandwidth)
-
Active connections (should be 4)
-
Errors and latency
# View service statussystemctl status cloudflared
# Real-time logssudo journalctl -u cloudflared -f
# Check resource usageps aux | grep cloudflared# Proper permissionssudo chmod 600 /etc/cloudflared/.jsonsudo chown root:root /etc/cloudflared/.json
# NEVER commit these files to version control# NEVER share them publiclyOne of the best security features is that you don’t need to open any inbound ports:
# Cloudflared only needs outbound connections to Cloudflare# No ports 80/443 need to be opened on your firewallFor additional security, use Cloudflare Access to require authentication:
-
Go to Zero Trust → Access → Applications
-
Create an access policy
-
Require login (email, Google, GitHub, etc.)
-
Apply to specific subdomains
Example: Protect admin interfaces but allow public API access.
Let’s break down the cost comparison:
| Solution | Monthly Cost | SSL | DDoS Protection | Setup Complexity |
|---|---|---|---|---|
| Cloudflare Tunnel | $0 | ✅ Auto | ✅ Yes | Low |
| VPS Reverse Proxy | $5-10 | Manual | Limited | High |
| ngrok | $8-25 | ✅ Auto | ❌ No | Low |
| Port Forwarding | $0 | Manual | ❌ No | Medium |
Cloudflare Tunnel gives you enterprise-grade features completely free. This is why it’s a game changer.
After setting up Cloudflare Tunnel, my architecture now looks like this:
┌──────────────────────────────────────────────────────┐│ Cloudflare Edge ││ (SSL/TLS, DDoS Protection, CDN, DNS) │└────────────┬─────────────────────────┬────────────────┘ │ │ ┌─────────▼──────────┐ ┌─────────▼──────────┐ │ Cloudflare Tunnel │ │ Cloudflare Tunnel │ │ (Mac Mini Home) │ │ (Oracle ARM Cloud) │ └─────────┬──────────┘ └─────────┬──────────┘ │ │ ┌─────────▼──────────┐ ┌─────────▼──────────┐ │ Mac Mini │ │ Oracle Server │ ├─────────────────────┤ ├────────────────────┤ │ MinIO (S3) │ │ LangFlow │ │ OpenSearch │ │ OpenWebUI │ │ Qdrant │ │ Docling │ └─────────────────────┘ └────────────────────┘ │ │ └───── S3 API ──────┘Key wins:
-
✅ My Oracle server can use MinIO S3 API at minioapi.example.com
-
✅ All services have HTTPS with valid certificates
-
✅ No port forwarding needed on my home router
-
✅ ISP port blocking is completely bypassed
-
✅ Services are protected by Cloudflare’s DDoS mitigation
-
✅ Everything just works™
Here are the commands I use regularly:
# View tunnel statussudo systemctl status cloudflared
# View real-time logssudo journalctl -u cloudflared -f
# Restart tunnelsudo systemctl restart cloudflared
# List all tunnelscloudflared tunnel list
# Test service connectivitycurl -I https://myservice.yourdomain.com
# Check Docker container IPdocker inspect | jq '.[0].NetworkSettings.Networks'
# Add new DNS routecloudflared tunnel route dns newservice.yourdomain.comCloudflare Tunnel has completely transformed how I approach self-hosting. What used to be a complex dance of VPS proxies, VPNs, and firewall rules is now:
-
Install cloudflared
-
Create a tunnel
-
Configure a simple YAML file
-
Add DNS records
That’s it.
The fact that this is completely free and includes SSL/TLS, DDoS protection, and high availability is almost too good to be true. But it is true, and it works beautifully.
If you’re self-hosting services and dealing with ISP restrictions, port blocks, or dynamic IPs, Cloudflare Tunnel is the solution you’ve been looking for. It certainly was for me.
Now my Mac Mini at home and my Oracle ARM server in the cloud work together seamlessly, as if they were in the same data center. And that’s exactly what I needed.
-
Cloudflare Tunnel Documentation: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/
-
Cloudflared GitHub: https://github.com/cloudflare/cloudflared
-
Cloudflare Dashboard: https://dash.cloudflare.com
-
My Setup: Mac Mini (Coolify) + Oracle ARM Server + MinIO S3 storage