The problem I kept hitting
I run a small automation server at home that talks to a few Raspberry Pis, a MySQL instance, and a Node.js API. When I moved the front‑end to Netlify, my Netlify Functions needed to hit that internal API. A Cloudflare Tunnel (formerly Argo Tunnel) solved the NAT issue, but every time the tunnel restarted its public URL changed. My functions started failing with 404s, and I was manually updating a Netlify secret each time. I needed a stable address that survived tunnel restarts.
Step 1 – Give the tunnel a permanent hostname
Instead of letting Cloudflare generate a random sub‑domain, I created a DNS record in my Cloudflare zone (e.g., home.api.mybiz.com). Then I ran cloudflared with a named tunnel so the hostname stayed the same.
# Create a named tunnel (only once)
cloudflared tunnel create home‑api
# Route the stable DNS name to that tunnel
cloudflared tunnel route dns home‑api home.api.mybiz.com
The home‑api tunnel now owns home.api.mybiz.com. Even if the tunnel process restarts, Cloudflare keeps the DNS entry pointing at the active tunnel instance.
Step 2 – Run the tunnel as a system service
I don’t want to remember to start the tunnel manually, so I added a systemd unit. The config file tells cloudflared which local service to expose.
# /etc/cloudflared/config.yml
url: http://localhost:3000
hostname: home.api.mybiz.com
logfile: /var/log/cloudflared.log
loglevel: info
# Enable and start the service
sudo systemctl enable cloudflared@home‑api
sudo systemctl start cloudflared@home‑api
Now the tunnel is always up after a reboot, and the public address never changes.
Step 3 – Call the tunnel from a Netlify Function
Inside my Netlify Function I simply fetch the stable hostname. I store the URL in a Netlify environment variable (HOME_API_URL) so I can change it later without redeploying.
// netlify/functions/get‑status.js
exports.handler = async () => {
const base = process.env.HOME_API_URL || "https://home.api.mybiz.com";
const res = await fetch(`${base}/status`);
const data = await res.json();
return { statusCode: 200, body: JSON.stringify(data) };
};
Because the tunnel terminates TLS for me, I don’t need to manage certificates on the home server. The only thing I had to watch was the firewall on the Raspberry Pi – port 3000 must be reachable from the machine running cloudflared.
Step 4 – Keep the tunnel healthy
Cloudflare provides a metrics endpoint that I poll from another Netlify Function. If the tunnel goes down, I trigger a webhook that restarts the systemd service via SSH. The logic lives in a tiny script, but the key idea is to let the tunnel self‑heal instead of manually checking logs.
# Example health check (run on a schedule)
curl -s https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/tunnels | jq '.result[] | select(.name=="home‑api") | .status'
When the status is anything other than active, I SSH into the home box and run sudo systemctl restart cloudflared@home‑api.
What I learned
- Name your tunnel – a named tunnel gives you a permanent DNS entry, eliminating the “random URL” problem.
- Run it as a service – systemd guarantees the tunnel survives reboots and crashes.
- Use environment variables – keeping the URL out of code makes future moves (e.g., changing the sub‑domain) painless.
- Monitor health – a lightweight status check prevents silent outages.
I’ve now got a Netlify front‑end that reliably talks to my home automation stack, even though the tunnel process may restart at any time.
I build sites and automations for small businesses at dreamfuturestech.com.
Top comments (0)