Open a port on your router and you’ve opened a door for everyone, not just yourself. That’s the tradeoff every self-hosted admin used to accept: forward port 443, point DNS at your home IP, and hope your firewall rules hold. Cloudflare Tunnel skips that tradeoff entirely. It builds an outbound-only connection from your server to Cloudflare’s edge, so nothing on your network ever listens for inbound traffic from the internet.
This tutorial walks through a full Cloudflare Tunnel setup in 2026, using the current cloudflared release, from a clean install to a production-hardened deployment with Zero Trust Access policies. By the end you’ll have a working tunnel exposing a real service, plus a persistent Docker Compose or systemd deployment you can copy into your own homelab or production box. Twelve steps, roughly 45 minutes, zero open inbound ports.
What Cloudflare Tunnel Actually Does (and Why It’s Not Warp)
Cloudflare Tunnel runs a small daemon called cloudflared on your server. That daemon opens an outbound connection to the nearest Cloudflare data center and registers itself against a named tunnel in your account. When a request hits your hostname, Cloudflare routes it down that existing connection to your origin. No port forwarding, no dynamic DNS hacks, no exposed IP.
People frequently conflate cloudflared with Cloudflare Warp, and the two solve different problems. Warp is the client-side agent that routes an individual device’s traffic through Cloudflare, the way a traditional VPN client would. Cloudflare Tunnel is the server-side counterpart: it exposes a service you run, rather than a device you carry. In a full Zero Trust deployment you’ll often use both, but for this tutorial we’re focused purely on the tunnel side, getting a self-hosted app reachable at a real hostname without touching your router.
Cloudflare Tunnel sits under the broader Cloudflare One / Zero Trust product line. In 2026, Cloudflare shipped GA releases of the Cloudflare One Client for both Windows and Linux at version 2026.7.1343.0, folding several features that had been in beta into the stable channel. An earlier Windows build, 2026.6.905.0, fixed a longstanding bug where the client failed to reconnect cleanly after a laptop woke from sleep, a complaint that had shown up repeatedly in support threads. None of that is required for the basic tunnel setup below, but it matters if you later layer Warp-based device enrollment on top.
How a Request Actually Travels Through the Tunnel
It helps to picture the full path a request takes before writing any config. A visitor types app.example.com into a browser. DNS resolves that hostname to a Cloudflare IP, since the CNAME you’ll create in Step 5 points at Cloudflare’s network rather than your server. The request lands at the nearest Cloudflare data center, where it’s matched against your Access policy if one exists, then against your ingress rules.
From there, Cloudflare looks up which cloudflared connector currently holds an active, authenticated connection for that tunnel, and forwards the request down that existing outbound tunnel. Your server never opens a listening socket to the public internet at any point in this chain, the only socket it opens is the one it initiated itself, outbound, to Cloudflare. That’s the entire security model in one sentence: nothing on your network accepts unsolicited inbound connections, ever, regardless of how many hostnames or services you route through it.
This also explains why a tunnel keeps working through IP changes, ISP outages that resolve on their own, or even switching your server to a completely different network. The tunnel identity lives in the credentials file, not in any network address, so as long as that file and the domain’s DNS setup stay intact, the tunnel reconnects and traffic resumes without touching a single DNS record.
Real-World Use Cases: Where This Actually Gets Used
Homelab admins reach for Cloudflare Tunnel most often to put a real domain in front of a Plex server, a Home Assistant dashboard, or a Grafana instance running on a Raspberry Pi tucked behind a residential ISP connection. None of those setups have a static IP, and most residential ISPs sit behind CGNAT anyway, so port forwarding was never really an option to begin with. A tunnel sidesteps that limitation completely since the connection direction is reversed: your box calls out to Cloudflare, Cloudflare never calls in.
On the production side, small SaaS teams use tunnels to expose internal admin panels, staging environments, and CI runners without maintaining a VPN concentrator or a bastion host. A common pattern: staging.internal-app.com sits behind a tunnel with an Access policy gating it to the engineering team’s email domain, while the production hostname skips Access entirely because it’s meant to be public. Agencies managing several client WordPress or Node installs on a single VPS use tunnels to give each client’s staging site its own hostname without opening a fresh port per project.
The through-line across all of these: the service being exposed never needs to trust the network it sits on. Whether that’s a residential router with a shared IP or a shared hosting box, the tunnel handles routing and Access handles who gets in. Cloudflare’s own get-started documentation for connecting networks covers the underlying model in more depth if you want the platform-level view before diving into commands.
Prerequisites
You don’t need much to follow along. Here’s the full list, with the versions this guide was tested against.
| Requirement | Version / detail | Notes |
|---|---|---|
| Cloudflare account | Free plan is enough | Domain must have its nameservers pointed at Cloudflare |
| cloudflared | 2026.8.2 (latest stable as of Aug 14, 2026) | Check github.com/cloudflare/cloudflared/releases for the current tag before you install |
| A server or VM | Linux (Debian/Ubuntu used here), macOS, or Windows | Docker also works if you’d rather containerize cloudflared |
| Docker Engine (optional) | Recent stable release | Only needed for the Docker Compose path in Step 7 |
| A domain on Cloudflare | Any TLD | Used to create the public hostname that routes to your tunnel |
| Terminal access | SSH or local shell | Root or sudo for systemd install |
That’s the whole list. You don’t need a static IP, you don’t need to touch your router’s admin panel, and you don’t need a paid Cloudflare plan to follow every step here.
Step 1-2: Install and Authenticate cloudflared
Start by installing the daemon. On Debian or Ubuntu, add Cloudflare’s package repository and pull the binary through apt so future updates are handled by your normal package manager instead of manual downloads.
sudo mkdir -p --mode=0755 /usr/share/keyrings
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared any main' | sudo tee /etc/apt/sources.list.d/cloudflared.list
sudo apt-get update && sudo apt-get install cloudflared
# Confirm the version you actually got
cloudflared --version
You should see output close to this (version numbers shift with each release, so treat the exact string as illustrative):
cloudflared version 2026.8.2 (built 2026-08-14-1223 UTC)
Next, authenticate the CLI against your Cloudflare account. This opens a browser window (or prints a URL if you’re on a headless box) where you pick the domain you want to route traffic for.
cloudflared tunnel login
A successful login drops a certificate at ~/.cloudflared/cert.pem. That certificate is what lets cloudflared create and manage tunnels under your account going forward, so treat it like any other credential, don’t commit it to a public repo, and don’t copy it to a machine you don’t control.
Step 3-4: Create the Tunnel and Configure Ingress Rules
With the certificate in place, create a named tunnel. Give it something descriptive since you’ll reference this name in every command and config file from here on.
cloudflared tunnel create homelab-tunnel
# Output looks like:
# Tunnel credentials written to /home/user/.cloudflared/<TUNNEL-ID>.json
# Created tunnel homelab-tunnel with id 7f3a1c9e-4b2d-4e11-9a6f-2d8c5e0b91aa
That JSON credentials file is tied to this specific tunnel. If you move the connector to a different host later, you need to copy this file across too, forgetting it is the single most common reason people see authentication failures on a fresh box.
Now write the ingress configuration. This file tells cloudflared which hostname maps to which local service. Create ~/.cloudflared/config.yml:
tunnel: homelab-tunnel
credentials-file: /home/user/.cloudflared/7f3a1c9e-4b2d-4e11-9a6f-2d8c5e0b91aa.json
ingress:
- hostname: app.example.com
service: http://localhost:8080
- hostname: grafana.example.com
service: http://localhost:3000
- service: http_status:404
Ingress rules are evaluated top to bottom, and the last rule has to be a catch-all. Put a generic service: http_status:404 rule anywhere above your real hostnames and every request after it gets swallowed before it ever reaches your app, that’s a debugging session waiting to happen, so keep the catch-all at the bottom, always.
Step 5-6: Route DNS and Run Your First Connection
A tunnel with no DNS record pointing at it is invisible to the outside world. Route each hostname from your config file to the tunnel:
cloudflared tunnel route dns homelab-tunnel app.example.com
cloudflared tunnel route dns homelab-tunnel grafana.example.com
This creates a CNAME record in your Cloudflare DNS zone pointing at <TUNNEL-ID>.cfargotunnel.com. You can confirm it in the Cloudflare dashboard under DNS, it’ll show up proxied (orange cloud) automatically.
Run the tunnel in the foreground first, so you can watch the connection come up and catch any config errors immediately:
cloudflared tunnel run homelab-tunnel
A healthy startup prints four separate connection registrations, one per edge location it connects to for redundancy:
2026-08-26T14:02:11Z INF Connection registered connIndex=0 location=lax07
2026-08-26T14:02:11Z INF Connection registered connIndex=1 location=sjc03
2026-08-26T14:02:12Z INF Connection registered connIndex=2 location=lax05
2026-08-26T14:02:12Z INF Connection registered connIndex=3 location=sjc06
Hit https://app.example.com from any browser and you should land on whatever’s running on localhost:8080. If it works, kill the foreground process with Ctrl+C and move to a persistent deployment, you don’t want the tunnel dying every time you close your SSH session.
Step 7: Deploy cloudflared as a Persistent Service
You’ve got two solid options here depending on how the rest of your stack is deployed: Docker Compose if your services already run in containers, or a systemd unit if you’re running bare metal or a VM. Both are shown below as the complete, working project, pick whichever matches your environment.
Option A: Docker Compose
This is the pattern most homelab admins reach for, because it keeps the app and the tunnel connector defined in one file, on one shared Docker network, with no manual localhost port juggling.
version: "3.9"
services:
app:
image: nginx:latest
container_name: demo-app
restart: unless-stopped
networks:
- tunnel-net
cloudflared:
image: cloudflare/cloudflared:latest
container_name: cloudflared
restart: unless-stopped
command: tunnel run homelab-tunnel
volumes:
- ./cloudflared:/etc/cloudflared
environment:
- TUNNEL_TOKEN=${TUNNEL_TOKEN}
networks:
- tunnel-net
depends_on:
- app
networks:
tunnel-net:
driver: bridge
Note the service: http://app:8080 pattern you’d use in config.yml when both containers share tunnel-net, referencing the app by its Docker service name rather than localhost. That’s the fix for the most common Docker-related tunnel failure: pointing at localhost from inside the cloudflared container, where localhost means the container itself, not your host or your other containers.
Option B: systemd
For bare-metal or VM deployments, run cloudflared under a dedicated system user with automatic restarts.
sudo useradd --system --home /etc/cloudflared --shell /usr/sbin/nologin cloudflared
sudo mkdir -p /etc/cloudflared
sudo cp ~/.cloudflared/config.yml /etc/cloudflared/
sudo cp ~/.cloudflared/*.json /etc/cloudflared/
sudo chown -R cloudflared:cloudflared /etc/cloudflared
sudo tee /etc/systemd/system/cloudflared.service <<'EOF'
[Unit]
Description=Cloudflare Tunnel agent
After=network-online.target
Wants=network-online.target
[Service]
User=cloudflared
Group=cloudflared
ExecStart=/usr/local/bin/cloudflared tunnel --config /etc/cloudflared/config.yml run
Restart=always
RestartSec=5
NoNewPrivileges=true
ProtectHome=true
ProtectSystem=full
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now cloudflared
sudo systemctl status cloudflared
Either path gets you a tunnel that survives reboots and reconnects automatically if your network blips. Check logs with journalctl -u cloudflared -f for the systemd route, or docker logs -f cloudflared for the container route.
Multiple Services, Wildcards, and Path-Based Routing
The two-hostname example in Step 4 covers the common case, but real deployments usually need more flexibility. Ingress rules also accept wildcard hostnames and path matching, so you can route an entire subdomain tree through one rule or split a single hostname across multiple backend services by URL path.
ingress:
- hostname: "*.internal.example.com"
service: http://localhost:8443
originRequest:
noTLSVerify: true
- hostname: api.example.com
path: ^/v1/.*
service: http://localhost:9001
- hostname: api.example.com
path: ^/v2/.*
service: http://localhost:9002
- service: http_status:404
The wildcard rule catches anything under internal.example.com without you having to add a new DNS route and ingress entry for every new subdomain, useful if you’re spinning up short-lived preview environments. The path-based rules on api.example.com let two different backend versions live behind the same hostname, common when you’re running a v1/v2 API split during a migration. Remember that the ordering rule from Step 4 still applies here: more specific rules go first, the catch-all stays last, and cloudflared matches top to bottom without backtracking.
Step 8-9: Lock It Down with Zero Trust Access Policies
A public hostname without an access policy is just a differently-shaped open port. Cloudflare Access sits in front of your tunnel and forces authentication before any request reaches your origin, and this is the step people skip most often because the tunnel “already works” without it.
In the Zero Trust dashboard, go to Access > Applications and add a new self-hosted application for each hostname you want to protect:
- Application domain:
grafana.example.com - Session duration: pick something short for admin tools, 24 hours is reasonable for a homelab dashboard
- Policy: Allow, with a rule matching your email domain or a specific list of emails
- Identity provider: Cloudflare’s built-in one-time PIN works with zero extra setup, or wire up Google/GitHub/SAML if you already run an IdP
Once saved, visiting grafana.example.com redirects to a Cloudflare-hosted login page before it ever touches your Grafana instance. Anyone without a matching policy gets a 403, and they never see your origin server exists.
Access policies aren’t limited to a single allow rule either. You can stack conditions, requiring both a matching email domain and a specific country, or a matching group membership and a hardware key for anything genuinely sensitive. For teams managing more than a handful of applications, defining these policies as code beats clicking through the dashboard every time:
resource "cloudflare_zero_trust_access_application" "grafana" {
zone_id = var.zone_id
name = "Grafana Dashboard"
domain = "grafana.example.com"
session_duration = "24h"
}
resource "cloudflare_zero_trust_access_policy" "grafana_allow_eng" {
application_id = cloudflare_zero_trust_access_application.grafana.id
zone_id = var.zone_id
name = "Allow engineering team"
decision = "allow"
include {
email_domain = ["example.com"]
}
}
Keeping Access policies in version control alongside your config.yml means a policy change goes through the same review process as any other infrastructure change, and you get a full history of who could access what and when, which matters the first time someone asks “who had access to the staging environment last month.”
Leave your genuinely public-facing hostname (like app.example.com above) without an Access application if it’s meant to be open to the internet. Access policies are for internal tools, admin panels, and anything you wouldn’t want indexed by Google.
Step 10-12: Harden Further with mTLS, Service Tokens, and Credential Rotation
For machine-to-machine access, say a CI runner that needs to hit an internal API, browser-based login flows don’t work. Cloudflare Access supports service tokens for exactly this case: generate a client ID and secret pair in the dashboard, then attach them as headers on automated requests instead of a human login.
curl -H "CF-Access-Client-Id: <CLIENT_ID>.access" \
-H "CF-Access-Client-Secret: <CLIENT_SECRET>" \
https://internal-api.example.com/health
For an extra layer, mutual TLS (mTLS) can require a client certificate on top of the Access policy, useful when you’re exposing something sensitive like a database admin UI. Rotate both the tunnel credentials JSON and any service tokens on a schedule rather than leaving them static forever, and if a laptop or server that had tunnel access is ever decommissioned, revoke its tunnel and re-create it rather than trying to remember every place the old credentials file was copied.
Finally, keep cloudflared itself updated. Check the current release against what you’re running with a quick script you can drop in a cron job:
curl -s https://api.github.com/repos/cloudflare/cloudflared/releases/latest \
| grep '"tag_name"' | cut -d '"' -f4
Complete Working Project: What You Should Have Right Now
At this point you should have a fully working, production-hardened tunnel. Here’s the complete file layout if you followed the Docker Compose path from Step 7:
homelab-tunnel/
├── docker-compose.yml
├── .env # holds TUNNEL_TOKEN, never committed
└── cloudflared/
├── config.yml # ingress rules from Step 4
├── cert.pem # from `cloudflared tunnel login`
└── 7f3a1c9e-...json # tunnel credentials from Step 3
Before calling it done, run through this checklist:
- Both public and internal hostnames resolve and serve the correct backend service.
- The catch-all
http_status:404rule sits last inconfig.yml. - Internal hostnames (Grafana, admin panels, anything not meant to be public) sit behind an Access application with a working policy.
- The tunnel is running under Docker Compose or systemd with
restart: unless-stoppedorRestart=always, not in a foreground terminal session. - Credentials and config files are backed up somewhere outside the server itself, so a disk failure doesn’t force you to rebuild the tunnel from scratch.
- You’ve confirmed the current
cloudflaredversion against GitHub’s releases page and scheduled a recurring check, even a monthly cron job beats forgetting entirely.
That’s the complete project: a service reachable from anywhere on the internet, authenticated where it needs to be, running with zero inbound firewall rules, and surviving reboots without babysitting.
Cloudflare Tunnel vs Tailscale Funnel vs ngrok
All three solve the “expose a local service without opening a port” problem, but they come from different starting points. Tailscale Funnel builds on top of a mesh VPN you’re likely already running for device-to-device access, so Funnel is more of an add-on feature than a standalone product. ngrok leans developer-tool first, with a polished web inspector for replaying requests, which makes it a favorite for quick demos and webhook debugging rather than long-term production hosting. Cloudflare Tunnel sits inside a full edge and Zero Trust platform, so once you’re using it you also get DNS, WAF, and Access policies without adding another vendor.
| Feature | Cloudflare Tunnel | Tailscale Funnel | ngrok |
|---|---|---|---|
| Free tier | Yes, tunnels included on Cloudflare’s free plan | Available on Tailscale’s free/personal plan with limits | Yes, with rate limits and rotating URLs |
| Best fit | Homelabs and production apps needing a stable hostname | Teams already using Tailscale’s mesh network | Quick demos, webhook testing, short-lived shares |
| Access control | Built-in Zero Trust Access policies | Tailscale ACLs, ties to your tailnet identity | Basic auth / OAuth on paid plans |
| Static hostname | Yes, permanent CNAME on your own domain | Yes, on your tailnet’s assigned domain | Free tier gets a random rotating URL; paid tiers add reserved domains |
| Setup complexity | CLI + config.yml, moderate | Low if Tailscale is already installed | Lowest, single binary and one command |
If you already run Tailscale for device access across your network, Funnel is the path of least resistance since it reuses infrastructure you’ve already set up. If you need a public URL in the next ten seconds for a webhook test, ngrok wins on sheer speed to first request. If you want a permanent hostname on your own domain with authentication, WAF, and DNS bundled in, Cloudflare Tunnel is built for that specific job, and it’s the only one of the three where the free tier is realistic for something you’d actually run in production rather than just testing.
Cloudflare Tunnel Pricing in 2026
Cloudflare Tunnel itself carries no separate line-item charge, it’s bundled into Cloudflare Zero Trust. The free tier covers the setup in this tutorial completely: unlimited tunnels, unlimited hostnames, and Access policies for small teams. Third-party 2026 tracking of Cloudflare’s published Zero Trust plans puts the free tier at up to 50 seats, with paid Access and Gateway add-ons (Browser Isolation, CASB, DLP) priced per user on top of that. For exact current figures, check Cloudflare’s live Zero Trust pricing page before budgeting for a larger rollout, plans and seat counts do shift.
There’s no documented per-gigabyte charge for traffic passing through a tunnel, which is a meaningful difference from cloud egress pricing on AWS or GCP, where moving data out of the network is often the line item that actually hurts. What you pay for, if anything, is seats on the Zero Trust plan once you outgrow the free tier’s user cap or need enterprise-grade filtering. For a homelab or a small team’s internal tools, most setups following this tutorial never leave the free tier at all.
| Plan tier | Who it’s for | What you get |
|---|---|---|
| Zero Trust Free | Homelabs, solo devs, small teams | Cloudflare Tunnel, Access, up to roughly 50 users |
| Standalone Access | Teams needing app-level identity gating without Gateway filtering | Access policies, SSO integration, per-user pricing |
| Standalone Gateway | Teams wanting DNS/HTTP filtering on top of Tunnel | Secure web gateway features, per-user pricing |
| Zero Trust Standard | Organizations needing the full suite | Access + Gateway + Browser Isolation + DLP, annual billing |
5 Common Pitfalls (and How to Avoid Them)
Most Cloudflare Tunnel problems trace back to one of five mistakes. Catching these before you deploy saves an entire troubleshooting cycle.
- Losing the credentials JSON when moving hosts. The
<TUNNEL-ID>.jsonfile created in Step 3 is tied to that specific tunnel, not to your Cloudflare account in general. Copy it along withconfig.ymlwhenever you migrate the connector to a new machine, and don’t regenerate a fresh tunnel just because the file went missing, you’ll end up with orphaned DNS records pointing at a dead tunnel ID. - Placing the catch-all ingress rule too early. Ingress rules run top to bottom, and cloudflared stops at the first match. A
service: http_status:404rule anywhere but last silently swallows every hostname listed after it, and the failure looks identical to a broken origin, which sends people debugging the wrong layer entirely. - Pointing at localhost from inside a container. In Docker,
localhostinside the cloudflared container refers to that container, not your host or sibling containers. Use the Docker service name on a shared network instead, as shown in the Compose file in Step 7, or the connection will simply refuse with no useful error. - Skipping the DNS route step. Creating a tunnel and configuring ingress rules doesn’t automatically create a DNS record. Run
cloudflared tunnel route dnsfor every hostname or the domain simply won’t resolve to your tunnel, it’ll fall back to whatever (if anything) was there before. - Forgetting Access policies on internal tools. A working tunnel with no Access application is publicly reachable by anyone who guesses or finds the hostname through certificate transparency logs, which are public by design. Gate anything that isn’t meant to be fully public before you consider the tunnel done.
Troubleshooting cloudflared: 8 Issues and Fixes
Here’s what to check, in order, when something isn’t working.
- “could not read tunnel credentials” on startup. The path in
config.ymlundercredentials-filedoesn’t match where the JSON actually lives. Double-check the absolute path, especially after moving hosts or after copying files into a Docker volume mount that renamed the directory structure. - Hostname resolves but returns a Cloudflare 1033 error. The tunnel isn’t running or lost its connection to Cloudflare’s edge entirely. Check
cloudflared tunnel info homelab-tunneland restart the service if the connection count reads zero, a healthy tunnel shows four active connections as in the Step 6 output above. - Every route returns 404 even though the origin is healthy. Your catch-all ingress rule is placed before your real hostname rules. Reorder
config.ymlsohttp_status:404is the last entry, then restart cloudflared to pick up the change, it doesn’t hot-reload config edits by default. - TLS or certificate errors in the logs. Usually a clock skew issue rather than a real certificate problem. Check
timedatectlor your VM’s NTP sync, cloudflared’s handshake with Cloudflare’s edge is time-sensitive and a drift of even a few minutes can break it. - Access policy blocks a user who should be allowed. This isn’t a tunnel failure, it’s the policy evaluating correctly against the wrong rule. Check the Access application’s policy list in the dashboard, not the cloudflared logs, and confirm the identity provider actually returned the email or group you expected.
- Tunnel works from your laptop but not from a phone on cellular. Almost always a DNS caching issue on the mobile carrier’s resolver rather than anything wrong with cloudflared. Flush the local DNS cache on the phone or wait out the TTL rather than assuming the tunnel itself is broken.
- Docker container restarts in a loop. Check
docker logs cloudflaredfor the actual error, most often it’s a missing or malformedconfig.ymlmount, or the credentials file wasn’t copied into the mounted volume before the container first started. - Tunnel connectivity drops after a known Cloudflare incident. Cloudflare’s own status history includes a July 28, 2026 Tunnel availability incident lasting roughly two hours and fifty minutes, and a brief five-minute Anchorage point-of-presence disruption on May 28, 2026. Cloudflare’s guidance for the July incident was to restart affected
cloudflaredconnectors once the incident resolved. Check Cloudflare’s status page before assuming a local misconfiguration, third-party status mirrors sometimes lag or disagree with Cloudflare’s own incident log.
Advanced Tips for Production Tunnels
Once the basic setup is stable, a few adjustments make a real difference for anything beyond a hobby project. Run multiple cloudflared replicas pointed at the same tunnel for redundancy, Cloudflare load-balances across whichever connectors are online, so a second box in a different location keeps you up if one host goes down. Split high-traffic and low-traffic hostnames across separate tunnels rather than piling everything into one config.yml, it keeps ingress rule debugging simpler and limits blast radius if one tunnel’s credentials are ever compromised.
For anything handling real user data, pair the tunnel with Cloudflare’s WAF rules on the same zone, Access policies stop unauthorized users but a WAF rule stops malformed or malicious requests before they reach cloudflared at all. And build the version check from Step 12 into your actual monitoring stack rather than running it manually, an outdated cloudflared binary is a quiet source of missed bug fixes and, occasionally, missed security patches.
Naming conventions matter more than they seem to at first. Once you’re past two or three tunnels, homelab-tunnel stops being descriptive, name tunnels after the environment and purpose instead, something like prod-internal-apps or staging-preview-envs, so a teammate reading cloudflared tunnel list six months from now doesn’t have to guess what each one routes. Tag ingress rules with comments in config.yml too, YAML supports them, and a one-line note on why a rule exists saves real time during an incident.
Log retention is worth deciding upfront rather than after an incident. Both the systemd and Docker paths shown earlier send logs to their respective platform defaults, journald or the Docker JSON log driver, neither of which keeps history forever. Ship those logs somewhere durable, even a simple syslog forward to a log aggregator, if you need to reconstruct who accessed what after the fact. Combined with Access’s own audit log in the Zero Trust dashboard, that gives you two independent records instead of one.
Frequently Asked Questions
Is Cloudflare Tunnel really free?
Yes, for the setup covered in this tutorial. Tunnels, hostnames, and basic Access policies are included in Cloudflare’s Zero Trust free plan, which reportedly covers up to around 50 seats. Paid tiers only come into play if you need advanced Gateway filtering, Browser Isolation, or DLP features, or if your team grows past the free plan’s user cap.
Do I need a paid Cloudflare domain plan?
No. Any domain with its nameservers pointed at Cloudflare, even on the free zone plan, works for tunnel routing.
Can I run multiple tunnels on one server?
Yes. Each tunnel gets its own credentials file and config, and you can run several cloudflared processes or containers side by side, or list multiple hostnames under a single tunnel’s ingress rules as shown in Step 4. Most single-server setups are better off with one tunnel and several ingress rules, splitting into multiple tunnels only makes sense once you want separate credentials or separate failure domains for different services.
Does Cloudflare Tunnel work behind CGNAT?
Yes, and this is one of its biggest advantages over traditional port forwarding. Since the connection from your server to Cloudflare is always outbound, it works fine even if your ISP puts you behind carrier-grade NAT and you have no public IP at all.
How is this different from just using a reverse proxy like Nginx?
A reverse proxy still needs an inbound port open somewhere for the internet to reach it. Cloudflare Tunnel replaces that inbound requirement entirely with an outbound connection, and you can still run Nginx or Caddy behind it as your local reverse proxy for multiple internal services, the tunnel just becomes the thing that gets traffic to your reverse proxy in the first place rather than a router port forward.
What happens if my server loses internet access?
The tunnel drops and your hostname becomes unreachable until connectivity returns, cloudflared reconnects automatically once the network is back, no manual restart needed under normal conditions.
Can I use Cloudflare Tunnel for TCP services, not just HTTP?
Yes, cloudflared supports TCP and SSH tunneling in addition to HTTP ingress rules, using a different service definition in config.yml, useful for exposing things like a private SSH bastion without an open port 22.
How do I remove a tunnel completely?
Stop the running service, then run cloudflared tunnel delete homelab-tunnel, and manually remove the CNAME records created in Step 5 from your DNS zone since deletion doesn’t always clean those up automatically.
Related Coverage
- Cloudflare Workers Setup: 12 Steps, 30 Min [2026]
- Tailscale vs WireGuard: Free vs $8/User [2026]
- WireGuard vs OpenVPN: 3-4x Faster [2026]
- Container Security Hardening: 12 Steps, 90 Min [2026]
- ECS vs EKS: $0 vs $438/Mo Control Plane [2026]
- Cloudflare Logs 13 Outages in 8 Days as R2 Falters [2026]
- More cloud computing coverage




