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
- A running Kubernetes cluster (K3s works fine)
- A Cloudflare account with a domain
- A Cloudflare API token with DNS edit permissions for the zone
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:
- Detects the current public IP
- Looks up the current IP in the Cloudflare DNS record
- 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.
Related Posts
- Using Cloudflare with K3s and Cert-Manager — how Cloudflare proxying and cert-manager work together
- Traefik v3 on K3s — the ingress layer behind Cloudflare