Guides Cloudflare Tunnel

Cloudflare Tunnel from scratch: publish a service without opening a port

Install cloudflared, create a locally-managed tunnel, write the ingress rules, route DNS and run it as a service. Includes what each step is for.

By Micheal Garner. Published 1 November 2026. 5 minute read.

A Cloudflare Tunnel lets you publish a web service, an SSH server or a TCP port to the internet without opening anything on your router. A small program called cloudflared runs next to your service and makes outbound connections to Cloudflare. Visitors talk to Cloudflare, and Cloudflare sends their requests back down those connections to you.

That one change of direction is the whole trick. Your firewall never needs an inbound rule, your home IP address is not in public DNS, and it works from behind carrier-grade NAT where port forwarding is impossible.

This guide builds a locally-managed tunnel, where the configuration lives in a file on your server. Cloudflare also offers remotely-managed tunnels, which are configured in the dashboard. The differences are covered near the end.

What you need

  • A domain on Cloudflare (we use example.com below; use your own).
  • A Linux server or VM that can reach the service you want to publish.
  • Outbound access to Cloudflare on port 7844, over both UDP (QUIC, preferred) and TCP (the fallback).

Before you start: everything below uses the hostname files.example.com and a service listening on localhost:8080. Replace those with your own hostname and port. Nothing else needs to change.

Install cloudflared

On Debian and Ubuntu, use Cloudflare’s package repository so that apt keeps cloudflared updated. The repository details are on pkg.cloudflare.com, which is the place to check if these commands ever change.

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
cloudflared --version

Installing from the repository is worth the extra three lines. A binary dropped into /usr/local/bin will never be patched unless you remember to do it, and a tunnel connector is internet-facing software.

Log in and create the tunnel

cloudflared tunnel login

This prints a URL. Open it in a browser, pick the domain you want to use, and authorise. cloudflared saves a certificate called cert.pem in ~/.cloudflared/. That certificate lets the command line create tunnels and DNS records for your account, so treat it like a credential.

Now create the tunnel:

cloudflared tunnel create homelab

The output includes the tunnel’s UUID and the path of a credentials file, ~/.cloudflared/<UUID>.json. The credentials file is what the running tunnel authenticates with. Anyone who has it can run your tunnel, so keep it readable only by the account that runs cloudflared. List your tunnels any time with:

cloudflared tunnel list

Write the configuration

Create ~/.cloudflared/config.yml. This example publishes two services and refuses everything else:

tunnel: 6ff42ae2-765d-4adf-8112-31c55c1551ef
credentials-file: /home/anon/.cloudflared/6ff42ae2-765d-4adf-8112-31c55c1551ef.json

ingress:
  - hostname: files.example.com
    service: http://localhost:8080
  - hostname: ssh.example.com
    service: ssh://localhost:22
  - service: http_status:404

Use the UUID printed by your own tunnel create command in both places. The one above is only an example.

Three things in that file matter more than they look:

  1. Rules are checked from top to bottom and the first match wins. Put specific hostnames above general ones.
  2. The last rule must match everything. cloudflared refuses to start a config that has ingress rules without a catch-all. http_status:404 is the conventional choice, and it means a request for a hostname you did not list gets a plain 404 instead of reaching something by accident.
  3. The service scheme decides how cloudflared talks to your origin. http:// and https:// are for web apps, ssh:// and rdp:// for those protocols, and tcp:// for anything else.

Check the file before you run it:

cloudflared tunnel ingress validate
cloudflared tunnel ingress rule https://files.example.com

The second command tells you which rule a given URL would hit. It is the fastest way to find out that your wildcard rule is shadowing a specific one.

Point DNS at the tunnel

Each public hostname needs a DNS record that sends traffic to the tunnel. The command creates it for you:

cloudflared tunnel route dns homelab files.example.com
cloudflared tunnel route dns homelab ssh.example.com

Each command adds a proxied CNAME that points at <UUID>.cfargotunnel.com. If it complains that a record already exists, an old A, AAAA or CNAME is using that hostname. Delete it in the dashboard or choose another name.

Run it once, by hand

Before turning it into a service, run it in the foreground and watch it connect:

cloudflared tunnel run homelab

You should see several connections registered to Cloudflare’s edge. Browse to https://files.example.com and you should reach your service. Stop the tunnel with Ctrl+C. Running it by hand first means any mistake shows up as a readable error rather than a failed systemd unit.

Run it as a service

A tunnel that stops when you close your SSH session is not much use. Copy the config and credentials to a system location, so the service does not depend on anyone’s home directory:

sudo mkdir -p /etc/cloudflared
sudo cp ~/.cloudflared/config.yml /etc/cloudflared/config.yml
sudo cp ~/.cloudflared/6ff42ae2-765d-4adf-8112-31c55c1551ef.json /etc/cloudflared/
sudo sed -i 's#/home/anon/.cloudflared/#/etc/cloudflared/#' /etc/cloudflared/config.yml
sudo cloudflared --config /etc/cloudflared/config.yml service install
sudo systemctl enable --now cloudflared
sudo systemctl status cloudflared

The sed line rewrites the credentials-file path so it points at the copy in /etc/cloudflared. Without it, the service would look for a file under a home directory it may not be allowed to read.

Watch the logs with journalctl -u cloudflared -f. After a config change, restart with sudo systemctl restart cloudflared.

When you want more than the stock unit gives you, such as a dedicated user and a locked-down sandbox, see hardening cloudflared with systemd.

Locally-managed or remotely-managed?

Locally-managed Remotely-managed
Where config lives config.yml on your server Cloudflare dashboard
Change a route Edit the file, restart the service Click, and it applies in seconds
Version control Easy: it is a file Harder: the dashboard is the source of truth
Install cloudflared service install with a config path The dashboard gives you a one-line install that includes a token

Neither is wrong. A file you can commit to Git suits people who like infrastructure as code. The dashboard suits people who add a hostname every other week and do not want to SSH in each time. Be aware that a tunnel is one or the other: once you manage routes from the dashboard, the local file stops being the source of truth.

Next steps

A published hostname is open to the world until you say otherwise. If the service is for you or a small team, put a login in front of it with Cloudflare Access. If you want shell access without an open SSH port, read SSH through a Cloudflare Tunnel. And when something does not connect, debugging tunnel errors maps each symptom to its cause.

Sources: Cloudflare’s documentation on creating a locally-managed tunnel, the tunnel configuration file and running a tunnel as a service on Linux.