A thin, opinionated wrapper around cloudflared to quickly manage ingress rules for a single Cloudflare Tunnel and map subdomains to local services.
Assuming you’ve initialized the CLI with example.com:
Map api.example.com to http://localhost:3000 (supports http and web socket traffic by default):
cl-tunnel add api 3000You can then access: https://api.example.com (after DNS propagation) which will be proxied to http://localhost:3000.
Note: Only single-level subdomains are supported (e.g., api.example.com). Nested subdomains like dev.api.example.com are not supported because Cloudflare only issues default SSL certs for one-level subdomains.
- Install
cloudflared(or see Cloudflare docs)
brew install cloudflared- Login
cloudflared tunnel login- Create a tunnel
cloudflared tunnel create <tunnel-name>Example:
cloudflared tunnel create local-dev-mac-tunnel- This will create the required credentials file and write your Cloudflare Tunnel config under
~/.cloudflared/.
npm install -g cl-tunnelThis package ships a single-file Bun-compiled executable at dist/cl-tunnel and exposes it via the npm bin field.
Run the CLI to install and patch the cloudflared launch agent so it runs the tunnel reliably on macOS login:
cl-tunnel install-serviceWhat this does:
- Runs
cloudflared service installwhich creates~/Library/LaunchAgents/com.cloudflare.cloudflared.plist. - Patches the plist to execute
cloudflared tunnel run(not justcloudflared), then restarts the launch agent.
Why the patch? There’s a long-standing macOS issue where the service may start without tunnel run. See the discussion here: cloudflared issue #327.
If you prefer Cloudflare’s official guidance on running as a service, refer to Cloudflare’s official guidance on running as a service.
Tell cl-tunnel which root domain you’ll be using for subdomains:
cl-tunnel init <your-root-domain>
# example:
cl-tunnel init example.comThis writes ~/.cl-tunnel/config.json with your domain. The CLI will combine this with your cloudflared config to manage ingress.
- Add a mapping: creates/updates an ingress rule and a DNS record for the subdomain.
cl-tunnel add <subdomain> <port>
# example: api.example.com -> http://localhost:3000
cl-tunnel add api 3000
# overwrite existing mapping if it exists
cl-tunnel add api 4000 --force- List mappings:
cl-tunnel list- Remove a mapping (removes the ingress rule; DNS removal is not yet automated):
cl-tunnel remove <subdomain>When you run cl-tunnel add <subdomain> <port>, the CLI performs the following steps:
- Reads CLI config
~/.cl-tunnel/config.jsonto get your root domain (e.g.,example.com). - Reads Cloudflare Tunnel config
~/.cloudflared/config.ymland validates its shape. - Appends or updates an ingress rule in
~/.cloudflared/config.yml:- Adds
hostname: <subdomain>.<domain>withservice: http://localhost:<port> - Sorts rules (hostnamed rules first, alphabetically) and de-duplicates by hostname
- Keeps your fallback rule (e.g.,
- service: http_status:404) last
- Adds
- Writes the updated YAML back to
~/.cloudflared/config.yml. - Validates the config using
cloudflared:
cloudflared tunnel ingress validate- If validation fails, the CLI restores the previous file contents (rollback) and exits.
- Creates a DNS route for the subdomain to your tunnel UUID from the config:
cloudflared tunnel route dns <tunnel-uuid> <subdomain>- If DNS routing fails, the CLI restores the previous config and exits.
- Restarts the macOS launch agent to apply changes:
launchctl kickstart -k gui/$(id - u)/com.cloudflare.cloudflared- If restart fails, the CLI restores the previous config, then restarts again to return to the prior state.
Notes:
- DNS record removal is not automated by
removeyet.
- Install and setup
cloudflared - Run as a service: Cloudflare’s official guidance on running as a service
- macOS service caveat: cloudflared issue #327