docker/Dockerfile.hub builds the console and ships it with the hub sources on node:22-alpine, without node_modules, as a non-root user with a healthcheck. .gitea/workflows/hub.yaml checks and pushes git.shx.one/denozord/technitium-hub (latest from main, sha-*, X.Y.Z and X.Y from hub-v tags), independent of the console pipeline, which now ignores hub-only changes. Compose example for hub.shnt.top and hub/README.md, with the read-only permissions each server's token needs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Technitium hub
A small, dependency-free Node program that looks like ONE Technitium DNS Server to the
console and answers from several real ones. It serves the console's static build and a
read-only, Technitium-compatible /api/*:
node=<id>forwards the call to that server, with that server's token.- No
node(ornode=cluster) asks every server in parallel and merges the answers (statistics are summed, top lists and query logs are merged). If a server does not answer, the others are shown and the response carrieshubWarnings. - Anything that would change data is refused with
403. The hub has its own sign-in.
The servers themselves are not changed: each only needs a read-only API token. Create it for a user whose group has View, and nothing else, on Dashboard, Logs, Apps (the logging apps Query Logs reads from) and Settings (the blocking state).
Configuration
| Variable | Default | Meaning |
|---|---|---|
HUB_SERVERS |
required | Ordered id=url list, e.g. dns=https://dns.example.net,ndns=https://ndns.example.net. The first server is shown as the Primary node. |
<ID>_TOKEN |
required | API token of each server; the id is uppercased with non-alphanumerics as _ (dns gives DNS_TOKEN, my-dns gives MY_DNS_TOKEN). |
HUB_USER, HUB_PASSWORD |
required | The hub's own sign-in. It refuses to start without them. |
HUB_PORT |
8080 |
Listening port. |
HUB_CACHE_SECONDS |
15 |
How long successful answers of the servers are cached. |
HUB_TIMEOUT_MS |
8000 |
Per-request timeout towards a server. |
HUB_DOMAIN |
hub |
Shown as the server's domain in the console. |
HUB_STATIC |
./dist |
The console's build to serve. |
Endpoints
Public: status, user/login. After signing in: user/session/get, user/logout,
user/checkForUpdate (always "no update") and these reads:
dashboard/stats/get, dashboard/stats/getTop, dashboard/metrics/json, apps/list,
logs/query, logs/list, settings/get, admin/cluster/state (synthesized).
Everything else answers 403 The hub is read-only.
Run locally
npm run build
node dev/mock-api.mjs --port 5380 &
node dev/mock-api.mjs --port 5381 --scale 2 &
HUB_SERVERS=a=http://127.0.0.1:5380,b=http://127.0.0.1:5381 A_TOKEN=x B_TOKEN=y \
HUB_USER=admin HUB_PASSWORD=admin HUB_PORT=5390 npm run hub
Then open http://localhost:5390. Tests: npx vitest run hub.
Docker
docker run --rm -p 8080:8080 \
-e HUB_SERVERS=dns=https://dns.example.net,ndns=https://ndns.example.net \
-e DNS_TOKEN=... -e NDNS_TOKEN=... -e HUB_USER=admin -e HUB_PASSWORD=... \
git.shx.one/denozord/technitium-hub:latest
The image is built by docker/Dockerfile.hub and published by .gitea/workflows/hub.yaml
(latest from main, sha-<7>, and X.Y.Z / X.Y from a hub-vX.Y.Z tag). A compose
example with Traefik is in docs/compose.hub.example.yaml.