Cloudflare Tunnel is the solution to the single biggest obstacle for home web hosting: the ISP blocking inbound connections. Instead of waiting for inbound traffic that the router will refuse, the tunnel reverses the connection — your server reaches out to Cloudflare, and Cloudflare forwards visitors' requests back through that outbound connection. No open ports. No port forwarding. Works behind Virgin Media, CGNAT, or any other restrictive ISP.
What this chapter covers: How Cloudflare Tunnel works architecturally — why outbound connections bypass ISP restrictions. Installing cloudflared on your server. The two setup methods: dashboard (GUI) and CLI. The config.yml file and ingress rules. Running the tunnel as a permanent systemd service. Adding multiple services through one tunnel. Troubleshooting common failure modes. This chapter assumes DNS is managed by Cloudflare (Chapter 3) — the tunnel cannot create CNAME records in IONOS.
How Cloudflare Tunnel Works
Traditional web hosting requires your router to accept inbound connections on port 80/443 and forward them to your server. ISPs like Virgin Media block these inbound connections — it's why every approach in this session's troubleshooting hit a wall.
Cloudflare Tunnel inverts the model. A small daemon called cloudflared runs on your server and establishes an outbound encrypted connection to Cloudflare's edge network. When a visitor requests osztromok.com, Cloudflare receives it at their edge, then forwards it back through the existing outbound connection to your server. Your server's response travels back the same way.
Traditional hosting (blocked by Virgin Media):
Visitor ──────[port 80]──────▶ Router ──▶ Server
❌ Blocked
Cloudflare Tunnel (works because it's outbound):
Server ──outbound HTTPS──▶ Cloudflare Edge
│
Visitor ──HTTPS──▶ Cloudflare Edge
│
(routes through existing tunnel)
│
▼
Your Server
Key insight: the server initiates the connection TO Cloudflare.
ISPs and NAT/firewalls allow outbound connections freely.
No port forwarding. No open inbound ports. No router config needed.
The tunnel uses QUIC (or falls back to HTTPS) for the connection between cloudflared and Cloudflare's edge. It maintains multiple persistent connections for redundancy. If cloudflared restarts, it reconnects automatically.
Prerequisites before starting this chapter: 1. DNS for osztromok.com is managed by Cloudflare (Chapter 3 complete)
2. You have a Cloudflare account with osztromok.com added
3. Apache is running on your server (already confirmed — port 80, systemctl status apache2)
Configure everything through Cloudflare's web UI. The dashboard creates the tunnel, generates credentials, and creates DNS records automatically.
Pros:
Guided — less chance of mistakes
DNS records created automatically
Easy to add/edit public hostnames later
Tunnel visible and manageable in the UI
Cons:
Requires browser access to Cloudflare
Config stored in Cloudflare, not on your server
CLI Method (More control)
Create the tunnel via the cloudflared command line, write a local config.yml, run as a systemd service.
Pros:
Config lives on your server — easy to back up
More control over ingress rules
Better for multiple services / complex routing
Easier to version control
Cons:
More steps — credentials, config file, DNS
Need to create DNS records manually (or via CLI)
This chapter covers both methods. The dashboard method gets you running fastest; the CLI method gives you a setup that's easier to maintain long-term. Both end up with the same result.
Method 1 — Dashboard Setup
Setup Walkthrough · Dashboard Method
Create a tunnel through the Cloudflare Zero Trust dashboard and connect osztromok.com to Apache on your server.
1
Navigate to Zero Trust. In the Cloudflare dashboard, click Zero Trust in the left sidebar (or go to one.dash.cloudflare.com). If prompted to set up a team name, enter anything — it doesn't matter for tunnel use.
2
Create the tunnel. Go to Networks → Tunnels → Create a tunnel. Select Cloudflared as the connector type. Give the tunnel a name — something like home-server. Click Save tunnel.
3
Install the connector. Cloudflare will show you an install command. Select Debian as the OS. It will look like:
$ sudo cloudflared service install eyJhIjoiYWJjMTIzLi4u...
# This long token contains your tunnel credentials.
# Run this on your server — it installs cloudflared as a systemd service
# and writes the credentials automatically. No separate config.yml needed.$ sudo systemctl start cloudflared
$ sudo systemctl status cloudflared
After running this, the connector dot in the Cloudflare dashboard should turn green.
4
Add a public hostname. In the tunnel configuration, go to the Public Hostname tab and click Add a public hostname:
— Subdomain: leave blank (for root domain) or enter www
— Domain: select osztromok.com
— Type: HTTP
— URL:localhost:80
Click Save. Cloudflare automatically creates the DNS CNAME record for you.
5
Test it. Open a browser (or use curl from another machine) and visit http://osztromok.com. If Apache is running and the tunnel is healthy, you should see your site.
$ curl -I http://osztromok.com
HTTP/1.1 200 OKServer: Apache/2.4.57 (Debian)CF-Ray: 7f3a...← CF-Ray header confirms traffic went through Cloudflare
The dashboard method stores the tunnel token inside the systemd service file rather than a separate config.yml. This is fine for a single-service setup. If you later need multiple services (e.g. a separate app on port 8080), switch to the CLI method or add additional public hostnames through the dashboard.
Method 2 — CLI Setup with config.yml
Step 1 — Authenticate cloudflared with your Cloudflare account
$ cloudflared tunnel login
Please open the following URL and log in with your Cloudflare account:https://dash.cloudflare.com/argotunnel?callback=...# Open this URL in a browser, log in, and select osztromok.com.
# cloudflared saves a certificate to ~/.cloudflared/cert.pemYou have successfully logged in.If you wish to copy your credentials to a server, they have been saved to:/home/philip/.cloudflared/cert.pem
Step 2 — Create the tunnel
$ cloudflared tunnel create home-server
Tunnel credentials written to /home/philip/.cloudflared/abc12345-...-def6-7890-ghij-klmnopqrstuv.json.Created tunnel home-server with id abc12345-def6-7890-ghij-klmnopqrstuv# Note the tunnel ID — you'll need it in config.yml.
# The .json file is the tunnel credential — keep it safe.# Move credentials to /etc/cloudflared/ so the system service can read them$ sudo mkdir -p /etc/cloudflared
$ sudo cp ~/.cloudflared/abc12345-*.json /etc/cloudflared/
$ sudo chmod 600 /etc/cloudflared/abc12345-*.json
# List tunnels to confirm it was created$ cloudflared tunnel list
ID NAME CREATED STATUSabc12345-def6-7890-ghij-klmnopqrstuv home-server 2026-06-14T21:00:00Z inactive
Step 3 — Create the config.yml file
# /etc/cloudflared/config.ymltunnel: abc12345-def6-7890-ghij-klmnopqrstuv# your tunnel IDcredentials-file: /etc/cloudflared/abc12345-def6-7890-ghij-klmnopqrstuv.jsoningress:# Route the root domain to Apache
- hostname: osztromok.comservice: http://localhost:80# Route www to the same place
- hostname: www.osztromok.comservice: http://localhost:80# Catch-all rule — REQUIRED, must be last# Any request that doesn't match a hostname above gets a 404
- service: http_status:404
The catch-all rule is mandatory. config.yml must end with an ingress rule that has no hostname — it catches requests that don't match any other rule. Without it, cloudflared will refuse to start. The value http_status:404 returns a 404 for unmatched requests, which is the correct behaviour.
Step 4 — Create DNS records for the tunnel
# Tell Cloudflare to create a CNAME DNS record pointing the hostname
# to this tunnel. Run once for each hostname in your ingress rules.$ cloudflared tunnel route dns home-server osztromok.com
2026/06/14 21:05:00 Added CNAME osztromok.com which will route to this tunnel tunnelID=abc12345...$ cloudflared tunnel route dns home-server www.osztromok.com
2026/06/14 21:05:01 Added CNAME www.osztromok.com which will route to this tunnel tunnelID=abc12345...# Verify the records were created in Cloudflare DNS$ dig osztromok.com +short
abc12345-def6-7890-ghij-klmnopqrstuv.cfargotunnel.com.# CNAME (Cloudflare flattens this)
Step 5 — Test the tunnel before installing as a service
# Run in the foreground first — you'll see connection logs and can confirm it works$ cloudflared tunnel --config /etc/cloudflared/config.yml run
2026/06/14 21:06:00 Starting metrics server on 127.0.0.1:...2026/06/14 21:06:00 Registered tunnel connection connIndex=0 ip=198.41.200.132026/06/14 21:06:00 Registered tunnel connection connIndex=1 ip=198.41.192.472026/06/14 21:06:00 Registered tunnel connection connIndex=2 ip=198.41.200.132026/06/14 21:06:00 Registered tunnel connection connIndex=3 ip=198.41.192.47# 4 connections registered = healthy tunnel. Open a browser and test now.
# Press Ctrl+C to stop once confirmed working.
Step 6 — Install as a systemd service
# Install cloudflared as a system service (reads /etc/cloudflared/config.yml)$ sudo cloudflared --config /etc/cloudflared/config.yml service install
2026/06/14 21:10:00 Installing cloudflared client as a systemd service2026/06/14 21:10:00 cloudflared client was successfully installed as a systemd service# Enable and start the service$ sudo systemctl enable cloudflared
$ sudo systemctl start cloudflared
$ sudo systemctl status cloudflared
● cloudflared.service - cloudflared Loaded: loaded (/etc/systemd/system/cloudflared.service) Active: active (running) since Sun 2026-06-14 21:10:05 BST; 5s ago# "active (running)" = tunnel is up and will restart automatically on reboot
Routing Multiple Services Through One Tunnel
A single tunnel can serve multiple domains and subdomains to different backend services. You don't need a separate tunnel for each service — just add more ingress rules to config.yml and create the corresponding DNS records.
# After editing config.yml, reload the service$ sudo systemctl restart cloudflared
# Create DNS records for any new hostnames you added$ cloudflared tunnel route dns home-server blog.osztromok.com
$ cloudflared tunnel route dns home-server grafana.osztromok.com
Troubleshooting
# Check the service status$ sudo systemctl status cloudflared
# Watch live logs$ sudo journalctl -u cloudflared -f
# Check the tunnel is registered in Cloudflare$ cloudflared tunnel info home-server
NAME: home-serverID: abc12345-...CONNECTIONS: 4 active ← healthy: 4 connections to Cloudflare edge# Validate config.yml syntax before restarting$ cloudflared tunnel --config /etc/cloudflared/config.yml ingress validate
Validating rules from /etc/cloudflared/config.ymlOK
Service starts but website shows "ERR_CONNECTION_REFUSED" or "522" error
Apache isn't running or isn't listening on port 80. Check: systemctl status apache2 and ss -tlnp | grep :80. The tunnel forwards traffic to localhost:80 — if nothing is there to receive it, you get a connection error.
Tunnel is "active" in systemctl but website doesn't load — just times out
DNS record not pointing to the tunnel. Check: dig osztromok.com +short — should return a Cloudflare IP or the tunnel CNAME. If it still returns your home IP (82.2.236.221), the CNAME record wasn't created. Run cloudflared tunnel route dns home-server osztromok.com and check the Cloudflare DNS dashboard.
SSL/TLS mode mismatch. In Cloudflare dashboard → SSL/TLS → Overview, check the mode. Flexible = Cloudflare speaks HTTPS to visitors, HTTP to your server (no server cert needed). Full = requires HTTPS on your server. Full (Strict) = requires a valid signed certificate on your server. If you haven't set up Let's Encrypt yet, use Flexible for now.
cloudflared service won't start — "no such file or directory" for credentials
The credentials JSON file path in config.yml doesn't match the actual file. Check: ls /etc/cloudflared/ and verify the filename matches what's in the credentials-file: line exactly, including the full UUID.
"config.yml: ingress: rule #X has no service" — tunnel refuses to start
The catch-all rule at the end of ingress is missing or malformed. It must be the last rule and must have no hostname: key — just - service: http_status:404. Check indentation too — YAML is whitespace-sensitive; use spaces, never tabs.
Tunnel was working, stopped after router reset / reboot
The service may not be enabled to start on boot. Check: systemctl is-enabled cloudflared. If it says disabled, run sudo systemctl enable cloudflared. The tunnel itself doesn't depend on port forwarding — it should survive router changes as long as outbound internet access is available.
Quick Reference — Chapter 4
Command
Purpose
cloudflared tunnel login
Authenticate with your Cloudflare account — required for CLI method
cloudflared tunnel create NAME
Create a new named tunnel and generate credentials JSON
cloudflared tunnel list
List all tunnels on your account and their status
cloudflared tunnel route dns NAME host
Create a CNAME DNS record in Cloudflare pointing a hostname to this tunnel
cloudflared tunnel --config /etc/cloudflared/config.yml run
Run the tunnel in the foreground for testing — Ctrl+C to stop
cloudflared tunnel ingress validate
Check config.yml for syntax errors before restarting the service
cloudflared tunnel info NAME
Show tunnel details and number of active connections to Cloudflare edge
sudo cloudflared service install
Install cloudflared as a systemd service (reads /etc/cloudflared/config.yml)
sudo systemctl enable cloudflared
Ensure tunnel starts automatically on every boot
sudo journalctl -u cloudflared -f
Watch cloudflared logs live — first place to look when troubleshooting
File / Location
Purpose
/etc/cloudflared/config.yml
Main tunnel config — tunnel ID, credentials path, ingress rules
/etc/cloudflared/<uuid>.json
Tunnel credentials — treat like a password, don't share or commit to git
~/.cloudflared/cert.pem
Account-level certificate from cloudflared tunnel login