A graphical console for favonia/cloudflare-ddns.
cloudflare-ddns is an excellent dynamic-DNS updater configured entirely through environment
variables. cfdyn puts a web interface in front of it: pick hostnames from your actual Cloudflare
zones, set the schedule, and let it install and run the updater for you — no YAML editing, no
reading a variable reference, no docker run incantations.
- Five-step setup wizard — password, token, hostnames, schedule, done. The token is verified against Cloudflare before anything saves, and hostnames are picked from your real zones instead of typed from memory.
- Live overview — the address your network currently presents, whether the updater is running, and a minute-by-minute pulse rail showing whether your DNS records actually agree with reality.
- One-click upstream updates — the updater is favonia's published image, run unmodified. Their bug fixes reach you the moment you press Install update (or automatically, if you opt in).
- Quick start
- The complete docker-compose.yml
- First-run setup
- Creating the Cloudflare API token
- Using the console
- Configuration reference
- How updates work
- Security notes
- Troubleshooting
- FAQ
- Development
- Relationship to the upstream project
Requirements: a Linux host with Docker and the compose plugin (docker compose version to check),
and a Cloudflare account with at least one zone.
mkdir cfdyn && cd cfdyn
curl -fsSLO https://raw.githubusercontent.com/hyprlab/cfdyn/main/docker-compose.yml
docker compose up -dThen open http://your-server-ip:8787 and follow the wizard. That's the whole install — cfdyn pulls and configures the DDNS updater itself during setup.
Prefer to paste instead of download? The complete compose file is below.
# cfdyn — a graphical console for favonia/cloudflare-ddns
# https://github.com/hyprlab/cfdyn
#
# Only ONE service is declared here. The cloudflare-ddns container itself is
# created and managed by cfdyn at runtime, straight from the upstream published
# image (favonia/cloudflare-ddns:1). That keeps upstream fully independent:
# their fixes arrive by pulling their tag, not by anything vendored here.
#
# docker compose up -d → open http://localhost:8787
services:
cfdyn:
image: hyprlab/cfdyn:${CFDYN_IMAGE_TAG:-1}
container_name: cfdyn
restart: unless-stopped
ports:
- "${CFDYN_PORT:-8787}:8787"
environment:
# Image the console manages. Tag "1" tracks upstream's stable v1.x line.
CFDYN_DDNS_IMAGE: "${CFDYN_DDNS_IMAGE:-favonia/cloudflare-ddns}"
CFDYN_DDNS_TAG: "${CFDYN_DDNS_TAG:-1}"
CFDYN_DDNS_CONTAINER_NAME: "${CFDYN_DDNS_CONTAINER_NAME:-cloudflare-ddns}"
# UID:GID the managed ddns container drops to. Must be able to read /secrets.
CFDYN_DDNS_USER: "${CFDYN_DDNS_USER:-1000:1000}"
TZ: "${TZ:-UTC}"
volumes:
# Root-equivalent access — see the security notes in README.md.
- /var/run/docker.sock:/var/run/docker.sock
- cfdyn-data:/data
# Shared with the managed ddns container so the API token can be passed as
# a file (CLOUDFLARE_API_TOKEN_FILE) instead of an inspectable env var.
- cfdyn-secrets:/secrets
security_opt:
- no-new-privileges:true
volumes:
cfdyn-data:
cfdyn-secrets:Every ${VAR:-default} can be overridden with a .env file next to the compose file — see the
configuration reference. Most installs need no .env at all.
The wizard walks five steps. Nothing touches Cloudflare until the final one.
- Password. The console can rewrite your DNS, so it locks itself first. Ten characters minimum, hashed with scrypt, five-minute lockout after eight failed tries.
- Token. Paste a Cloudflare API token (how to create one). cfdyn verifies it against Cloudflare immediately — a mispasted token fails here, not silently later — and tells you how many zones it can edit.
- Hostnames. Pick from the A/AAAA records that already exist in your zones, or type new hostnames — they don't need to exist yet; the updater creates the records. Tick Proxy on any hostname whose traffic should route through Cloudflare rather than exposing your address.
- Schedule. How often to check (default: every 5 minutes), the record TTL, and IPv6 detection. If your network has no IPv6, set IPv6 detection to Off — it saves the updater logging a failure every cycle that it can't do anything about.
- Review. See the hostnames and the exact container configuration that will be created, then
Install and start. cfdyn pulls
favonia/cloudflare-ddns:1, starts it with your settings, and takes you to the overview.
- Open dash.cloudflare.com/profile/api-tokens.
- Create Token → Create Custom Token.
- Under Permissions, add: Zone → DNS → Edit.
- Under Zone Resources, select the specific zones you want cfdyn to manage — not All zones, unless you really mean it.
- Only if you plan to maintain WAF lists: add Account → Account Filter Lists → Edit.
- Create the token and paste it into the wizard. cfdyn stores it encrypted; you won't need it again.
A token can be replaced at any time under Token & access, and the new one is verified before it takes effect.
Overview. The current public address (IPv4 and IPv6), the updater's state, and the sync history rail — one bar per minute, green when your DNS records match the detected address, amber when a record has fallen behind, red when something is wrong. These are cfdyn's own measurements against the Cloudflare API, not text scraped from logs.
Hostnames. The managed list, with each hostname's live DNS record shown beside the detected address so a stale record is visible at a glance. In Cloudflare, not managed here lists records that exist in your zones but aren't being updated — a record you added in the Cloudflare dashboard appears there, ready to take on with one click, defaulting to the record types it already has. Check for new records re-reads every authorized zone, bypassing caches.
Settings. Every option the upstream updater understands, grouped and explained in plain
language, with rarely-needed ones behind a Show advanced options toggle. Anything left at its
default is omitted from the container, so the Generated configuration preview shows only what you
actually changed — and doubles as a standalone docker-compose.yml for the updater if you ever
want to run it without cfdyn.
Container & logs. Start, stop, restart, or recreate the updater; check for and install upstream image updates; tail the updater's log live.
Token & access. Replace the Cloudflare token, change the console password, control automatic update checking, and switch the updater between host and bridge networking.
Saving vs. applying. Saving stores a change; Apply recreates the updater container with it. cfdyn compares the running container against your configuration and shows an Unapplied changes banner whenever they differ, so nothing silently sits half-done.
All of these go in a .env file next to docker-compose.yml. Everything is optional.
| Variable | Default | Purpose |
|---|---|---|
CFDYN_PORT |
8787 |
Port the console listens on |
CFDYN_IMAGE_TAG |
1 |
cfdyn image tag to run (1 tracks the stable 1.x line) |
CFDYN_DDNS_IMAGE |
favonia/cloudflare-ddns |
Upstream updater image |
CFDYN_DDNS_TAG |
1 |
Updater tag to track — 1 is upstream's stable v1.x line |
CFDYN_DDNS_CONTAINER_NAME |
cloudflare-ddns |
Name of the managed updater container |
CFDYN_DDNS_USER |
1000:1000 |
UID:GID the updater drops to |
TZ |
UTC |
Timezone for the console and cron schedules |
Advanced, set as environment variables on the cfdyn service itself:
| Variable | Default | Purpose |
|---|---|---|
CFDYN_SESSION_SECRET |
generated | Session signing key; generated and persisted if unset |
CFDYN_SECRET_KEY |
generated | Fernet key for the token at rest; generated at /data/secret.key (0600) if unset |
CFDYN_HTTPS |
false |
Set true behind an HTTPS reverse proxy, to mark the session cookie Secure |
Everything about DNS behaviour — schedule, TTL, proxying, IP detection, notifications (Healthchecks.io, Uptime Kuma, shoutrrr), WAF lists — is configured in the web UI under Settings, not in this file.
Two separate things update independently, by design:
The updater (favonia/cloudflare-ddns). cfdyn never forks, vendors, patches or rebuilds it. It
pulls the published favonia/cloudflare-ddns:1 image and runs it as a sibling container with the
hardening upstream's README recommends (read_only, cap_drop: all, no-new-privileges,
non-root). Check for updates pulls the tag — always safe, the running container is untouched —
and Install update recreates the container on the new image. Under Token & access you can
enable a daily automatic check, and optionally automatic installs. An upstream release needs
nothing from this project to reach you.
cfdyn itself. Standard image update:
docker compose pull && docker compose up -dYour password, token, hostnames and settings live in the cfdyn-data volume and survive updates
and recreations. The running DDNS updater isn't touched by a cfdyn update.
Read these before exposing the console beyond your own machine.
- The Docker socket is root-equivalent. Anything that can talk to
/var/run/docker.sockcan control the host. Managing a sibling container requires it — the same trust you extend to Watchtower or Portainer. If that's not acceptable, run the updater yourself using the snippet under Settings → Generated configuration and skip cfdyn entirely. - The API token is handled carefully. Encrypted at rest (Fernet, key at
/data/secret.key, mode 0600), and delivered to the updater as a file in a private volume viaCLOUDFLARE_API_TOKEN_FILE— upstream's own supported mechanism — so it never appears indocker inspecton the updater container. Scope the token to only the zones you manage. - The console is password-locked from first run, with scrypt hashing and a login lockout.
- Keep it off the public internet. There is no TLS and no multi-user model. LAN or VPN only;
if you front it with an HTTPS reverse proxy, set
CFDYN_HTTPS=true.
"Docker is out of reach" on the overview.
The socket isn't mounted or isn't readable. Confirm the compose file includes
/var/run/docker.sock:/var/run/docker.sock and that the Docker daemon is running.
A record I added in Cloudflare doesn't show up. Hostnames → Check for new records. Zone listings are cached for five minutes, so it would also appear on its own shortly. New records land in In Cloudflare, not managed here until you take them on.
The updater logs IPv6 failures every cycle. Your network has no working IPv6. Settings → IPv6 detection → Off, then Apply.
Another machine on the LAN can't reach the console.
Check the host firewall (sudo ufw status). Docker-published ports usually bypass ufw via its own
iptables chains, but restrictive setups differ.
I forgot the console password.
Delete the password_hash row from the settings table in /data/cfdyn.db (inside the cfdyn-data
volume) and restart; setup starts over. Token and hostnames are untouched:
docker exec cfdyn python -c "
import sqlite3; c = sqlite3.connect('/data/cfdyn.db')
c.execute(\"DELETE FROM settings WHERE key IN ('password_hash','setup_complete')\"); c.commit()"
docker restart cfdynRecords went stale while the console was stopped. The console and the updater are separate containers. The updater keeps running — and keeps updating DNS — even when cfdyn is down. cfdyn only needs to be up when you want to look at or change something.
Does cfdyn write my DNS records? No. The upstream updater does all record writing. cfdyn's own Cloudflare access is read-only — verifying tokens, listing zones and records, comparing them against the detected address.
Can some hostnames be proxied and others not?
Yes. Upstream takes a single PROXIED value but accepts an expression language; cfdyn compiles
per-hostname toggles into is(a) || is(b) form when hostnames disagree.
Why don't proxy toggles change existing records? Upstream never alters the proxy status of a record that already exists, and cfdyn feeds that value straight through. Change an existing record's proxy status in the Cloudflare dashboard.
What happens to DNS records when I remove a hostname from cfdyn? Nothing. The record is left exactly as it is; cfdyn just stops updating it.
Can I run the updater with DELETE_ON_STOP or @once?
Yes — both are in Settings. cfdyn understands @once (a clean exit is reported as success, not a
crash).
Where is my data?
Everything is in the cfdyn-data Docker volume: an SQLite database plus the encryption key.
Back up that volume and you've backed up cfdyn.
git clone https://github.com/hyprlab/cfdyn && cd cfdyn
# containerised
docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build
# or bare
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
CFDYN_DATA_DIR=./data CFDYN_SECRETS_DIR=./secrets .venv/bin/python -m app.wsgiWithout a reachable Docker socket the console degrades to read-only and says so, which makes UI work pleasant outside a container.
| File | Role |
|---|---|
app/schema.py |
Every upstream option, typed and documented. The one file to edit when upstream changes. |
app/ddns.py |
Docker lifecycle for the updater: pull, create, update, logs |
app/cloudflare.py |
Read-only Cloudflare client and address detection |
app/store.py |
SQLite persistence and token encryption |
app/monitor.py |
Background sampler feeding the pulse rail |
app/views.py |
Routes |
app/__init__.py |
App factory — and the single source of the version number (__version__), which flows to the UI footer and /healthz |
app/static/app.css |
The whole design system; no CDN, no build step |
The version lives in exactly one place: __version__ in app/__init__.py. The sidebar footer and
/healthz read it at runtime, and the release build stamps it into the image's OCI labels — bump
that one string and everything follows.
cfdyn is an independent companion to favonia/cloudflare-ddns and is not affiliated with or endorsed by it. The upstream image is used exactly as published, under its own licence. All credit for the actual DNS updating — which is the hard part — belongs upstream.
Verified against upstream v1.16.2.
cfdyn is built by a human maintainer working with generative AI as a development tool:
- Code — the large majority of the Python code in this repository was written with Anthropic's Claude (via Claude Code), working from the maintainer's direction. The maintainer decides what gets built, reviews the results, tests every release, and signs off on everything that ships.
- Text — documentation, release notes, and in-app copy are largely AI-drafted and human-edited.
- The app itself contains no AI. cfdyn has no AI features and makes no requests to AI services — it only drives the upstream cloudflare-ddns updater, and your Cloudflare tokens and DNS records go nowhere else. AI was used to build the app, not to run it.
Bug reports and pull requests are welcome from humans and their AI tools alike; everything merged gets the same human review.
cfdyn is free software, released under the GNU Affero General Public License v3.0. If you run a modified version as a network service, the AGPL requires you to offer its source to the users who interact with it — the sidebar footer's source link is where cfdyn itself honours that.
The upstream updater, favonia/cloudflare-ddns, is a separate work under its own licence (Apache 2.0) and is used as published, not linked or modified — running it beside cfdyn does not bring it under this licence.