Automating Cloudflare DNS Updates from a Kubernetes CronJob


estimated read time: 3 minutes

The Problem

My homelab is exposed to the internet through Cloudflare, with DNS A records pointing to my home IP. For a long time this was stable — the IP barely changed. Then my ISP moved to a new infrastructure setup and the IP started rotating roughly once a week.

At first I updated it manually when things broke. Then I wrote a script. Then I thought: I have a Kubernetes cluster running 24/7 — this is exactly the kind of job it should be doing.

The result is a CronJob that runs every 15 minutes, checks the current public IP, and updates the Cloudflare DNS record if it has changed. It uses a small Python CLI tool and reads credentials from Kubernetes Secrets — no hardcoded tokens anywhere.

What You Need

The Tool

The CLI is dfoulkes/k3s_cloudflare_dynamic_ip_updater — a Python + Click application published to GitHub Container Registry. The container image is ghcr.io/dfoulkes/k3s_cloudflare_dynamic_ip_updater:latest.

The single command it exposes is cfcli update, which:

  1. Detects the current public IP
  2. Looks up the current IP in the Cloudflare DNS record
  3. Updates the record if the IP has changed, skips it if not

Create the Secrets

Store the Cloudflare API token and domain name as Kubernetes Secrets. Never put these in the manifest directly.

kubectl create secret generic cf-secret \
  --from-literal=CF_TOKEN=your-cloudflare-api-token

kubectl create secret generic website-domain-secret \
  --from-literal=CURRENT_DOMAIN=yourdomain.com

The CronJob

apiVersion: batch/v1
kind: CronJob
metadata:
  name: cloudflare-ip-updater
  namespace: default
spec:
  schedule: "*/15 * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
            - name: cloudflare-ip-updater
              image: ghcr.io/dfoulkes/k3s_cloudflare_dynamic_ip_updater:latest
              command: ["cfcli", "update"]
              env:
                - name: CURRENT_DOMAIN
                  valueFrom:
                    secretKeyRef:
                      name: website-domain-secret
                      key: CURRENT_DOMAIN
                - name: CF_TOKEN
                  valueFrom:
                    secretKeyRef:
                      name: cf-secret
                      key: CF_TOKEN
          restartPolicy: OnFailure

Deploy it:

kubectl apply -f cloudflare-ip-updater.yaml

Verify It’s Working

Check that the CronJob was created:

kubectl get cronjob cloudflare-ip-updater

After 15 minutes, check that a Job ran successfully:

kubectl get jobs

View the logs from the most recent run:

kubectl logs -l job-name=$(kubectl get jobs --sort-by=.metadata.creationTimestamp -o jsonpath='{.items[-1].metadata.name}')

If your IP has changed since the last run, you should see the update confirmed in the logs.

How It Fits Into the Broader Setup

This CronJob is a small but important piece of the homelab network stack. The flow looks like this:

Internet
  └─▶ Cloudflare (DNS + proxy)
        └─▶ Home IP (kept current by this CronJob)
              └─▶ Traefik ingress (K3s)
                    └─▶ Internal services

Without this, a weekly IP rotation would take down all externally accessible services until I noticed and updated the record manually. Now it self-heals within 15 minutes.

The 15-minute polling interval is a reasonable balance — frequent enough to minimise downtime after a rotation, infrequent enough that it doesn’t hammer the Cloudflare API. If you want faster recovery you could tighten it to */5 * * * *, though IP rotations are unlikely to happen at a time-critical moment.