Securely Expose Homelab Services with Cloudflare Tunnels
If you run a homelab, you’ve probably faced this dilemma: you want to access your services from outside your home network, but opening ports on your router feels risky. Port forwarding exposes your public IP, requires constant certificate management, and creates an attack surface you’d rather avoid.
Cloudflare Tunnels solves this elegantly — no open ports, automatic HTTPS, built-in DDoS protection, and optional Zero Trust authentication. And it’s completely free.
In this guide, I’ll walk you through setting up a fully Dockerized Cloudflare Tunnel from scratch and securing it with Cloudflare Access.
What is a Cloudflare Tunnel?
A Cloudflare Tunnel (formerly known as Argo Tunnel) creates an outbound-only connection between a lightweight daemon called cloudflared running on your machine and Cloudflare’s global edge network. Instead of opening ports on your router, cloudflared initiates the connection outward — just like a regular client accessing the internet. Cloudflare then routes traffic from your domain through that tunnel to your internal service.
The connection is post-quantum encrypted, and each tunnel maintains four long-lived connections to two Cloudflare data centers for built-in redundancy. You can run multiple cloudflared replicas for additional high availability.
Source: Cloudflare Tunnel Documentation
Main advantages
- Zero open router ports — no port forwarding or direct public IP exposure
- Automatic HTTPS — Cloudflare handles TLS certificates; no Let’s Encrypt needed
- DDoS protection — traffic passes through Cloudflare’s edge before reaching your service
- Cloudflare Access integration — require authentication (Google, GitHub, Azure AD, etc.) before any request reaches your service
- Free — everything works on Cloudflare’s free plan (up to 50 users in Access)
Why not just open ports?
The traditional approach to making an internal service internet-accessible involves:
- Opening a port on the router (port forwarding)
- Setting up DDNS for a stable hostname
- Obtaining and renewing TLS certificates (Let’s Encrypt)
- Hoping nobody discovers that open port
Each step adds attack surface. Even with best practices, any service directly exposed to the internet can be scanned and exploited. Cloudflare Tunnels eliminates all four problems at once.
Prerequisites
Before we start, make sure you have:
- Docker and Docker Compose installed on your homelab machine
- A domain configured in Cloudflare (nameservers pointing to Cloudflare)
- A Cloudflare account with access to the Zero Trust Dashboard
Step 1 — Create a tunnel in the Zero Trust Dashboard
- Go to one.dash.cloudflare.com
- Navigate to Networks → Tunnels → Create a tunnel
- Select the Cloudflared tunnel type
- Give your tunnel a name (e.g.,
homelab-tunnel) - Save the Tunnel Token displayed on screen — you’ll need it in the next step
You can also create tunnels via the CLI with
cloudflared tunnel create, but the dashboard approach is simpler and recommended for getting started.
Step 2 — Configure your services with Docker Compose
Instead of installing cloudflared natively, we’ll run everything in containers. This keeps your homelab clean, reproducible, and easy to manage.
Create a project directory and a docker-compose.yml:
version: "3.8"
services: # Cloudflare Tunnel — connects your # Docker network to Cloudflare's edge cloudflared: image: cloudflare/cloudflared:latest container_name: cloudflared restart: unless-stopped command: tunnel run environment: - TUNNEL_TOKEN=$TUNNEL_TOKEN networks: - homelab-net depends_on: - pihole - homeassistant
# Pi-hole — DNS ad-blocker + dashboard pihole: image: pihole/pihole:latest container_name: pihole restart: unless-stopped environment: - TZ=Europe/Lisbon - WEBPASSWORD=$PIHOLE_PASSWORD volumes: - ./pihole/etc-pihole:/etc/pihole - ./pihole/etc-dnsmasq.d:/etc/dnsmasq.d networks: - homelab-net
# Home Assistant — home automation hub homeassistant: image: ghcr.io/home-assistant/home-assistant:stable container_name: homeassistant restart: unless-stopped environment: - TZ=Europe/Lisbon volumes: - ./homeassistant/config:/config network_mode: host
# Nextcloud — self-hosted file sync nextcloud: image: nextcloud:latest container_name: nextcloud restart: unless-stopped environment: - MYSQL_HOST=db - MYSQL_DATABASE=nextcloud - MYSQL_USER=$NC_DB_USER - MYSQL_PASSWORD=$NC_DB_PASS volumes: - ./nextcloud/html:/var/www/html networks: - homelab-net depends_on: - db
# MariaDB — database for Nextcloud db: image: mariadb:latest container_name: mariadb restart: unless-stopped environment: - MYSQL_ROOT_PASSWORD=$MYSQL_ROOT_PASS - MYSQL_DATABASE=nextcloud - MYSQL_USER=$NC_DB_USER - MYSQL_PASSWORD=$NC_DB_PASS volumes: - ./mariadb/data:/var/lib/mysql networks: - homelab-net
networks: homelab-net: driver: bridgeNow create a .env file in the same directory:
TUNNEL_TOKEN=eyJhIjoi...paste-your-token-here... PIHOLE_PASSWORD=your-secure-password NC_DB_USER=nextcloud NC_DB_PASS=your-db-password MYSQL_ROOT_PASS=your-root-passwordNever commit the
.envfile to git. Add it to your.gitignore.
Note on
network_mode: host: Some services like Home Assistant need host networking for device discovery (mDNS, Bluetooth, etc.). If you prefer everything on the Docker bridge network, usenetworks: [homelab-net]and set up an mDNS reflector like Avahi.
Step 3 — Configure tunnel routes (Dashboard UI approach)
With the dashboard-managed tunnel (recommended), you configure public hostnames directly in the Zero Trust UI — no YAML file needed.
Go to Networks → Tunnels → [your tunnel] → Public Hostname and add:
| Subdomain | Domain | Service | URL |
|---|---|---|---|
pihole | yourdomain.com | HTTP | http://pihole:80 |
hass | yourdomain.com | HTTP | http://homeassistant:8123 |
cloud | yourdomain.com | HTTP | http://nextcloud:80 |
Important: Because
cloudflaredand your services are on the same Docker network (homelab-net), you can reference containers by service name (e.g.,pihole,nextcloud) instead of IP addresses. Docker’s internal DNS resolves these automatically.
For Proxmox or services running outside Docker (bare metal / VMs), use the internal IP instead:
| Subdomain | Domain | Service | URL |
|---|---|---|---|
proxmox | yourdomain.com | HTTPS | https://192.168.1.10:8006 |
When proxying to an HTTPS origin with a self-signed cert (like Proxmox), enable No TLS Verify under Additional application settings → TLS → No TLS Verify.
Step 4 — (Alternative) Configure routes with config.yml
If you prefer managing routes declaratively via a config file (good for version control), mount a config.yml into the cloudflared container.
Update the cloudflared service in your docker-compose.yml:
cloudflared: image: cloudflare/cloudflared:latest container_name: cloudflared restart: unless-stopped command: tunnel --config /etc/cloudflared/config.yml run volumes: - ./cloudflared/config.yml:/etc/cloudflared/config.yml - ./cloudflared/credentials.json:/etc/cloudflared/credentials.json networks: - homelab-netAnd create ./cloudflared/config.yml:
tunnel: YOUR_TUNNEL_IDcredentials-file: /etc/cloudflared/credentials.json
ingress: - hostname: pihole.yourdomain.com service: http://pihole:80 - hostname: hass.yourdomain.com service: http://homeassistant:8123 - hostname: cloud.yourdomain.com service: http://nextcloud:80 - hostname: proxmox.yourdomain.com service: https://192.168.1.10:8006 originRequest: insecureSkipVerify: true # Catch-all (required) - service: http_status:404Then create the DNS CNAME records: bash cloudflared tunnel route dns homelab-tunnel pihole.yourdomain.com cloudflared tunnel route dns homelab-tunnel hass.yourdomain.com cloudflared tunnel route dns homelab-tunnel cloud.yourdomain.com cloudflared tunnel route dns homelab-tunnel proxmox.yourdomain.com
See the official configuration file docs for all available options.
Step 5 — Bring everything up
docker compose up -dCheck that all containers are running:
docker compose psCheck cloudflared logs to confirm the tunnel is connected:
docker logs cloudflaredYou should see output like: INF Connection … registered connIndex=0 … INF Connection … registered connIndex=1 … INF Connection … registered connIndex=2 … INF Connection … registered connIndex=3 …
Four connections = full redundancy (two per Cloudflare data center).
Verify the tunnel status in the Zero Trust Dashboard under Networks → Tunnels. A green HEALTHY badge means everything is working.
Step 6 — Secure with Cloudflare Access (Zero Trust)
Right now, anyone who knows your subdomains can access your services. Let’s lock that down.
- In the Zero Trust Dashboard, go to Access → Applications → Add an application
- Select Self-hosted
- Enter the service hostname (e.g.,
pihole.yourdomain.com) - Configure an Identity Provider:
| Provider | Setup difficulty | Notes |
|---|---|---|
| Easy | Best for personal use; requires a Google OAuth client | |
| GitHub | Easy | Great for developers |
| Azure AD / Entra ID | Medium | Good if you already use Microsoft services |
| One-time PIN | Easiest | Cloudflare emails a 6-digit code; no IdP setup needed |
- Create an access policy (Allow) with rules such as:
- Include → emails ending in
@yourdomain.com - Or Include → specific emails (
[email protected])
- Include → emails ending in
- Save the application
Repeat this process for each service you want to protect.
From now on, when someone accesses pihole.yourdomain.com, they’ll be redirected to the identity provider’s login page. Only after successful authentication is the traffic forwarded to your Pi-hole container. Without a valid session token, the request never reaches your service.
Cloudflare Access is free for up to 50 users on the Zero Trust free plan. See pricing details.
Step 7 — Additional security layers
Beyond Cloudflare Access, you can add further protection:
| Layer | What it does | Plan availability |
|---|---|---|
| Cloudflare Access | Requires authentication before traffic reaches the service | Free (50 users) |
| WAF Rules | Blocks SQL injection, XSS, known malicious bots | Free (managed rules) |
| Rate Limiting | Limits requests per IP to mitigate brute-force | Free (basic) |
| Country Blocks | Restricts access by country (e.g., only allow Portugal) | Free |
| Bot Management | Challenges automated traffic with CAPTCHA | Pro plan |
| Session Duration | Controls how long an Access session lasts before re-auth | Free |
Learn more in the Cloudflare Zero Trust documentation.
Comparison with alternatives
| Feature | Cloudflare Tunnel | Tailscale | Port Forwarding + DDNS |
|---|---|---|---|
| Open router ports | No | No | Yes |
| Public IP exposed | No | No | Yes |
| Requires VPN client | No | Yes | No |
| Automatic HTTPS | Yes | No | No (needs Let’s Encrypt) |
| Built-in auth (IdP) | Yes (Access) | Not natively | No |
| Cost | Free (50 users) | Free (100 devices) | Free |
| Best for | Exposing services with a custom domain | Private personal access between devices | Not recommended for homelab |
Considerations and limitations
- Traffic passes through Cloudflare — Cloudflare terminates TLS at the edge (between the user and Cloudflare). Traffic between Cloudflare and your origin is also encrypted, but Cloudflare is in the path. For most homelab use cases, this is perfectly acceptable.
- Latency — Traffic routing through Cloudflare’s edge adds a few milliseconds. Generally imperceptible.
- Terms of Service — Cloudflare’s free plan prohibits serving non-HTML content (videos, large images) through the proxy. For admin panels and APIs, there’s no issue.
- Cloudflare dependency — If Cloudflare has an outage, you lose remote access to your services. Always keep local network access as a fallback.
- Docker networking — Services using
network_mode: host(like Home Assistant) won’t be on thehomelab-netbridge. Reference them byhost.docker.internalor the host’s LAN IP in your tunnel config.
Conclusion
Cloudflare Tunnels combined with Access offer, for free, a Zero Trust architecture that would be impractical to build from scratch for most homelabbers. No open ports, automatic HTTPS, federated authentication, and everything running in Docker containers — clean, reproducible, and easy to manage.
If you don’t already have a domain on Cloudflare, this is the perfect excuse to get one.
References
- Cloudflare Tunnel — Official Documentation
- Cloudflare One — Connectors
- Cloudflare Tunnel Configuration File
- Cloudflare Zero Trust Pricing
- Cloudflare Blog — Free Tunnels for Everyone
← Back to blog