If you’ve been running a homelab, you’ve probably hit the same wall I did: you want to access your self-hosted services from outside your house, but you don’t want to open ports on your router, deal with dynamic IP addresses, or pay for a static IP. Cloudflare Tunnels (formerly Argo Tunnel) solves all three problems at once — and it’s free for personal use.
In this guide, I’ll walk you through exactly how to set up cloudflared to securely expose your homelab services — Nextcloud, Grafana, Home Assistant, whatever you’re running — without touching your firewall, buying a VPN subscription, or fighting with Let’s Encrypt renewals ever again.
What Is a Cloudflare Tunnel?
A Cloudflare Tunnel creates an outbound-only encrypted connection from your server to Cloudflare’s edge network. Instead of the internet connecting to your server, your server’s cloudflared daemon connects out to Cloudflare and keeps that connection persistent. When someone visits your domain, their request goes to Cloudflare’s edge, travels down that tunnel, and hits your local service — all without a single inbound port open on your router.
This fundamentally flips the traditional port-forwarding model:
- No port 80/443 open on your router
- No static IP needed —
cloudflaredreconnects automatically if your IP changes - Free TLS certificates, managed and auto-renewed by Cloudflare
- Built-in DDoS protection from Cloudflare’s global anycast network
- Optional Zero Trust access policies — require a Google, GitHub, or email login before anyone reaches your service
Compare this to something like Tailscale, which is a peer-to-peer VPN — both are excellent tools, but they solve different problems. Tailscale is perfect for personal access between your own devices. Cloudflare Tunnels shine when you want to share a service with others or access it from any browser, anywhere, without installing any client software. You could, for example, share a Recipes app or a read-only dashboard with family members using just a URL.
Prerequisites
- A domain name managed by Cloudflare — even a cheap one from Namecheap or Porkbun works; just point the nameservers to Cloudflare’s free DNS
- A free Cloudflare account (no credit card required)
- A Linux server, VM, or Raspberry Pi running your services
- Docker installed (optional but recommended — see our Docker beginner’s guide if you need to get started)
You do not need a Cloudflare Pro plan. The free tier is fully featured for homelab use, including Zero Trust Access.
Step 1: Install cloudflared
You can install cloudflared directly on the host or run it as a Docker container. I strongly prefer Docker — it keeps the daemon contained, makes updates a one-liner, and integrates naturally with whatever else you’re running.
Option A: Native Install (Debian/Ubuntu)
# Download the latest .deb for amd64
curl -L --output cloudflared.deb \
https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
sudo dpkg -i cloudflared.deb
# Verify
cloudflared --version
For ARM (Raspberry Pi 4/5):
curl -L --output cloudflared.deb \
https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm64.deb
sudo dpkg -i cloudflared.deb
Option B: Docker (Recommended)
docker pull cloudflare/cloudflared:latest
We’ll wire up the Docker Compose configuration once we have a tunnel token from the dashboard.
Step 2: Create the Tunnel in Cloudflare Dashboard
Log into your Cloudflare dashboard and navigate to Zero Trust → Networks → Tunnels. If it’s your first time, you’ll be prompted to set up a Zero Trust account name — this is free and doesn’t require billing info.
Click Add a tunnel, choose Cloudflared, and give it a descriptive name (something like homelab, proxmox-host, or raspberrypi). Cloudflare will generate a tunnel token — copy it. This token is the only credential your server needs; it replaces the older certificate-based authentication flow.
You’ll see installation snippets for various platforms. We’ll use the Docker approach.
Step 3: Run cloudflared with Docker Compose
Create a directory and a docker-compose.yml:
mkdir -p /opt/cloudflared
cat > /opt/cloudflared/docker-compose.yml << 'EOF'
services:
cloudflared:
image: cloudflare/cloudflared:latest
container_name: cloudflared
restart: unless-stopped
command: tunnel --no-autoupdate run
environment:
- TUNNEL_TOKEN=your_token_here
networks:
- proxy
networks:
proxy:
external: true
EOF
I'm using an external Docker network called proxy — the same network my other containers (Nginx Proxy Manager, Nextcloud, Grafana) live on. This lets cloudflared reach them by container name rather than by IP address, which is more reliable since container IPs can change on restart.
# Create the network if it doesn't exist yet
docker network create proxy
# Start cloudflared
cd /opt/cloudflared
docker compose up -d
# Tail the logs to confirm a clean connection
docker logs cloudflared -f
A successful startup looks like this:
2026-09-17T10:00:00Z INF Starting tunnel tunnelID=abc123def456
2026-09-17T10:00:01Z INF Registered tunnel connection connIndex=0 location=SJC
2026-09-17T10:00:01Z INF Registered tunnel connection connIndex=1 location=LAX
Notice two connections — cloudflared maintains redundant connections to different Cloudflare PoPs for high availability. Back in the dashboard, your tunnel status should flip to HEALTHY within about 10 seconds.
Step 4: Configure Public Hostnames
This is where the real magic happens. In the tunnel config screen, click Public Hostnames → Add a public hostname. You'll configure:
- Subdomain: e.g.,
nextcloud - Domain: your domain, e.g.,
example.com - Service Type: HTTP or HTTPS
- Service URL: where cloudflared should forward traffic, e.g.,
http://nextcloud:80
If your service is on the same Docker network, use the container name. If it's running natively on the host, use http://localhost:PORT. If it's on a different machine on your LAN, use the local IP: http://192.168.1.50:8096.
Common homelab examples:
| Public Hostname | Service URL | What It Exposes |
|---|---|---|
| nextcloud.example.com | http://nextcloud:80 | Nextcloud file sync |
| grafana.example.com | http://grafana:3000 | Monitoring dashboards |
| ha.example.com | http://homeassistant:8123 | Home Assistant |
| status.example.com | http://uptime-kuma:3001 | Uptime monitoring |
| jellyfin.example.com | http://192.168.1.55:8096 | Media server (different host) |
Cloudflare automatically creates the DNS CNAME records pointing to your tunnel and provisions a TLS certificate from its own CA. Within about 30 seconds, https://nextcloud.example.com is live, with a valid cert, no port forwarding, and no ACME challenge dance.
Step 5: Lock It Down with Access Policies (Don't Skip This)
Putting services on the public internet without authentication is asking for trouble — especially something like a Nextcloud instance, a router admin panel, or a Proxmox dashboard. Cloudflare Access lets you bolt on an authentication layer in front of any tunnel endpoint, entirely separate from the application's own login.
Navigate to Zero Trust → Access → Applications → Add an application → Self-hosted.
Set the application domain to match your public hostname (e.g., grafana.example.com), then configure an access policy. Your options include:
- Email OTP: Cloudflare emails a one-time code to whitelisted addresses — zero infrastructure required
- Google or GitHub login: Allow specific accounts or entire domains (e.g.,
*@yourcompany.com) - One-time PIN: Simpler version of OTP, useful for sharing with non-technical family members
- Service tokens: Machine-to-machine access for scripts or APIs that need to call your service programmatically
For a personal homelab, Email OTP restricted to your own address is the simplest secure option. Even if someone discovers the URL, they'll hit a Cloudflare-hosted login page and get no further without access to your inbox. This authentication happens at Cloudflare's edge — your local service never sees the unauthenticated request at all.
Handling HTTPS Services Locally
Some services (Proxmox, pfSense, Synology DSM) only serve HTTPS with self-signed certificates. Pointing cloudflared at https://proxmox:8006 will fail TLS validation because the cert isn't signed by a public CA.
The fix is to configure noTLSVerify in a local config file. This tells cloudflared to skip cert validation on the origin side (your server) while still presenting a valid Cloudflare cert to the client side (the browser):
tunnel: your-tunnel-id-here
credentials-file: /etc/cloudflared/credentials.json
ingress:
- hostname: proxmox.example.com
service: https://proxmox:8006
originRequest:
noTLSVerify: true
- hostname: synology.example.com
service: https://192.168.1.100:5001
originRequest:
noTLSVerify: true
- service: http_status:404
The final http_status:404 catch-all rule is required — cloudflared rejects configs without it.
Update your Docker Compose to mount the config:
services:
cloudflared:
image: cloudflare/cloudflared:latest
container_name: cloudflared
restart: unless-stopped
command: tunnel --no-autoupdate run --config /etc/cloudflared/config.yml
volumes:
- ./config.yml:/etc/cloudflared/config.yml:ro
- ./credentials.json:/etc/cloudflared/credentials.json:ro
Get the credentials file by authenticating with the CLI:
cloudflared tunnel login
# Opens a browser — log in and authorize
cloudflared tunnel create homelab
# Outputs your tunnel ID and writes ~/.cloudflared/<tunnel-id>.json
Running Multiple Tunnels vs. One Tunnel with Multiple Hostnames
A common architectural question: should you run one cloudflared daemon exposing multiple services, or one per service? For most homelabs, one tunnel with multiple public hostnames is the right answer. It uses minimal RAM (cloudflared sits at about 30-50 MB), and each hostname gets its own Access policy configured independently.
The exception: if you have services spread across multiple physical machines — say a dedicated NAS running TrueNAS and a separate Proxmox box — run a separate cloudflared container on each machine. They can all share the same logical tunnel in the dashboard (just run the same token on both), or be separate named tunnels. Either way, Cloudflare routes traffic to whichever instance is currently connected.
Troubleshooting Common Issues
Tunnel shows DEGRADED or UNHEALTHY in the dashboard
docker logs cloudflared --tail 100 | grep -E "ERR|WARN|failed"
The most common cause is outbound connectivity. Cloudflared needs HTTPS to *.argotunnel.com and *.cloudflare.com on port 443. If your homelab firewall has a restrictive outbound policy, add those as allowed destinations. Also check for DNS resolution — cloudflared needs to resolve Cloudflare hostnames, so make sure it's not pointing to a broken resolver.
502 Bad Gateway when visiting your hostname
Cloudflared connected to Cloudflare's edge successfully, but couldn't reach your local service. Debug checklist:
- Is the target container on the same Docker network? Run
docker network inspect proxyand look for both containers in the list. - Is the service actually running?
docker ps | grep <container-name> - Did you use the right port? Try
curl -v http://<container-name>:PORTfrom inside the cloudflared container:docker exec -it cloudflared wget -qO- http://nextcloud:80
WebSocket connections dropping (Home Assistant, Jupyter, etc.)
Some apps depend heavily on WebSockets. In the public hostname settings, scroll down to Additional Application Settings and enable HTTP/2 Origin Connection and ensure WebSocket proxying isn't disabled. If you're using config.yml, add:
originRequest:
connectTimeout: 30s
noHappyEyeballs: false
Container restarts with buffer size warnings
docker inspect cloudflared --format '{{.State.ExitCode}}'
Exit code 0 or 1 with "failed to sufficiently increase receive buffer size" is a harmless Linux kernel message — cloudflared works fine despite it. Token errors (exit code 1, "tunnel not found" or "authentication failed") mean the token was revoked; regenerate it in the Cloudflare dashboard and update your environment variable.
Performance Considerations
Since traffic routes through Cloudflare's edge before reaching your server, there's an extra network hop. In practice, Cloudflare's anycast network is so well-distributed that the added latency is typically 5-20ms — imperceptible for web apps. For most homelab services — file managers, dashboards, password managers, home automation — you won't notice the difference at all.
Where you will notice it: high-throughput transfers or latency-sensitive protocols. If you're mirroring a 500 GB backup through a tunnel, it's slower than a direct LAN connection. The Cloudflare free tier also caps tunnel throughput in practice around 100 Mbps, which is plenty for typical homelab use but won't saturate a 2.5G NIC. For that kind of workload, a direct peer-to-peer VPN is a better fit.
A Real Homelab Setup
Here's how my current configuration looks — one cloudflared container on my Proxmox host, single tunnel, five public hostnames:
homelab.example.com → http://nginx-proxy-manager:81 (NPM admin UI)
cloud.example.com → http://nextcloud:80 (file sync)
monitor.example.com → http://grafana:3000 (dashboards)
ha.example.com → http://homeassistant:8123 (home automation)
status.example.com → http://uptime-kuma:3001 (uptime tracking)
All five hostnames are protected by Cloudflare Access with Email OTP. If you're not yet using Uptime Kuma to monitor your services, it pairs extremely well with this setup — check out our guide on using Uptime Kuma to monitor your homelab.
The router has zero inbound ports forwarded. The entire cloudflared Docker Compose is about 20 lines. When my ISP rotates my dynamic IP, nothing breaks — cloudflared reconnects within seconds. The whole thing has been running for months with no intervention required.
What Cloudflare Tunnels Won't Do
Being clear about limitations saves headaches later:
- Non-HTTP protocols: Tunnels handle HTTP, HTTPS, and SSH (via
cloudflared access ssh). Raw TCP or UDP (game servers, RTSP streams, MQTT) need a different approach — Cloudflare's TCP tunnel feature exists but requires WARP on the client side. - Privacy from Cloudflare: Cloudflare terminates TLS at their edge, meaning they technically see plaintext traffic in transit. For a homelab running dashboards and file sync this is generally fine, but worth knowing if you're handling genuinely sensitive data.
- Mass video delivery: Cloudflare's Terms of Service prohibit using the free tier to serve large volumes of video content (think a Plex server with 50 external users). Personal use is fine; running a CDN substitute is not.
- Latency-sensitive real-time apps: Game servers, SIP/VoIP, and similar protocols will suffer from the extra hop. Use direct port forwarding or a peer-to-peer VPN for those.
Wrapping Up
Cloudflare Tunnels are one of those tools that genuinely change how you think about homelab networking. Once you've set it up, the old approach — dynamic DNS client, Let's Encrypt certbot cron job, port 443 forwarded on your router, hoping your ISP doesn't block inbound connections — starts to feel like a lot of unnecessary friction.
The combination of zero inbound ports, automatic TLS, and Cloudflare Access authentication gives you a security posture that's genuinely better than most port-forwarding setups, and you get it entirely for free. If you're already self-hosting services with Docker and haven't tried Cloudflare Tunnels yet, this weekend is the perfect time to start.
Got questions or running into a specific error? Drop a comment below — I respond to all of them.