Cloudflare Tunnels for Your Homelab: Zero Port Forwarding, Free Tier
Expose homelab services to the internet without opening a single port using Cloudflare Tunnels. Free, easy to set up, and more secure than traditional port forwarding.
This guide contains no paid affiliate links. Cloudflare Tunnels are entirely free for personal use.
Cloudflare Tunnels let you publish homelab services to the internet without opening any ports on your router. Your server makes an outbound connection to Cloudflare’s edge network, and Cloudflare handles inbound traffic on your behalf. From the outside, your service looks like any other website. Your home IP is never exposed.
I wrote a whole post about why I don’t open ports. Cloudflare Tunnels are the main reason I can keep that policy even for services I want accessible outside my home network.
This is the alternative to Tailscale if you want to share access with people who don’t have Tailscale installed — a public URL they can just open in a browser. For private access (just you and your devices), Tailscale is usually simpler. For anything public-facing, tunnels are the better tool.
What Cloudflare Tunnels Actually Do
The traditional approach to remote access: open a port on your router, point it at your server, and hope your ISP gives you a stable IP. Problems: your home IP is public, ISPs can ban port 80/443, and every open port is an attack surface.
Cloudflare Tunnels reverse this. Your server runs a small daemon called cloudflared that makes an outbound connection to Cloudflare. When someone hits yourapp.yourdomain.com, Cloudflare routes the request through that existing connection to your server. Nothing inbound. No open ports. Your router doesn’t even know it’s happening.
What you need:
- A domain you own (registered anywhere, but managed through Cloudflare DNS)
- A free Cloudflare account
- A Linux server at home (or a Docker container)
The tunnel itself is free. Cloudflare charges nothing for the tunneling functionality on personal plans.
Prerequisites
Before starting, make sure:
- You have a domain added to Cloudflare (nameservers pointing to Cloudflare). If your domain is registered elsewhere, you just need to update the nameservers — you don’t have to transfer the registration.
- You can SSH into your homelab server.
- You have at least one service running that you want to expose (Nginx Proxy Manager works well here — see the NPM setup guide).
Step 1: Create a tunnel in the Cloudflare dashboard
Go to dash.cloudflare.com, log in, and navigate to Zero Trust → Networks → Tunnels.
Click Create a tunnel, give it a name (I use homelab), and click Save tunnel.
Cloudflare will show you an install command. It looks like this:
curl -L --output cloudflared.deb https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb && \
sudo dpkg -i cloudflared.deb && \
sudo cloudflared service install eyJhIjoiYW...
The long token at the end is your tunnel token — it’s unique to your tunnel and proves to Cloudflare which tunnel this is. Copy the entire command.
Step 2: Install cloudflared on your server
SSH into your server and paste the command from the dashboard. This:
- Downloads the
cloudflaredbinary - Installs it as a systemd service
- Configures it with your tunnel token and starts it automatically
After running, verify the service started:
sudo systemctl status cloudflared
You should see active (running). Back in the Cloudflare dashboard, your tunnel should show a green Healthy status within a minute or two.
Step 3: Add a public hostname
With the tunnel running, you need to tell Cloudflare which services to expose and under which hostnames.
In the tunnel dashboard, click your tunnel → Edit → Public Hostname tab → Add a public hostname.
Fill it out:
- Subdomain: the prefix for your URL (e.g.,
recipesforrecipes.yourdomain.com) - Domain: select your domain from the dropdown
- Type: HTTP or HTTPS depending on your service
- URL: the internal address of your service (e.g.,
localhost:8080or192.168.1.100:3000)
Click Save hostname. Cloudflare automatically creates a DNS record for you. Your service is now live at https://subdomain.yourdomain.com within a few seconds.
No certificate setup on your end. Cloudflare handles SSL termination at their edge. The connection between Cloudflare and your server is private through the tunnel.
Step 4: Add more services
Repeat the public hostname step for each service you want to expose. I have several services this way:
| Subdomain | Internal URL | What it is |
|---|---|---|
recipes.yourdomain.com | localhost:9000 | Mealie |
docs.yourdomain.com | localhost:8080 | Paperless-ngx |
status.yourdomain.com | localhost:3001 | Uptime Kuma |
Each gets its own CNAME in Cloudflare DNS pointing to the tunnel. One cloudflared instance handles all of them.
Step 5: Add access control (optional but recommended)
Cloudflare Zero Trust includes an Access product that lets you put an authentication layer in front of your tunneled services. For services without their own login — or where you want an extra layer — this is useful.
Go to Zero Trust → Access → Applications → Add an application → Self-hosted.
- Set the domain/subdomain you want to protect
- Create an access policy: I use email-based OTP (Cloudflare emails a one-time code, no app required)
- Anyone hitting that URL will see a Cloudflare login page first
This is a lighter alternative to running Authelia yourself, though Authelia gives you more control and doesn’t require Cloudflare Access for services already behind your internal SSO.
Running cloudflared as a Docker container
If you prefer keeping everything in Docker rather than installing a system service:
services:
cloudflared:
image: cloudflare/cloudflared:latest
container_name: cloudflared
restart: unless-stopped
command: tunnel --no-autoupdate run
environment:
TUNNEL_TOKEN: your-tunnel-token-here
Get the token from the Cloudflare dashboard (it’s in the install command they show you). Use it here instead of running the install script.
docker compose up -d
The container connects outbound and registers with Cloudflare just like the system install. Public hostnames work the same way — you configure them in the dashboard regardless of how cloudflared is running.
Cloudflare Tunnels vs Tailscale: Which to Use
Both solve remote access without port forwarding. They solve different problems.
| Cloudflare Tunnels | Tailscale | |
|---|---|---|
| Access type | Public URL (anyone with the link) | Private device-to-device |
| Who can use it | Anyone with a browser | Devices on your tailnet |
| Auth layer | Cloudflare Access (optional) | Built-in (login to Tailscale) |
| Your IP exposed | No | No |
| Setup complexity | Low | Low |
| Cost | Free | Free (up to 100 devices) |
| Best for | Sharing with others, webhooks, public-facing services | Personal remote access, split DNS, full network access |
I run both. Tailscale is my primary remote access tool — it gives me full internal network access with split DNS so internal hostnames work everywhere. Cloudflare Tunnels handle the handful of services I want accessible without requiring anyone to install software: sharing a recipe collection with family, webhooks from external services, a public-facing status page.
For strictly private access, use Tailscale. For anything where you need a plain URL someone can open in a browser, use Cloudflare Tunnels.
What Not to Expose
A few cautions about what you put on a public tunnel:
Don’t expose your router admin interface. If it’s on a public URL, it’s a target regardless of how strong your password is.
Don’t expose services that lack authentication without adding Cloudflare Access. Uptime Kuma’s status page is designed to be public. The Uptime Kuma admin interface is not.
Be thoughtful about file managers and media servers. Jellyfin has solid authentication; exposing it is reasonable. A raw file browser with no login is not.
The Cloudflare dashboard shows you all active tunnels and hostnames in one place. Audit it occasionally — it’s easy to add a hostname for testing and forget it’s still running.
Troubleshooting
Tunnel shows Unhealthy in the dashboard:
Check the cloudflared service:
sudo systemctl status cloudflared
sudo journalctl -u cloudflared -n 50
Common causes: server lost internet connectivity, token expired (shouldn’t happen but can after a reinstall), or the service isn’t running.
502 Bad Gateway on your public URL:
Cloudflare can reach the tunnel but the tunnel can’t reach your service. Check:
- Is the service actually running?
docker psorsystemctl status service-name - Is the URL in the public hostname config correct?
localhost:3001nothttp://localhost:3001(Cloudflare adds the protocol based on the Type field) - Is the service listening on the right port?
ss -tlnp | grep 3001
SSL certificate error:
This usually means you set Type to HTTPS but your internal service uses a self-signed cert. Either switch to HTTP in the public hostname config (Cloudflare still serves it as HTTPS externally), or check the TLS → No TLS Verify option in the public hostname settings.
The One-Sentence Version
Install cloudflared on your server, create a tunnel in the Cloudflare dashboard, point it at your service’s internal address, and you get a public HTTPS URL with no port forwarding and no exposed home IP — for free.
For full private access with split DNS and your internal hostnames working everywhere, pair this with Tailscale. For the SSO layer that protects everything, see the Authelia setup guide.