[ ← BACK TO ARTICLES ]
ESSAY // 2026.08.15

How to Set Up Cloudflare Tunnel for Your Home Server Without Port Forwarding

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