Skip to content
CFOCoder

How Cloudflare Tunnel Became a Game Changer for My Self-Hosted Setup

The Problem: ISP Restrictions and Port Blocking

Cloud 6 min read
cloudflare tunnels blog
cloudflare tunnels blog

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:

Terminal window
# Download the latest version
curl -L --output cloudflared.deb \
https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
# Install
sudo dpkg -i cloudflared.deb
# Verify installation
cloudflared --version

On macOS:

brew install cloudflared

cloudflared tunnel login

This 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 name
cloudflared tunnel create macmini

Expected output:

Tunnel credentials written to /home/user/.cloudflared/a1b2c3d4-5e6f-7g8h-9i0j-k1l2m3n4o5p6.json
Created 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:

Terminal window
# List running containers
docker ps
# Inspect the container's networks
docker 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:

Terminal window
sudo mkdir -p /etc/cloudflared
sudo nano /etc/cloudflared/config.yml

Basic configuration (single service):

Terminal window
tunnel: a1b2c3d4-5e6f-7g8h-9i0j-k1l2m3n4o5p6
credentials-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:404

Advanced configuration (multiple services):

Terminal window
tunnel: a1b2c3d4-5e6f-7g8h-9i0j-k1l2m3n4o5p6
credentials-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:404

Copy credentials to the system directory:

Terminal window
sudo cp ~/.cloudflared/.json /etc/cloudflared/
sudo chmod 600 /etc/cloudflared/.json
sudo chown root:root /etc/cloudflared/.json

You have two options:

Option A: Automatic (Recommended)

# For each hostname, run:
cloudflared tunnel route dns macmini minio.yourdomain.com
cloudflared tunnel route dns macmini minioapi.yourdomain.com

This automatically creates CNAME records in Cloudflare.

Option B: Manual (via Cloudflare Dashboard)

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:

Terminal window
# Install the service
sudo cloudflared service install
# Start the service
sudo systemctl start cloudflared
# Enable auto-start on boot
sudo systemctl enable cloudflared
# Check status
sudo systemctl status cloudflared

Expected 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 connection
Dec 15 10:00:01 macmini cloudflared[1234]: INF Registered tunnel connection
Dec 15 10:00:01 macmini cloudflared[1234]: INF Registered tunnel connection
Dec 15 10:00:01 macmini cloudflared[1234]: INF Registered tunnel connection

You should see 4 registered connections – this provides high availability!

Check the logs:

# View logs in real-time
sudo journalctl -u cloudflared -f
# View last 50 lines
sudo journalctl -u cloudflared -n 50

Test connectivity:

Terminal window
# Test with curl
curl -I https://minio.yourdomain.com
# Should return:
# HTTP/2 200
# server: cloudflare

Open in browser:

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):

Terminal window
# Instead of:
service: http://10.0.2.2:9000
# Use:
service: http://minio-container:9000

Always 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:404

Without 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:

Terminal window
# Verify service is running
docker ps | grep
# Check if IP changed
docker inspect | jq '.[0].NetworkSettings.Networks'
# Update config.yml with correct IP
sudo nano /etc/cloudflared/config.yml
# Restart tunnel
sudo systemctl restart cloudflared

Cause: CNAME records not created or propagation pending.

Solution:

# Create DNS record
cloudflared tunnel route dns macmini myservice.yourdomain.com
# Wait 1-2 minutes for propagation
# Verify with Cloudflare DNS
nslookup myservice.yourdomain.com 1.1.1.1

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

Status (Active/Inactive)

  • Traffic stats (requests, bandwidth)

  • Active connections (should be 4)

  • Errors and latency

Terminal window
# View service status
systemctl status cloudflared
# Real-time logs
sudo journalctl -u cloudflared -f
# Check resource usage
ps aux | grep cloudflared

Terminal window
# Proper permissions
sudo chmod 600 /etc/cloudflared/.json
sudo chown root:root /etc/cloudflared/.json
# NEVER commit these files to version control
# NEVER share them publicly

One 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 firewall

For 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:

SolutionMonthly CostSSLDDoS ProtectionSetup Complexity
Cloudflare Tunnel$0✅ Auto✅ YesLow
VPS Reverse Proxy$5-10ManualLimitedHigh
ngrok$8-25✅ Auto❌ NoLow
Port Forwarding$0Manual❌ NoMedium

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:

Terminal window
# View tunnel status
sudo systemctl status cloudflared
# View real-time logs
sudo journalctl -u cloudflared -f
# Restart tunnel
sudo systemctl restart cloudflared
# List all tunnels
cloudflared tunnel list
# Test service connectivity
curl -I https://myservice.yourdomain.com
# Check Docker container IP
docker inspect | jq '.[0].NetworkSettings.Networks'
# Add new DNS route
cloudflared tunnel route dns newservice.yourdomain.com

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