Cloudflare On-Demand Tunnel with Custom Domain
Revised article
This is a revised version of an article originally published on [larsolino.dev](https://larsolino. ev/articles/cloudflare-tunnel) on April 17, 2025.
Cloudflare Tunnel lets you expose a local service to the internet without opening inbound ports on your router or publishing your origin IP address. The cloudflared process creates outbound connections to Cloudflare, which forwards incoming requests to your local service.
This guide sets up a locally managed tunnel with a custom domain and a local config.yml. You start the tunnel manually when needed, rather than installing it as a permanent service.
The examples use macOS with Homebrew and a domain managed through Cloudflare.
Note
The tunnel’s routing configuration and credentials are stored locally. DNS records, tunnel registration, and any Cloudflare Access policies are managed through Cloudflare.
Install cloudflared
brew install cloudflared
Authenticate with Cloudflare
Run:
cloudflared tunnel login
This opens a browser where you sign in to Cloudflare and select the domain.
The command creates an account certificate:
~/.cloudflared/cert.pem
It does not create your config.yml.
The certificate authorizes management operations such as creating tunnels and configuring DNS routes. Running an existing tunnel requires its tunnel credentials, described below.
Important
Keep
cert.pemand the tunnel credentials out of Git repositories. The account certificate grants broader management permissions than the credentials for an individual tunnel.
Create the tunnel
cloudflared tunnel create my-tunnel
This registers the tunnel and creates a credentials file:
~/.cloudflared/<TUNNEL-ID>.json
Record the tunnel UUID and credentials path printed by the command.
The JSON file contains credentials for running this tunnel. Routing settings belong in the separate YAML configuration.
Creating the tunnel does not start it.
To list your tunnels:
cloudflared tunnel list
Create config.yml
Create:
~/.cloudflared/config.yml
Add the following configuration, replacing <TUNNEL-ID> and <mac-user>:
tunnel: <TUNNEL-ID>
credentials-file: /Users/<mac-user>/.cloudflared/<TUNNEL-ID>.json
ingress:
- hostname: my-tunnel.example.com
service: http://localhost:8080
- service: http_status:404
The fields mean:
tunnel: The UUID of your tunnel.credentials-file: The absolute path to its JSON credentials file.ingress: Rules mapping incoming hostnames to local services.service: The destination thatcloudflaredconnects to.
The final rule returns HTTP 404 for requests that do not match an earlier rule. Keep this catch-all rule last.
In this example, your application must be running at:
http://localhost:8080
Validate the ingress configuration:
cloudflared tunnel --config ~/.cloudflared/config.yml ingress validate
Configure DNS
Create the DNS route:
cloudflared tunnel route dns my-tunnel my-tunnel.example.com
This creates a CNAME record pointing to your tunnel’s Cloudflare hostname:
<TUNNEL-ID>.cfargotunnel.com
Important
The hostname in the DNS command must match
ingress.hostnameinconfig.yml. Here, both usemy-tunnel.example.com.
If a conflicting DNS record already exists, inspect it in the Cloudflare dashboard and update or remove it as appropriate.
This DNS step is normally needed only once. Starting and stopping the tunnel does not require recreating the record.
Run the tunnel
First, start your local application on port 8080.
Then start the tunnel:
cloudflared tunnel --config ~/.cloudflared/config.yml run my-tunnel
Explicitly specifying the configuration file and tunnel makes it clear which settings are being used.
While the tunnel is running, open:
https://my-tunnel.example.com
Press Ctrl+C in the terminal to stop the foreground process.
Note
Publishing an HTTP application through a tunnel makes it reachable through its public hostname. To restrict access, configure a Cloudflare Access application and an allow policy for the intended users.
Run in the background
Start the tunnel with a log file and save its process ID:
nohup cloudflared tunnel \
--config ~/.cloudflared/config.yml \
run my-tunnel \
> ~/.cloudflared/tunnel.log 2>&1 &
echo $! > ~/.cloudflared/tunnel.pid
Read the log:
tail -f ~/.cloudflared/tunnel.log
Stop the saved process:
kill "$(cat ~/.cloudflared/tunnel.pid)"
rm ~/.cloudflared/tunnel.pid
Use the PID while it still identifies that running tunnel process. A stale PID file can refer to a different process later.
Avoid using pkill cloudflared for a specific tunnel: it terminates all matching cloudflared processes.
Background execution does not keep the Mac awake or restart the tunnel after a reboot.
Optional ZSH helper script
Save the following as tunnel.sh:
#!/bin/zsh
CONFIG="${HOME}/.cloudflared/config.yml"
LOGFILE="${HOME}/.cloudflared/tunnel.log"
TUNNEL="my-tunnel"
if [[ "$1" == "-b" ]]; then
echo "Starting tunnel in the background."
echo "Log: ${LOGFILE}"
nohup cloudflared tunnel \
--config "$CONFIG" \
run "$TUNNEL" \
> "$LOGFILE" 2>&1 &
echo "PID: $!"
echo "Stop this process with: kill $!"
else
echo "Starting tunnel in the foreground."
exec cloudflared tunnel \
--config "$CONFIG" \
run "$TUNNEL"
fi
Make it executable:
chmod +x tunnel.sh
Run in the foreground:
./tunnel.sh
Or in the background:
./tunnel.sh -b
For background execution, record the printed PID so you can stop that specific process.
Check tunnel status
You can inspect your tunnel in either dashboard:
- Cloudflare dashboard → Networking → Tunnels
- Cloudflare Zero Trust → Networks → Connectors
From the terminal:
cloudflared tunnel info my-tunnel
Caching during development
HTTP traffic through Cloudflare Tunnel can be subject to Cloudflare’s CDN caching rules.
Cloudflare does not cache HTML or JSON by default. Certain static resources, including CSS, JavaScript, and images, are eligible for caching. Actual behavior depends on file extensions, response headers, and configured cache rules.
If changes are not appearing:
- Check whether the browser is serving a cached copy.
- Check the application’s
Cache-Controlheaders. - Review Cloudflare Cache Rules for the development hostname.
- Purge the affected URLs if necessary.
For a development hostname, a Cache Rule that bypasses caching can be more convenient than repeatedly purging the cache.
Using a tunnel for SSH
The configuration above publishes a web application. For SSH access, use a separate hostname and an SSH service rule:
ingress:
- hostname: ssh.example.com
service: ssh://localhost:22
- service: http_status:404
Create the corresponding DNS route:
cloudflared tunnel route dns my-tunnel ssh.example.com
The Mac must have Remote Login enabled so its SSH server is available.
For the client-side cloudflared method:
- Install
cloudflaredon the client computer as well. - Configure Cloudflare Access to restrict access to the intended users.
- Add a
ProxyCommandto the client’s SSH configuration.
For example, on Windows, assuming cloudflared.exe is available on the PATH used by SSH:
Host macbook-cloudflare
HostName ssh.example.com
User <mac-user>
ProxyCommand cloudflared.exe access ssh --hostname %h
If the executable is not found, use its actual absolute path in the ProxyCommand, enclosed in double quotes if it contains spaces.
Test the connection:
ssh macbook-cloudflare
Cloudflare Access handles authentication at the Cloudflare layer. Your Mac’s SSH server still requires its own authentication, such as an SSH key or your Mac account password.
Once the connection works, select macbook-cloudflare through Remote-SSH: Connect to Host… in VS Code and open the project folder on the Mac.

