CaddyUI v2.4.3 — dashboard polish, smarter health dots, multi-host fixes

Hey all,

Shipped CaddyUI v2.4.3 today — a GUI for managing Caddy (proxy hosts, certs, managed DNS across Cloudflare/Namecheap/GoDaddy/DigitalOcean/Hetzner, multi-server push over WG/Tailscale). Sharing a few of the real-world fixes from this cycle in case they’re useful to anyone running similar setups.

Features

  • Proxy Hosts — point domains at upstream services with one-click TLS via Caddy’s automatic HTTPS

  • Redirections — 301/302/307/308 redirect rules across hostnames

  • Advanced Routes — import raw Caddyfile blocks or write JSON directly for anything the UI can’t model

  • Certificates — manage custom PEM/path certificates; expiry alerts via email and/or webhook

  • Multi-server — manage multiple Caddy instances from a single UI; switch with a dropdown. Edge hosts only need Caddy — no CaddyUI container required (see Agent mode)

  • Multi-user — admin and user roles; each user sees and manages only their own proxies

  • Email notifications — SMTP support (STARTTLS / TLS / plain) for cert-expiry and upstream health alerts

  • Upstream health — live health check per proxy; polls Caddy’s own admin API so Docker-internal hostnames work correctly

  • Activity log — every create/edit/delete/sync action is logged with actor and timestamp

  • Snapshots — one-click SQLite database backup; auto-snapshot on sync

  • Import from Caddy — pull your existing live Caddy config into the DB on first run

  • Paste Caddyfile — convert a Caddyfile block into a managed advanced route

  • Dark mode — toggleable, remembers your choice; system preference respected on first visit

  • 2FA / TOTP — per-user time-based one-time passwords

  • PWA — installable on desktop and mobile; offline-capable service worker

  • Update notifications — sidebar badge when a newer Docker Hub release is available**
    Repo:** https://github.com/X4Applegate/caddyui
    Release: https://github.com/X4Applegate/caddyui/releases/tag/v2.4.3
    Docker: applegater/caddyui:v2.4.3 (multi-arch, scratch base, SBOM + provenance)

What’s in v2.4.3

1. Stop flagging Docker-name backends as “down”
If your proxy target is a Docker service name (status-server:3000, snipeit-app:80, etc.), CaddyUI used to show a red “unreachable” dot because the caddyui container itself usually isn’t on that backend’s Docker network — only Caddy is. It asked Caddy’s /reverse_proxy/upstreams first, but if the upstream wasn’t cached there it fell back to a direct probe that couldn’t resolve the name.

Now: hostnames with no dots → skip the direct probe, render an amber “unknown” with a tooltip explaining why. DNS-resolution errors on public names get the same treatment. Genuinely unreachable public hostnames still go red.

2. Server-online indicator stops flapping over WG
Health poller was marking servers “offline” after a single failed 5 s ping. Over WireGuard that flaps every time rekey drops a packet. Now: 3 consecutive misses + 8 s timeout. Every success resets the counter immediately. Dashboard stays stable.

3. Dashboard source gives you both actions
Click the domain pill → opens the site in a new tab. Click the pencil icon next to it → opens the edit form. Save redirects back to /proxy-hosts, so click-pencil-edit-save-back is a clean loop.

v2.4.x cumulative highlights (context for anyone new)

  • Per-server public IP for managed DNS — editing one server’s IP retargets only that server’s records (no more rewriting every provider record when one IP changes)

  • Startup sync (CADDYUI_SYNC_ON_START=1) rehydrates Caddy from the DB on every docker compose restart, with a safety guard that refuses to push when the DB is empty so it can’t wipe an existing Caddyfile

  • Firewall docs — the Docker-iptables-bypass gotcha bit me personally. Binding port 2019 to the WG interface ("10.8.0.2:2019:2019") is the clean fix; UFW rules alone don’t block Docker-published ports

One operational note that’s worth calling out

If you run Caddy in default mode (caddy run --config /etc/caddy/Caddyfile) and push config to it via the admin API from CaddyUI or anything else, pushes don’t survive Caddy container restarts — the static Caddyfile reloads and overwrites them. The fix is --resume:

services:

caddy:

command: caddy run --config /config/caddy/autosave.json --resume --adapter json

That way Caddy persists its current config to autosave.json and reloads from there on restart. Not a CaddyUI bug but a common pothole for multi-host setups using the admin API as a source of truth.

Feedback welcome

Happy to hear what’s missing for your use case. Anything around multi-host orchestration, DNS providers, or observability in particular — that’s where I’ve been focusing.

— Richard

1 Like

CaddyUI v2.5.0 — switchable CAPTCHA, timezone picker, branded error pages

Hey everyone :waving_hand:

Small update from the last time I posted — two releases shipped today that I thought the folks here might care about, since they’re all Caddy-adjacent features and a few of you asked for them directly.

For context: CaddyUI is a self-hosted web UI that drives Caddy through its admin API. It stores everything in SQLite, pushes JSON config to Caddy on save, and covers proxy hosts, redirects, certs, advanced/raw routes, multi-server, multi-user, TOTP, snapshots, and paste-a-Caddyfile import. If you’ve seen Nginx Proxy Manager, the surface area is similar, but it speaks Caddy natively instead of translating to nginx.


What’s new in v2.5.0

Switchable CAPTCHA protection

You can now pick one of three modes from Settings → CAPTCHA protection:

  • Off (default on fresh installs)
  • Cloudflare Turnstile (managed, free, privacy-friendly)
  • Google reCAPTCHA v3 (score-based, invisible)

…and it applies to three forms: /login, /login/totp, and /users/new (admin creating a new account).

Why two providers? A while back someone in the community had a Cloudflare outage that briefly made Turnstile unreachable, and they got stuck on their own login page. v2.5.0 ships a reCAPTCHA v3 fallback plus an env-var kill-switch:

environment:
  CADDYUI_CAPTCHA_DISABLE: "1"

Set that, restart the container, and the widget stops rendering and the server stops verifying. Pull it back out once you’ve logged in. Intended specifically for “I’m locked out of my own admin” recovery.

Existing Turnstile keys from v2.4.x upgrade in place — the old settings keys are preserved, so if you had Turnstile configured, flip the provider to Turnstile and everything Just Works. Inactive-provider keys also stay in the DB across switches, so you can toggle between Turnstile and reCAPTCHA without re-typing credentials.

Small implementation details that might matter to you:

  • TOTP captcha failure does not consume the pending-TOTP token (5-min auto-expire still caps abuse — wrong captcha ≠ burned 2FA slot).
  • reCAPTCHA v3 uses a submit-hook: first submit fetches a token via grecaptcha.execute, populates a hidden input, then re-submits. If api.js fails to load (ad-blocker, outage), the fallback path just submits anyway so the server returns a clean “Security check failed” error instead of a stuck form.
  • Verify-endpoint HTTP client has a 10s timeout — a slow siteverify can’t wedge /login for 30+ seconds.

Also bundled in v2.4.12 (shipped earlier today)

Timezone picker

Settings → Timezone now has an IANA zone dropdown (America/New_York, Europe/London, etc.) with an “Other…” free-text fallback. Every DB-stored timestamp in the UI flows through it: cert expiry, activity log, snapshots, “last contact”, “last sync”. Resolution priority is:

  1. DB value (what you picked)
  2. TZ environment variable (Go’s time.Local reads this at startup)
  3. UTC

There’s also a new TZ: ${TZ:-UTC} env entry on both services in docker-compose.yml — pair it with the same zone on your Caddy container so the access-log timestamps line up.

Branded error pages

This one’s Caddy-flavored and I’m curious what you think. CaddyUI now injects a set of routes into apps.http.servers.srv0.errors.routes so every 404/502/503/504 from Caddy itself (not from an upstream that returns its own error body) renders a dark-mode-aware HTML page with:

  • The status code + short human-readable explanation
  • {http.error.id} — Caddy’s 9-char correlation ID (same one that ends up in the access log, which is the whole point: when a user screenshots a 502, you can grep the log)
  • Current HTTP-Date timestamp

Had to bang my head on one thing worth mentioning: the {err.status_code} / {err.id} placeholders you see in the Caddyfile docs only work through the Caddyfile adapter. If you’re pushing raw JSON to /load (which CaddyUI does), you have to use the full {http.error.status_code} / {http.error.id} paths. Lost an hour to that. Writing it down here so you don’t.

E2E-validated against caddy:2-alpine with a reverse_proxy to a dead upstream — 502 returns the branded page with real {http.error.id} and {time.now.http} substitutions.


Upgrade

docker pull applegater/caddyui:v2.5.0
# or
docker pull applegater/caddyui:latest

Multi-arch linux/amd64 + linux/arm64, SBOM + provenance attestations, scratch base, non-root UID 10001.

No schema migration required — captcha settings default to “off” on fresh installs, and existing Turnstile keys carry over.


Links


Happy to answer questions, take feature requests, or hear about things that break. Especially interested in feedback on the error-page design — it’s the first time I’ve written HTML that Caddy itself serves, and I’d rather get the conventions right early.

Thanks again for approving the last thread — really appreciate the warm reception. :folded_hands:

1 Like

This looks very cool and useful, thanks for sharing it. I know a lot of people have asked for a management interface.

How much of it was vibe coded? Edit: Ah, I see the disclosure in the GitHub readme. I guess Claude did most of the heavy lifting. Thanks for using your credits for a Caddy project! I’m sure it can benefit a lot of people.

Huh, sorry about that. I don’t see why that is surprising though: why would you expect the Caddyfile docs to apply to the JSON config? If you’re writing JSON config, don’t use the Caddyfile docs. IMO the placeholders are very clearly explained in the JSON docs: json/apps/http/servers/errors/routes — Caddy Documentation

If you can elaborate on where the confusion lies maybe we can improve the situation…

Like you wrote the HTML yourself? Congrats, if so – it can take some getting used to. :slight_smile:

1 Like