Join a home or office Node (no public IP)
Cloudflare Tunnel is the default inbound path for residential, CGNAT, and office boxes — the agent itself is pull-only and still joins the fleet without it.
Join a home or office Node (no public IP)
A laptop on Wi-Fi, an office PC, or a GPU box behind a consumer ISP usually has no
public IPv4 — RFC1918, CGNAT (100.64.0.0/10), or a firewall you do not control.
HostSSH still onboards that machine. The agent is pull-only: it dials the control
plane, heartbeats, and claims jobs. Nothing inbound is required for the fleet loop.
Inbound is a separate problem: publishing an app, exposing a local port, or reaching
a dashboard on that box from the internet. For those, Cloudflare Tunnel is the
default path. Native reverse-tunnel (no Cloudflare) is the later vendor-exit; until
that ships, named cloudflared is what we document and what hostssh doctor checks.
What you get without a tunnel
After install + license activate:
- The Node appears in the fleet and heartbeats.
- Captures, restores, deploys, and prune jobs run (the agent pulls them).
- You can SSH to the box on your LAN the usual way.
You cannot yet: put a public hostname on an app that lives only on that Node, or use Expose to publish a Node-local port. Those need the tunnel below.
1. Install cloudflared on the Node
On Debian/Ubuntu:
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 $(lsb_release -cs) main" \
| sudo tee /etc/apt/sources.list.d/cloudflared.list
sudo apt-get update && sudo apt-get install -y cloudflared
Or grab a release binary from Cloudflare and put it on PATH. Confirm:
cloudflared --version
2. Create a named tunnel and install it as a service
From the Node (one-time, uses your Cloudflare account):
cloudflared tunnel login
cloudflared tunnel create hostssh-home
cloudflared tunnel route dns hostssh-home apps.example.com # optional hostname
sudo cloudflared service install
sudo systemctl enable --now cloudflared
The service reads /etc/cloudflared/config.yml. HostSSH's expose job can merge
hostname → localhost:<port> ingress into that file and reload the service; you can
also hand-edit ingress. GPU / long-lived inference origins need the long proxy
timeouts documented in GPU workloads.
3. Point the agent at the tunnel
Set the tunnel name (not the token) on the agent:
# /etc/hostssh/agent.env (or the systemd drop-in for hostssh-agent)
HOSTSSH_CF_TUNNEL=hostssh-home
Then restart the agent:
sudo systemctl restart hostssh-agent
Residential / office installs can also set HOSTSSH_TUNNEL_REQUIRED=1. That does
not fail doctor if cloudflared is missing (a box that only heartbeats is still
valid) — it changes the warning so you cannot miss it.
4. Prove it with doctor
hostssh doctor
Rows that matter here:
| Check | Healthy | What a warn means |
|---|---|---|
reachability | ok with a public address | private/CGNAT only — expected at home; inbound needs the tunnel |
cloudflared | binary + HOSTSSH_CF_TUNNEL + cloudflared.service active | Missing binary, unset name, or service not running |
A warn never fails doctor by itself. A fail (Docker missing, engine scripts gone)
still exits 1.
hostssh doctor --json
is the machine-readable form (same checks).
5. Expose a port from the dashboard
Once doctor is green on cloudflared, Access → Expose (or an expose job) publishes
a Node-local port at the public hostname the tunnel already serves. Teardown is
Unexpose. Live proof of a named tunnel on a specific box is still an operator
window — the code path is what this page describes.
Honest limits
- The control plane does not provision your Cloudflare account. You create the
named tunnel; we consume
HOSTSSH_CF_TUNNEL. - cloudflared is not in
install.sh. It is optional for VPS nodes with a public IP and Traefik. Home/office nodes that want inbound should install it themselves (Apache-2.0, not in the HostSSH binary). - Firewall: allow the agent's egress first (see Hardening). A
default-deny inbound policy is correct on a tunnel-first Node — do not punch
:80on a residential NAT.