README
¶
DNSFleet
Chinese version: README_zh.md
Unified Operations Console for AdGuard Home fleets
A self-hosted operations console for AdGuard Home at scale: inventory nodes, edit fleet configuration, sync with a full trace, bootstrap from an existing node, and investigate DNS queries across the fleet — while resolution stays on the edge.
DNSFleet is the Hub (desired configuration + inventory in SQLite). Each AdGuard Home instance remains autonomous. Talk to nodes over HTTP; one process serves UI + API.
| You get | You do not get |
|---|---|
| Fleet DNS / filters / clients, sync & drift | A full AdGuard Home UI clone |
| Cross-node query investigation (fan-out) | SIEM / query history warehouse |
| Live Logs (WebSocket tail + REST pages) | Multi-tenant SaaS / microservices |
v0.2 — Operations Console Foundation: structured Fleet Configuration (not JSON-only), Import from node (Bootstrap), Sync Trace (domain / step / node), Query Explorer, and safe sync that skips domains the Hub does not yet manage — so an empty Hub cannot wipe live edge lists.
Scope: one shared Admin credential; query logs are never persisted on the Hub (live observation + on-demand search only). Aimed at homelab and small edge fleets (~5–100 nodes). UI: English (default) and Chinese.
Demo
Fleet

Fleet Configuration

Live Logs

Why DNSFleet
Hub (fleet desired config) → Sync Trace → AdGuard Home nodes
↑ ↑
Bootstrap import Investigate across nodes
- Fleet Configuration — DNS upstream, rewrites, filters, clients; Advanced JSON as a power-user escape hatch.
- Bootstrap — pull from an already-tuned AdGuard Home into the Hub once; Hub stays the source of truth (not Origin→Replica).
- Sync Trace — see what ran, which domain failed, and why — not only ok/fail.
- Query Investigation — fan-out search across online nodes; jump into Live Logs with scope/hint.
- Live Logs — continuous observation with investigation context (scope, session tops) — not a second dashboard.
Who it is for
- Operators who already run AdGuard Home on several hosts and want one console for inventory, configuration, sync, and investigation.
- Teams fine with self-hosted services, Bearer-style admin auth, and TLS at a reverse proxy.
Not a fit (today): replacing the AdGuard Home UI entirely, long-term DNS analytics / SIEM, non–AdGuard Home resolvers as first-class data plane, or hosted multi-tenant SaaS.
Capabilities
- Nodes: CRUD, credentials (
basic/bearer), online/offline, probe, runtime stats snapshot. - Fleet configuration: DNS upstream, rewrites, filters, clients (
GET/PUT /api/v1/config/globalwith partial updates andmanagedflags); Advanced JSON for export/import. - Bootstrap import: preview + apply from an online node (
/api/v1/config/import/*). - Sync & drift: push only managed domains; step-level Sync Trace; scheduled drift with bounded concurrency.
- Query Explorer: cross-node fan-out (
GET /api/v1/query/explore) — investigation only, no querylog persistence. - Dashboard / Overview: fleet snapshot metrics from node-owned AdGH stats — not historical charts.
- Live Logs: Hub polls
GET /control/querylog; browsers useGET /api/v1/ws/logsandGET /api/v1/nodes/:id/querylog(seeapi/DNSFLEET_HTTP_API.md). - Distribution: Next.js static export embedded in one Go binary; Docker / Compose under
deploy/.
Released builds: GitHub Releases — static binaries (Linux / Windows / macOS; amd64/arm64), checksums, and GHCR images (v* workflow).
Run a release binary or container (no Go / Node)
You do not need a compiler on the machine where the control plane runs.
Asset names on GitHub Releases
Each archive or standalone file follows:
dnsfleet-<tag>-<os>-<arch>[.exe]
<tag>is the Git tag for that release (see the release page on GitHub).<os>islinux,windows, ordarwin.<arch>isamd64orarm64..exeis only on Windows.
Version placeholder: In the table and shell examples below, vX.Y.Z is a placeholder — replace it with the actual Git tag from the release you downloaded (for example v0.2.0).
| OS / arch | Example filename |
|---|---|
| Windows amd64 | dnsfleet-vX.Y.Z-windows-amd64.exe |
| Linux amd64 | dnsfleet-vX.Y.Z-linux-amd64 |
| Linux arm64 | dnsfleet-vX.Y.Z-linux-arm64 |
| macOS amd64 (Intel) | dnsfleet-vX.Y.Z-darwin-amd64 |
| macOS arm64 (Apple Silicon) | dnsfleet-vX.Y.Z-darwin-arm64 |
Verify downloads with SHA256SUMS from the same release.
Minimal CLI: native binary
The process reads environment variables first (see Configuration); it does not read a .env file by itself.
Optional flags -admin-token and -listen, when non-empty, override the corresponding configuration fields after values are read from the environment (DNSFLEET_ADMIN_TOKEN and DNSFLEET_HTTP_ADDR, respectively; same sequence as dnsfleet -h). Go uses a single leading -; run dnsfleet -h for built-in help.
Linux / macOS (from the directory containing the binary):
chmod +x dnsfleet-vX.Y.Z-linux-amd64 # Linux example; skip on macOS if already executable
export DNSFLEET_ADMIN_TOKEN='your-long-random-secret'
./dnsfleet-vX.Y.Z-linux-amd64
Windows (PowerShell):
cd ~\Downloads # or wherever you saved the file
$env:DNSFLEET_ADMIN_TOKEN='your-long-random-secret'
.\dnsfleet-vX.Y.Z-windows-amd64.exe
Same examples with a flag instead of export / $env: (handy for a first run; see security note below):
./dnsfleet-vX.Y.Z-linux-amd64 -admin-token 'your-long-random-secret'
.\dnsfleet-vX.Y.Z-windows-amd64.exe -admin-token 'your-long-random-secret'
Optional listen override: -listen :8081 (overrides DNSFLEET_HTTP_ADDR; when the env var is unset, the default remains :8080).
Security: On Unix-like systems, other users may see process arguments in ps(1); on shared hosts prefer DNSFLEET_ADMIN_TOKEN via the environment (systemd, Docker, Compose) or a secret manager instead of putting secrets on the command line.
Then open http://127.0.0.1:8080 (unless you passed -listen). Smoke test: GET /healthz returns ok.
If the window closes immediately when double-clicking the .exe, run it from PowerShell or CMD so you can see the error (usually a missing admin token—set DNSFLEET_ADMIN_TOKEN, pass -admin-token, or use DNSFLEET_ADMIN_INSECURE_DISABLE=1 for local-only). dnsfleet -h prints usage without starting the server or creating the SQLite data directory.
Lower-friction ways to supply the Admin token
Typing export / $env:... each time is normal for servers but annoying on a laptop. Practical options:
-
Docker Compose — clone or copy
deploy/docker-compose.yml, replaceDNSFLEET_ADMIN_TOKEN: "change-me-in-production"with your secret, then from the repo root:
docker compose -f deploy/docker-compose.yml up --build
(No shellexportneeded; seedeploy/README.mdfor volumes and permissions.) -
Small wrapper next to the binary — e.g. on Windows,
run-dnsfleet.ps1containing only setting$env:DNSFLEET_ADMIN_TOKENandStart-Process/& .\dnsfleet-....exe. -
.env+ shell — copy.env.exampleto.env, editDNSFLEET_ADMIN_TOKEN, then in bash:set -a && source .env && set +a && ./dnsfleet-vX.Y.Z-linux-amd64(Still environment variables under the hood; the file is just easier to edit than a long one-liner.)
Optional auto-load of .env inside the Go binary would add behavior and edge cases (Windows paths, quoting, secrets on disk); DNSFleet keeps explicit env only. If you want file-based config without a shell, Compose or a one-line wrapper is the usual approach.
Docker image from GHCR (no local build)
Use the image tag published with the release (example org/repo; confirm on the release page if yours differs):
docker run --rm \
-e DNSFLEET_ADMIN_TOKEN=your-long-random-secret \
-p 8080:8080 \
ghcr.io/lensdns/dnsfleet:vX.Y.Z
Persist SQLite in a volume (path inside the container must be writable; see deploy/README.md):
docker run --rm \
-e DNSFLEET_ADMIN_TOKEN=your-long-random-secret \
-e DNSFLEET_DB_PATH=/data/dnsfleet.db \
-p 8080:8080 \
-v dnsfleet-data:/data \
ghcr.io/lensdns/dnsfleet:vX.Y.Z
Quick start (build from source)
Requirements: Go 1.26+ (see go.mod), Node 22+ only if you rebuild the web UI from web/.
If you only need a prebuilt binary or container, use Run a release binary or container above.
- Copy
.env.exampleto.env(or export the same variables). The process readsos.Getenvonly; it does not auto-load.env. - Set
DNSFLEET_ADMIN_TOKENto a strong secret (unless you deliberately use the insecure dev switch documented below). - Build the UI and embed it, then run:
cd web && npm ci && npm run build && cd ..
make ensure-webui-dist # Unix / Git Bash; or: powershell -File scripts/ensure-webui-dist.ps1
go run ./cmd/dnsfleet # optional: -admin-token … / -listen … / -h
Docker (recommended for trials): from the repository root (build context is the repo root):
docker compose -f deploy/docker-compose.yml up --build
Details: deploy/README.md (volumes, non-root UID, image build args). Local Next dev with API rewrites: web/README.md.
Repository layout
| Path | Purpose |
|---|---|
cmd/dnsfleet/ |
Process entrypoint |
internal/ |
Application code (HTTP, DB, AdGuard Home client, querylog hub, embedded UI) |
api/ |
Public HTTP contract notes (DNSFLEET_HTTP_API.md) |
web/ |
Next.js UI (static export for embed) |
deploy/ |
Dockerfile and Compose |
scripts/ |
Helper scripts (e.g. sync web/out into internal/webui/dist) |
Configuration
All variables are read at startup from the environment (see internal/config/config.go). Optional flags -admin-token and -listen, when non-empty, override DNSFLEET_ADMIN_TOKEN and DNSFLEET_HTTP_ADDR after values are read from the environment (same sequence as dnsfleet -h). Run dnsfleet -h for a short summary.
| Variable | Default | Description |
|---|---|---|
DNSFLEET_DB_PATH |
./data/dnsfleet.db |
SQLite file path (resolved to an absolute path on load). Not :memory:. Parent directory is created if missing. |
DNSFLEET_HTTP_ADDR |
:8080 |
Listen address (Echo). |
DNSFLEET_ADMIN_TOKEN |
(required) | Shared secret for /api/v1 (Authorization: Bearer or X-Admin-Token). Empty token fails startup unless insecure mode is enabled. |
DNSFLEET_ADMIN_INSECURE_DISABLE |
unset | If exactly 1, skips Admin checks and allows an empty token. Do not use in production or on an exposed network. |
DNSFLEET_SYNC_MAX_CONCURRENT |
8 |
Cap concurrent AdGuard Home HTTP calls for drift, POST /api/v1/sync, GET /api/v1/nodes/:id/querylog, and POST /api/v1/nodes/:id/probe. Probes run while creating or editing a node do not use this semaphore (see api/DNSFLEET_HTTP_API.md). |
DNSFLEET_SYNC_TOTAL_TIMEOUT |
5m |
Total timeout for POST /api/v1/sync (time.ParseDuration). |
DNSFLEET_DRIFT_INTERVAL |
5m |
Drift ticker interval; one drift run happens immediately on startup. |
DNSFLEET_QUERYLOG_MAX_CONCURRENT |
8 |
Cap concurrent GET /control/querylog calls from the querylog Hub (independent of sync cap). |
DNSFLEET_QUERYLOG_POLL_INTERVAL |
2s |
Hub polling interval per online node. |
DNSFLEET_QUERYLOG_PAGE_LIMIT |
100 |
Hub single-page limit for GET /control/querylog. REST history uses its own default (20, max 100); the two need not match. |
DNSFLEET_WS_MAX_FRAME_BYTES |
65536 |
Max outbound WebSocket text frame size toward browsers. |
HTTP: GET /healthz (no Admin). /api/v1 REST and /api/v1/ws/logs WebSocket require Admin (see api/DNSFLEET_HTTP_API.md).
Security and limits
- Single operator model: one Admin secret for the control plane API and WebSocket upgrade path used by the UI.
- Query logs are not a database product: tail and REST pages are ephemeral from the operator’s perspective; do not rely on DNSFleet as long-term audit storage.
- Reverse proxies: terminate TLS and forward WebSocket headers (
Upgrade,Connection) or Live Logs will fail through the proxy. - Client bundle: any
NEXT_PUBLIC_*value is fixed at web build time; do not bake insecure skips into production images (seedeploy/docker-compose.ymlcomments).
Development and CI
From the repository root:
go fmt ./...
go vet ./cmd/... ./internal/...
go test ./cmd/... ./internal/...
go test requires a non-empty internal/webui/dist (run make ensure-webui-dist after web production build, or make test which prepares it). Avoid go test ./... from the repo root if web/node_modules exists, to prevent the Go tool from picking up unrelated packages.
Web checks (web/README.md):
cd web && npm ci && npm run lint && npm run test && npm run build
GitHub Actions: .github/workflows/ci.yml runs the Go + web matrix on Ubuntu, Windows, and macOS, uploads Go coverage from the Ubuntu job to Codecov (optional repo secret CODECOV_TOKEN, or enable Codecov’s GitHub app / OIDC on your account), then builds the same Docker image as release with no registry push. Pushing a tag matching v* runs .github/workflows/release.yml: same tests, multi-platform static binaries and SHA256SUMS attached to the GitHub Release, and the image pushed to GHCR.
Design documents and maintainer-only notes are not shipped with this repository; behavior is defined by code and the public files linked above.
Contributing
See CONTRIBUTING.md for how we scope features (operations console, investigation vs history, anti-goals) and what to run before opening a PR.
License
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
dnsfleet
command
|
|
|
internal
|
|
|
config
Package config loads process configuration from environment variables with DNSFleet defaults.
|
Package config loads process configuration from environment variables with DNSFleet defaults. |
|
webui
Package webui embeds the Next.js static export for same-origin delivery.
|
Package webui embeds the Next.js static export for same-origin delivery. |