DEV Community

the bomber
the bomber

Posted on

Connecting a Netlify Function to a Home Server via a Dynamic Cloudflare Tunnel

Introduction

I run a small automation server at home that powers a few cron jobs and a private API for my own website. The site lives on Netlify, which gives me serverless functions for things like contact‑form handling. The challenge was: how do I call my home server from a Netlify function when the tunnel address changes every time I restart it?

In this post I walk through the exact steps I took, the code I wrote, and the pitfalls I hit along the way. Everything here is what I actually use for Dream Futures Tech – no fictional benchmarks or client data.


Why Netlify + Cloudflare Tunnel?

Netlify handles static assets and serverless functions with zero‑ops deployment. My home server runs on a modest Raspberry Pi, so exposing it directly to the internet would require port forwarding and a static IP – both of which I prefer to avoid for security and reliability reasons.

Cloudflare Tunnel (formerly Argo Tunnel) creates a secure outbound connection from my Pi to Cloudflare’s edge network. Cloudflare then provides a public hostname (e.g., myhome.example.com) that forwards traffic back through the tunnel. The tunnel can be started with cloudflared, a lightweight binary.

The snag: each time I restart the tunnel, Cloudflare may assign a new sub‑domain unless I reserve a custom hostname. I wanted a solution that works with the free tier, so I let Cloudflare generate the hostname and then discover it programmatically from my Netlify function.


Setting Up the Cloudflare Tunnel

On the Pi I installed cloudflared and created a configuration file that runs the tunnel as a background service.

# /home/pi/.cloudflared/config.yml
url: http://localhost:3000   # my internal API
# No hostname – let Cloudflare assign one
logfile: /var/log/cloudflared.log
loglevel: info
Enter fullscreen mode Exit fullscreen mode

I start it with:

cloudflared tunnel run myhome-tunnel &
Enter fullscreen mode Exit fullscreen mode

The process writes a line like:

2023-09-01T12:34:56Z INF Starting tunnel myhome-tunnel
2023-09-01T12:34:56Z INF Tunnel URL: https://random-12345.trycloudflare.com
Enter fullscreen mode Exit fullscreen mode

That URL is the address I need to call from Netlify.


Discovering the Dynamic Tunnel URL

Instead of parsing the log file (which can be flaky), I use Cloudflare’s API to list active tunnels and pull the public URL.

API Token

I created a token with the Zone:Read and Tunnel:Read permissions scoped to my account. The token lives in Netlify’s environment variables as CLOUDFLARE_TOKEN.

Helper Script (optional)

Locally I wrote a tiny Node script to verify the API call:

// scripts/get-tunnel-url.js
const fetch = require('node-fetch');
const token = process.env.CLOUDFLARE_TOKEN;

(async () => {
  const res = await fetch('https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/cfd_tunnel', {
    headers: { Authorization: `Bearer ${token}` },
  });
  const data = await res.json();
  const tunnel = data.result.find(t => t.name === 'myhome-tunnel');
  console.log(tunnel?.tunnel_url);
})();
Enter fullscreen mode Exit fullscreen mode

Running this prints the current public URL, e.g., https://random-12345.trycloudflare.com.


Netlify Function that Calls the Home Server

The function lives in netlify/functions/ping-home.js. It does three things:

  1. Fetch the tunnel URL from Cloudflare’s API.
  2. Append the path I want to hit on the home server (/status).
  3. Proxy the response back to the client.
// netlify/functions/ping-home.js
const fetch = require('node-fetch');

exports.handler = async function(event, context) {
  const token = process.env.CLOUDFLARE_TOKEN;
  const accountId = process.env.CLOUDFLARE_ACCOUNT_ID;

  // 1⃣ Get the tunnel URL
  const tunnelRes = await fetch(
    `https://api.cloudflare.com/client/v4/accounts/${accountId}/cfd_tunnel`,
    { headers: { Authorization: `Bearer ${token}` } }
  );
  const tunnelData = await tunnelRes.json();
  const tunnel = tunnelData.result.find(t => t.name === 'myhome-tunnel');

  if (!tunnel) {
    return { statusCode: 502, body: 'Tunnel not found' };
  }

  const homeUrl = `${tunnel.tunnel_url}/status`;

  // 2⃣ Call the home server
  const homeRes = await fetch(homeUrl);
  const body = await homeRes.text();

  // 3⃣ Return the result
  return {
    statusCode: homeRes.status,
    headers: { 'Content-Type': homeRes.headers.get('content-type') || 'text/plain' },
    body,
  };
};
Enter fullscreen mode Exit fullscreen mode

Important notes:

  • The function runs on Netlify’s edge, so the outbound request must be over HTTPS. Cloudflare’s tunnel URL is always HTTPS, which satisfies that.
  • I keep the token and account ID in Netlify’s UI under Site Settings → Build & Deploy → Environment. Never commit them to source control.

Handling Tunnel Restarts

Because the tunnel URL can change, the function always queries Cloudflare at runtime. That adds a few milliseconds of latency, but the overhead is negligible for my use case (a simple health‑check endpoint). If the latency ever becomes a concern, I could cache the URL in Netlify’s Edge Functions KV store for a short period (e.g., 30 seconds).


Testing the End‑to‑End Flow

  1. Deploy the Netlify site (netlify deploy --prod).
  2. Hit https://my-site.netlify.app/.netlify/functions/ping-home in a browser or with curl.
  3. You should see the JSON payload returned by the home server’s /status route.

If the tunnel is down, the function returns a 502 with the message “Tunnel not found”. This makes it easy to set up alerts in Netlify’s monitoring UI.


Lessons Learned

  • Never rely on a static tunnel URL when using the free Cloudflare tier. The API is the reliable source of truth.
  • Environment variables are your friend – keep secrets out of the repo and rotate them periodically.
  • Graceful error handling matters. A missing tunnel should surface as a clear HTTP status rather than a generic 500.
  • Keep the home server lightweight. My Pi only runs a Node/Express app on port 3000, which is more than enough for the occasional webhook or status check.

Conclusion

By combining Netlify’s serverless functions with Cloudflare Tunnel’s dynamic public endpoint, I now have a secure, low‑maintenance bridge from my static site to a private home automation server. The pattern works for any scenario where you need to call a self‑hosted service from a cloud function without exposing a permanent public IP.

I build sites and automations for small businesses at dreamfuturestech.com.

Top comments (0)