In the previous article, we discussed why Cloudflare Tunnel is interesting for home servers. Now let’s actually set it up — step by step, from scratch to running.
Here’s the basic flow:
Internet
│
▼
Cloudflare
│
│ Cloudflare Tunnel
▼
cloudflared
(Home Server)
│
▼
Local Service
192.168.1.10:8080
cloudflared creates an outbound connection from our server to Cloudflare. Since the connection starts from inside, we don’t need to open any inbound ports. Traffic from the internet flows through Cloudflare → tunnel → local server.
What You’ll Need
- A domain using Cloudflare DNS
- A Cloudflare account
- A home server connected to the internet
- Docker and Docker Compose
- A local service you want to expose
For example, we have a local app at http://192.168.1.10:8080 and want to access it via https://app.example.com.
The domain you want to use must be added to Cloudflare first before it can be used as a Published Application in the tunnel.
1. Create a Cloudflare Tunnel
Go to the Cloudflare Dashboard:
Networking → Tunnels → Create a tunnel
Name the tunnel. For example, home-server. This name is just for identification, so it’s flexible.
Cloudflare will generate a token — this is what cloudflared uses to connect to Cloudflare.
2. Run cloudflared with Docker
Create the folder:
mkdir -p ~/cloudflared
cd ~/cloudflared
Create docker-compose.yml:
services:
cloudflared:
image: cloudflare/cloudflared:latest
container_name: cloudflared
restart: unless-stopped
command: tunnel --no-autoupdate run --token ***}
Create a .env file:
TUNNEL_TOKEN=your-cloudflare-token
Run it:
docker compose up -d
Check:
docker compose ps
docker compose logs -f cloudflared
If successful, the tunnel will show as Connected on the Cloudflare dashboard.
Important: Never save or share your tunnel token in a public repo.
3. Add a Domain to the Tunnel
Open the Cloudflare Dashboard again:
Networking → Tunnels → home-server → Routes → Add route → Published application
Fill in the hostname:
Subdomain : app
Domain : example.com
For the Service, choose HTTP and enter:
http://192.168.1.10:8080
The concept:
app.example.com
│
▼
Cloudflare
│
▼
Cloudflare Tunnel
│
▼
192.168.1.10:8080
Save it, then open https://app.example.com. If everything’s correct, the app that was previously only accessible from your local network is now reachable from anywhere.
4. One Tunnel, Multiple Applications
You don’t need a separate tunnel for each app. A single tunnel can serve multiple apps — just add routes.
For example, one server running several services:
┌── Nextcloud (8080)
│
Internet ├── Portainer (9443)
│ │
▼ ├── Uptime Kuma (3001)
Cloudflare ─ Tunnel ┤
├── Immich (2283)
│
└── Home Assistant (8123)
Just add routes one by one:
cloud.example.com → http://192.168.1.10:8080
portainer.example.com → https://192.168.1.10:9443
uptime.example.com → http://192.168.1.10:3001
photos.example.com → http://192.168.1.10:2283
home.example.com → http://192.168.1.10:8123
All these hostnames go through the same tunnel.
5. About HTTPS
Users access your app via https://app.example.com — Cloudflare handles the HTTPS. From cloudflared to the local server can use plain HTTP:
Browser ←─ HTTPS ─→ Cloudflare ←─ HTTP ─→ cloudflared ←─ HTTP ─→ 192.168.1.10:8080
Your local service doesn’t need its own SSL certificate. But if you want to, you can still use HTTPS on the origin side — Cloudflare Tunnel supports both.
6. No Port Forwarding Needed
Traditional setup:
Internet → Public IP → Router → Port Forwarding → Server
With Cloudflare Tunnel:
Home Server ──outbound──→ Cloudflare ──→ Internet
cloudflared only makes an outbound connection. No ports are opened from outside.
This is also very useful if your ISP uses CGNAT or doesn’t provide a public IP.
7. Troubleshooting
If the domain isn’t loading:
Container not running?
docker compose ps
docker compose logs -f cloudflared
Local service not accessible?
curl http://192.168.1.10:8080
If it’s not accessible locally, Cloudflare Tunnel won’t be able to reach it either.
DNS already exists error?
Cloudflare says a record already exists for that hostname? Go to the DNS menu, delete the conflicting record, or use a different hostname.
Tunnel not connected?
Make sure the server can reach the internet. Strict firewall? Make sure port 7844 is open for connections to Cloudflare.
A Note on Security
Cloudflare Tunnel means we don’t need to expose our home IP or open ports on the router. But:
Tunnel ≠ automatically private.
If we create portainer.example.com as a Published Application without additional authentication, that hostname is accessible by anyone who knows the address.
For sensitive apps (Portainer, Grafana, internal dashboards), add protection — for example using Cloudflare Access.
We’ll cover this in Part 3.
Conclusion
With Cloudflare Tunnel, we can turn:
http://192.168.1.10:8080
into:
https://app.example.com
Without needing to:
✗ Buy a public IP
✗ Open ports 80/443 on the router
✗ Set up port forwarding
✗ Change NAT configuration
Just: Domain + Cloudflare + cloudflared + Home Server.
I think this is one of the most practical ways to expose a home server to the internet. But once an app is accessible from the internet, the next question arises: how do we make sure only we can open it?
In Part 3, we’ll try securing our home server apps using Cloudflare Access.
COMMENTS & DISCUSSION