Skip to content

Repository files navigation

cfdyn

A graphical console for favonia/cloudflare-ddns.

Docker Hub License: AGPL v3

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).

Table of contents


Quick start

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 -d

Then 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.

The complete docker-compose.yml

# 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.

First-run setup

The wizard walks five steps. Nothing touches Cloudflare until the final one.

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.

Creating the Cloudflare API token

  1. Open dash.cloudflare.com/profile/api-tokens.
  2. Create Token → Create Custom Token.
  3. Under Permissions, add: Zone → DNS → Edit.
  4. Under Zone Resources, select the specific zones you want cfdyn to manage — not All zones, unless you really mean it.
  5. Only if you plan to maintain WAF lists: add Account → Account Filter Lists → Edit.
  6. 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.

Using the console

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.

Configuration reference

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.

How updates work

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 -d

Your 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.

Security notes

Read these before exposing the console beyond your own machine.

  • The Docker socket is root-equivalent. Anything that can talk to /var/run/docker.sock can 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 via CLOUDFLARE_API_TOKEN_FILE — upstream's own supported mechanism — so it never appears in docker inspect on 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.

Troubleshooting

"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 detectionOff, 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 cfdyn

Records 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.

FAQ

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.

Development

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.wsgi

Without 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.

Relationship to the upstream project

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.

AI notice

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.

Licence

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.

About

A graphical console for favonia/cloudflare-ddns

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages