My ISP would not give me a public IP, so I stopped asking for one

I once spent an evening forwarding port 443 on a home router, only to find the router's "WAN address" was a private range. My provider shared one public IPv4 address between many customers. No port forward on my side could ever work, because the traffic never reached my router. That setup is called carrier-grade NAT, or CGNAT, and it is common on home and mobile connections.

The usual answer is a tunnel: a small program on your server dials out to a provider, and visitors come in through the provider. Levelrail, the self-hosted deploy platform I work on, can run a Cloudflare Tunnel connector for you. This post covers what I could verify and what I could not, because the honest answer on that second part matters for your decision.

TL;DR

Question Answer Verified how
Need an open inbound port? No, the connector dials out Cloudflare docs
Does Levelrail run the connector? Yes, one cloudflared container I ran it locally
Did a real tunnel carry traffic? Not tested by me No Cloudflare account used
Is the feature finished? The docs call it experimental Levelrail docs
Do I still need a certificate? Yes, use DNS-01 Levelrail docs

What I did not test, up front

I did not create a Cloudflare tunnel, did not route a hostname through one and did not load a page over one. A real tunnel needs a token from a Cloudflare account, and I did not use one. The Levelrail docs say the same about themselves: the feature is built and covered by reconciler and API unit tests, but has not been proven on a real host with a real Cloudflare account. Everything about live traffic below comes from the docs, not from me.

How a tunnel gets around CGNAT

Cloudflare describes its tunnel as an outbound-only connection model, and says it connects resources without a publicly routable IP address. <!-- source: https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/ --> Its guidance for firewalls is to allow only those outbound connections and block inbound traffic. That is exactly what CGNAT permits: your server can reach out, and nobody can reach in.

Public hostnames are routed on Cloudflare's side. In the Zero Trust dashboard you add a published application, pick a subdomain and domain, and set a service URL that the connector can reach. <!-- source: https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/get-started/create-remote-tunnel/ --> The domain must already be on Cloudflare, and the application is publicly reachable until you add an Access policy.

What Levelrail adds

Levelrail starts one container named <brand>-cloudflared from cloudflare/cloudflared, running tunnel --no-autoupdate run, and passes your tunnel token as the TUNNEL_TOKEN environment variable. The token is stored encrypted and is never returned by the API or CLI (docs/cloudflare-tunnel.md). Levelrail does not configure Cloudflare for you. The hostname-to-service mapping is yours to set in their dashboard.

The commands, once you have a real token:

levelrail-cli cloudflare-tunnel set --cf-tunnel-token TOKEN levelrail-cli cloudflare-tunnel get 

The feature is off by default and has to be switched on first. I did it with the APP_EXPERIMENTAL=cloudflare-tunnel seed on a local dev instance, and the docs also describe a dashboard switch in Settings.

What I ran, with a fake token

To see the reconciler work without an account, I gave it a deliberately invalid token on a throwaway local control plane (a build of the v1.0.1 release branch, on a Mac with Docker). The container appeared, pinned to cloudflare/cloudflared:2026.10.7, with the command tunnel --no-autoupdate run, no host port bindings, and restart policy no.

$ levelrail-cli cloudflare-tunnel get enabled: true has_token: true status: connected $ docker logs Levelrail-cloudflared Provided Tunnel token is not valid. 

Two things stand out. First, no inbound port was published, which matches the pitch. Second, the status said connected while the container had already exited with the invalid-token error. That is the documented behavior: connected reports that the local container is running, not Cloudflare's view of the tunnel, so you confirm health in the Zero Trust dashboard too (docs/cloudflare-tunnel.md). In my run it was a stale reading of a container that was crash-exiting. I would not alert on that field alone.

I then ran levelrail-cli cloudflare-tunnel disconnect, which deletes the stored token and removes the container, and confirmed no cloudflared container was left on the machine.

The part people forget: certificates

A tunnel solves reachability. Your certificate is a separate problem. Behind CGNAT the HTTP-01 check cannot reach port 80, so the certificate request fails. Levelrail's docs suggest DNS-01, where the certificate authority checks a TXT record in your zone, written through your DNS provider's API (docs/tutorials/dns-01-behind-cgnat.md):

levelrail-cli domains cloudflare-dns set --cf-api-token TOKEN levelrail-cli settings ingress dns01 enable 

The same tutorial states that DNS-01 for single hosts is not yet verified against a real certificate authority, and marks it as a pending run. I did not run it either. If your tunnel terminates TLS at Cloudflare and sends plain HTTP to a local service, you may not need an origin certificate for that path at all. That is a Cloudflare configuration choice, and I did not test it.

Scope: the control plane, not necessarily your apps

The Levelrail docs say the built-in tunnel setting covers the control plane, and that for app traffic you should look at the proxy in front of apps or another route. They also describe pointing a Cloudflare public hostname at your Levelrail ingress address, which is how you would expose apps. I did not verify that path end to end, so check it on a test hostname before you tell anyone the URL.

Other ways in, and where each wins

Option Needs an open port Where it wins Where it loses
Cloudflare Tunnel No Free to start, no VPS to run, Cloudflare handles the edge Traffic goes through a third party, domain must be on Cloudflare
Public IPv6 Yes, firewall allow Direct, nothing in the middle Only if your ISP gives you IPv6
Small VPS as a relay On the VPS You control the whole path You pay for and patch another server
Run behind the orange cloud only Yes Caching and WAF Needs a reachable origin, so no use behind CGNAT

The last row is from Levelrail's own page on running behind Cloudflare, which says the proxy does not make your server unreachable and needs the origin to be reachable (docs/behind-cloudflare.md). That is a different tool from a tunnel.

A private way to reach a service

One related command I did not need a tunnel provider for: levelrail-cli apps tunnel web --port 9000 opens a loopback port on your laptop and relays through the control plane to a container port that is not published. It is for admin ports, not public traffic (docs/app-tunnel.md). I read this page but did not run it for this post.

FAQ

Can I self-host behind CGNAT with no port forwarding?

Yes, with an outbound-only tunnel, according to Cloudflare's docs. What I proved is the Levelrail side: the connector container starts with no published ports. I did not prove live traffic.

Does the tunnel hide my server's address?

For proxied or tunneled hostnames, public DNS answers with a Cloudflare address. Levelrail's own page warns that mail records, direct subdomains and old DNS history can still reveal a server address (docs/behind-cloudflare.md).

What if the tunnel token leaks?

The docs say anyone holding it can run a connector for your tunnel. Rotate it in Cloudflare, then run set --cf-tunnel-token NEW.

What I would do next

I would create a real tunnel on a test domain, record the hostname that works end to end, and only then write the version of this post that says it works. Until that run exists, I would call this setup plausible and well documented, not proven. If you have run the Levelrail tunnel against a real account, what broke first?

If this saved you time, a star on the repo helps others find it.

GitHub logo glincker / levelrail

Self-hosted alternative to Vercel and Coolify. Git push, HTTPS, logs, rollback. No Kubernetes.

Levelrail logo

Levelrail

Your own Vercel, on your own servers.
Push to deploy with HTTPS, logs, metrics and one-step rollback. One Go binary, no Kubernetes. Apache 2.0.

GitHub stars License: Apache 2.0 Latest release CI status Last commit levelrail-cli on npm MCP server Discord

Website · Docs · Install · Compare · FAQ · Roadmap · Discussions

English · 简体中文 · Español · Português (BR) · Deutsch · Français · 日本語

Levelrail: your own Vercel, on your server. The lightweight, self-hosted alternative to Vercel, Heroku and Coolify.

Turn any Linux server into a private cloud: Git deploys, HTTPS, databases with verified backups, metrics, logs and alerts, without Kubernetes and without a monitoring stack to bolt on. Pre-1.0: single-server is production ready, multi-node is beta (Feature status).

Levelrail dashboard tour: fleet overview, apps list, deploy history and built-in metrics










200 MB

idle RAM, 1.5% of one CPU
measured, method included

1 binary

Go control plane, Docker runtime
no Kubernetes

360

one-click service templates
smoke-tested

240+

MCP tools for coding agents
scoped, read-only by default

Install

curl -fsSL https://levelrail.com/install.sh | sudo sh

Needs Linux (amd64 or arm64) with systemd and root; the installer adds Docker. Open ports…




Sources checked