DEV Community

the bomber
the bomber

Posted on

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

Why I needed a tunnel

I run a small home automation server on my personal network. It hosts a few Node.js services that expose a JSON API for my smart lights, a webhook endpoint for a contact form, and a simple status page. I wanted to call those endpoints from a Netlify site without exposing my home IP directly. Cloudflare's quick tunnel (formerly Argo Tunnel) gives me a public HTTPS URL that forwards traffic to a local port, but the URL changes each time the tunnel restarts. The article walks through the exact steps I used to keep a Netlify function in sync with that changing address.

1. Install and configure cloudflared

On my home machine (Ubuntu 22.04) I installed the Cloudflare daemon:

# Download the latest binary
wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
sudo dpkg -i cloudflared-linux-amd64.deb
Enter fullscreen mode Exit fullscreen mode

Next I logged into Cloudflare and created a tunnel token that only allows me to start a tunnel for a specific hostname:

cloudflared tunnel login   # opens a browser, stores a cert in ~/.cloudflared
cloudflared tunnel create home-tunnel
# The command prints a tunnel ID, e.g. 12345678-90ab-cdef-1234-567890abcdef
Enter fullscreen mode Exit fullscreen mode

I then wrote a small config.yml that points the tunnel at the local service running on port 3000:

# ~/.cloudflared/config.yml
url: http://localhost:3000
tunnel: 12345678-90ab-cdef-1234-567890abcdef
credentials-file: /home/kaleb/.cloudflared/12345678-90ab-cdef-1234-567890abcdef.json
Enter fullscreen mode Exit fullscreen mode

Running the tunnel is as simple as:

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

When it starts, Cloudflare prints a public URL, for example https://my-home-tunnel-abcdef.cloudflareteam.com. That URL is what the Netlify function will call.

2. Capture the public URL automatically

Because the tunnel URL changes whenever the daemon restarts (e.g., after a reboot), I needed a reliable way to surface the current address to Netlify. Cloudflare provides a metrics endpoint that returns the tunnel information in JSON:

curl -s http://localhost:8787/api/tunnels | jq -r '.tunnels[0].public_url'
Enter fullscreen mode Exit fullscreen mode

cloudflared exposes this metrics endpoint on port 8787 by default. I wrapped the call in a tiny script that writes the URL to a file:

#!/usr/bin/env bash
# fetch-tunnel-url.sh
URL=$(curl -s http://localhost:8787/api/tunnels | jq -r '.tunnels[0].public_url')
if [ -n "$URL" ]; then
  echo "$URL" > /home/kaleb/.cloudflared/current-url.txt
fi
Enter fullscreen mode Exit fullscreen mode

I set up a systemd timer to run this script every minute, ensuring the file always contains the latest URL.

3. Push the URL to Netlify as an environment variable

Netlify lets you store environment variables per site. I created a variable called HOME_TUNNEL_URL and marked it as secret. To keep it up‑to‑date I used Netlify's Deploy Hook API. The script reads the URL from the file and sends a PATCH request:

#!/usr/bin/env bash
# update-netlify.sh
TOKEN="${NETLIFY_PERSONAL_TOKEN}"   # stored as a secret on the home machine
SITE_ID="${NETLIFY_SITE_ID}"        # also a secret
URL=$(cat /home/kaleb/.cloudflared/current-url.txt)
if [ -z "$URL" ]; then exit 1; fi
curl -X PATCH "https://api.netlify.com/api/v1/sites/$SITE_ID" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"build_settings\":{\"environment\":{\"HOME_TUNNEL_URL\":\"$URL\"}}}"
Enter fullscreen mode Exit fullscreen mode

I added another systemd timer that runs this script shortly after the fetch script. The two timers together make sure Netlify always sees the current tunnel address.

4. Netlify function that talks to the home server

With the URL available as an environment variable, the function code is straightforward. I used a Node.js function placed in netlify/functions/proxy.js:

// netlify/functions/proxy.js
const fetch = require('node-fetch');

exports.handler = async (event, context) => {
  const base = process.env.HOME_TUNNEL_URL; // injected by Netlify at build time
  if (!base) {
    return { statusCode: 500, body: 'Tunnel URL not configured' };
  }

  // Forward the request path and query string to the home server
  const target = `${base}${event.path}${event.rawQuery ? `?${event.rawQuery}` : ''}`;
  try {
    const resp = await fetch(target, {
      method: event.httpMethod,
      headers: event.headers,
      body: event.body,
    });
    const data = await resp.text();
    return {
      statusCode: resp.status,
      headers: Object.fromEntries(resp.headers.entries()),
      body: data,
    };
  } catch (err) {
    console.error('Proxy error:', err);
    return { statusCode: 502, body: 'Bad gateway' };
  }
};
Enter fullscreen mode Exit fullscreen mode

The function simply proxies whatever path it receives to the home server. Because Netlify injects HOME_TUNNEL_URL at build time, the function always uses the most recent address.

5. Keeping the build up‑to‑date

Netlify only re‑reads environment variables when a new build is triggered. To avoid manual redeploys, I added a build hook that Netlify calls after the environment variable is updated. The update-netlify.sh script ends with a second request that triggers a deploy:

# Trigger a new deploy so the function sees the new URL
curl -X POST "https://api.netlify.com/api/v1/sites/$SITE_ID/builds" \
  -H "Authorization: Bearer $TOKEN"
Enter fullscreen mode Exit fullscreen mode

The extra request costs a few seconds but guarantees the function runs with the correct URL.

6. What went wrong and how I fixed it

  • Tunnel URL not ready – The metrics endpoint sometimes returns an empty array right after a restart. I added a small retry loop (max 5 attempts, 2 s delay) before writing the file.
  • Rate limits on Netlify API – The API allows a few calls per minute. My timers run every minute, which stays safely under the limit.
  • SSL verification – Cloudflare issues a valid certificate for the tunnel hostname, so the function can call it over HTTPS without extra configuration.

7. Summary of the workflow

  1. Install cloudflared and create a tunnel that points at the local service.
  2. Run a script that reads the public URL from the metrics endpoint and stores it locally.
  3. Push that URL to Netlify as an environment variable using the Netlify API.
  4. Trigger a new Netlify build so the function picks up the updated variable.
  5. In the Netlify function, read process.env.HOME_TUNNEL_URL and proxy the request.

All of this runs on my home machine, requires no external hosting, and works even when the tunnel address changes.


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

Top comments (0)