I went from ngrok (ephemeral URLs, per-project tunnels) to Tailscale Funnel (sidecar cruft, occasional failures) to Cloudflare Tunnels with Traefik (great, but too many manual steps for LLM agents). So I wrapped the whole thing in a CLI:
devtun add myappgives youhttps://myapp.dev.yourdomain.comwith automatic SSL, stable URLs, and zero per-project configuration.
I work on a lot of projects at the same time. Most of them run locally in Docker, and most of them need to be reachable from the internet at some point - webhook testing, mobile device testing, sharing a work-in-progress with someone, or integrating with third-party APIs that demand HTTPS callbacks.
For a long time I used ngrok. It works, but every time I switch projects I'm starting a new tunnel, getting a new URL, updating environment variables, and reconfiguring whatever service needs to call me back. For one project, that's fine. For six or seven running concurrently, it gets old fast.
I moved to Tailscale Funnel next. I'd either run it inside each Docker container or as a sidecar service. This was better - no ephemeral URLs, no separate tunnel process to manage - but it meant adding Tailscale configuration to every app repo. Extra cruft in my compose files, and every now and then the funnel would just stop working and need a bit of poking to come back. Not a lot of effort, but enough friction to be annoying.
Then I discovered that Cloudflare Tunnels paired with Traefik could solve the whole thing cleanly. One tunnel, one reverse proxy, stable hostnames, real SSL certificates, no per-project configuration beyond a few Docker labels. I set it up manually and it worked great.
But "set it up manually" was the problem. I use LLM agents to build most of my projects now, and I want them to be as autonomous as possible. Telling an agent "go to the Cloudflare dashboard, enable SaaS, create a custom hostname, add a DNS record, then edit your docker-compose.override.yml with these Traefik labels" is not autonomy. I needed a single command an agent could run.
So I built devtun.
How It Works
The architecture is straightforward. Three components work together:
- Cloudflare Tunnel maintains a persistent outbound connection from your machine to Cloudflare's edge. No inbound ports needed.
- Traefik acts as a local reverse proxy, auto-discovering Docker containers and routing traffic based on hostname labels.
- Cloudflare for SaaS issues individual SSL certificates per hostname - up to 100 for free - so each project gets its own valid HTTPS certificate at the edge.
When a request hits https://myapp.dev.example.com, Cloudflare terminates TLS, sends the request through the tunnel to your machine, Traefik routes it to the right container based on the hostname, and the response flows back. Your containers only need to serve plain HTTP.
The Setup
One-time setup takes a few minutes:
npm install -g devtun
devtun setup
The setup wizard walks you through connecting your Cloudflare account, creating the tunnel, and configuring DNS. It's idempotent - if something fails halfway through, just run it again.
Adding Projects
This is the part I actually care about. From your project directory:
devtun add myapp
That's it. devtun will:
- Create a DNS record for
myapp.dev.yourdomain.com - Issue an SSL certificate via Cloudflare for SaaS
- Detect your
docker-compose.ymland find the right service and port - Generate a
docker-compose.override.ymlwith the Traefik routing labels - Add your container to the shared tunnel network
The URL is stable. It doesn't change between restarts. It doesn't expire after two hours.
What I Like About This Approach
One tunnel for everything. A single Cloudflare Tunnel serves all your projects. Traefik handles the per-project routing locally. No juggling multiple tunnel processes.
Stable URLs. Each project gets a permanent subdomain. Configure your webhook URLs once and forget about them.
Real SSL certificates. Not self-signed, not Let's Encrypt on your local machine. Cloudflare issues proper edge certificates per hostname. Browsers are happy, APIs are happy.
Works with existing Docker Compose setups. devtun generates an override file rather than modifying your existing docker-compose.yml. Your project configuration stays clean.
Minimal dependencies. The only npm dependency beyond Node.js builtins is the yaml package for parsing compose files.
Cloudflare for SaaS: The Key Insight
The non-obvious piece of this puzzle is Cloudflare for SaaS. Normally, Cloudflare's Universal SSL only covers *.example.com - it won't cover *.dev.example.com because that's a subdomain of a subdomain. You'd need an expensive Advanced Certificate or a wildcard from another provider.
Cloudflare for SaaS solves this differently. It's designed for SaaS platforms that need SSL on customer domains, but it works perfectly for this use case too. Each hostname gets its own certificate, issued automatically when you register it. And the first 100 are free.
Getting Started
The tool is available on npm:
npm install -g devtun
You'll need a Cloudflare account with a domain, and a custom API token with Zone Settings, SSL/Certificates, and DNS edit permissions.
The README has the full setup guide. The source is on GitHub.
Top comments (0)