-
-
Notifications
You must be signed in to change notification settings - Fork 0
Installation
dcm has two parts: a privileged CLI backend (dcm-cli) that runs on every node, and a web frontend that runs on one of them. Everything except step 1 is done on that one node — the first sudo dcm-cli sync carries the launch config, the drop-ins, the host files, the node list and the dcm-cli binary to all the others.
- Two or more Debian/Ubuntu nodes running
dnsmasq, all of them the same version: one identical configuration goes to every node, and dnsmasq refuses to start on an option it does not know. dcm reports a mismatch and holds the UI to what the oldest node supports until it is resolved. - On the node that serves the UI: a web server (Apache2 in the example below) and PHP-FPM (8.x).
-
rsyncand passwordless root SSH from the UI node to every other node. -
addn-hostsset to a directory (not a single file) — dcm keeps its host files there. The setup below creates this drop-in; query logging is switched on later in the UI.
Pick one node to host the web UI — the one that runs the web server and PHP-FPM. With a single exception you do the entire setup on that node; the first sudo dcm-cli sync then replicates everything — the /etc/default/dnsmasq launch config, the drop-ins, the host files, the node list and the dcm-cli binary — to every other node.
The exception is step 1 (freeing port 53): it changes each node's own OS (systemd-resolved and /etc/resolv.conf), is not synced, and must be done on every node.
In the steps below castor is the UI node and pollux a second node — the example cluster from Network. Substitute your own short hostnames (hostname -s). Run every step as root.
Every other node needs only: dnsmasq installed and enabled, step 1 done locally, and passwordless root SSH reachable from the UI node (step 7). Its /etc/default/dnsmasq, drop-ins and the dcm-cli binary all arrive on the first sync. All remaining steps run on the UI node.
Two steps below install files out of the dcm source tree (sbin/dcm-cli in step 3, www/ in step 6) and give those files as relative paths, so fetch the tree onto the UI node first and run those commands from inside it. Clone it wherever you keep sources — nothing is ever served from that directory:
apt-get install -y git
git clone https://github.com/ernolf/dcm.git
cd dcmThere is no build step; dcm is one shell script and plain PHP.
If you would rather not install git on the node, download an archive of the same tree instead. The layout inside is identical, only the directory name differs, so every command below works unchanged:
curl -fsSL https://github.com/ernolf/dcm/archive/refs/heads/main.tar.gz | tar xz
cd dcm-mainThe same goes for a release tarball or zip once releases are published: unpack it and cd into the directory it creates.
What the tree contains:
dcm/
|-- sbin/dcm-cli the privileged CLI backend -> /usr/local/sbin/dcm-cli (step 3)
|-- www/ the web frontend -> /var/www/dcm (step 6)
`-- tools/ helper to regenerate the directive manual, not installed
On systemd-based distros systemd-resolved occupies port 53 on 127.0.0.53, so dnsmasq cannot bind it. Disable only the stub listener — systemd-resolved keeps running as the upstream / per-link DNS manager.
/etc/systemd/resolved.conf:
[Resolve]
DNSStubListener=nosudo systemctl restart systemd-resolvedPoint the node's own resolver at dnsmasq:
rm -f /etc/resolv.conf
echo 'nameserver 127.0.0.1' > /etc/resolv.confAlways required, regardless of upstream encryption. A DoH/DoT/DoQ proxy (dnscrypt-proxy & co.) listens on a different port as dnsmasq's upstream and never touches port 53 — it does not replace this step. Until port 53 is free, dnsmasq cannot bind it and the service fails to start (
failed to create listening socket for port 53: Address already in use).
Why not just keep systemd-resolved and bind dnsmasq to its own addresses instead?
-
You could:
bind-interfaces/bind-dynamicmakes dnsmasq bind127.0.0.1and the LAN IP individually instead of the default wildcard0.0.0.0:53, so it no longer collides with the stub on127.0.0.53:53. But running both resolvers at once is messy and fragile:-
Two sources of truth. Anything that still reaches
127.0.0.53— apps using the NSSresolvemodule, or aresolv.confthat got repointed — bypasses dnsmasq entirely. Yourhosts/isolated/ split-horizon rules and dnsmasq's query log do not apply, so dcm's live log and analytics silently miss those lookups. -
resolv.conf churn.
systemd-resolved(together with NetworkManager / netplan) may rewrite/etc/resolv.confback to127.0.0.53on a network change or reboot, switching the box off dnsmasq without warning. -
More moving parts.
bind-interfacesonly binds interfaces that exist at startup; addresses that appear later (DHCP, VPN tunnels) needbind-dynamic. The default wildcard bind avoids that — but the wildcard is exactly what collides with the stub.
Freeing port 53 once (
DNSStubListener=no) avoids all of it, and you do not need the stub: dnsmasq is your resolver.Aside:
listen-addressalone does not prevent the collision — by default it is only a software filter over the wildcard0.0.0.0:53socket, not an address-specific bind. -
Two sources of truth. Anything that still reaches
dnsmasq is configured entirely through drop-ins in /etc/dnsmasq.d/; there is no monolithic config file. Point dnsmasq at an empty config file so it reads only the drop-in directory.
/etc/default/dnsmasq:
ENABLED=1
DNSMASQ_OPTS="--conf-file=/dev/null"
CONFIG_DIR=/etc/dnsmasq.d,.dpkg-dist,.dpkg-old,.dpkg-new
IGNORE_RESOLVCONF=yes--conf-file=/dev/null overrides the compiled-in default (/etc/dnsmasq.conf) with an empty file, so dnsmasq's entire configuration comes from CONFIG_DIR (passed to dnsmasq as --conf-dir). dcm needs a hosts directory and a log file; create those as drop-ins:
mkdir -p /etc/dnsmasq.d
printf 'addn-hosts = /etc/dnsmasq.d/hosts\n' > /etc/dnsmasq.d/addn-hosts.conf
printf 'log-facility = /var/log/dnsmasq/dnsmasq.log\n' > /etc/dnsmasq.d/log-facility.confOnly the log file is defined here. Query logging itself is switched on later on the Configuration page — that also drives the Live Log and Analytics pages, which stay hidden until logging is on. Defining the file but enabling logging in the UI keeps a fresh install fail-safe.
Create the hosts directory and its three files:
mkdir -p /etc/dnsmasq.d/hosts
touch /etc/dnsmasq.d/hosts/{local,vms,isolated}These are ordinary dnsmasq hosts files (loaded via addn-hosts), each managed by its own UI page:
-
local— your LAN hosts, including one entry per cluster node (required — see below). -
vms— virtual-machine records, with one-click subnet relocation in the UI. (Interim solution: a future version will drop manual relocation and auto-detect the network, so a VM is always reachable by name no matter which connected network it is started in.) -
isolated— software phone-home endpoints (e.g. the license-check servers of Acronis, Adobe, …) pinned to127.0.0.1and::1so those lookups fail silently. This is not an ad-blocking list; ad-list blocking is a separate, still-planned feature.
hosts/local must contain one line per node mapping its hostname to its IP — dcm-cli reads these to generate each node's listen.conf:
192.168.189.1 castor
192.168.189.101 pollux
From the source directory (see Get the source):
install -o root -g root -m 755 sbin/dcm-cli /usr/local/sbin/dcm-cli
mkdir -p /etc/dcm
printf '%s\n' castor pollux > /etc/dcm/nodes # short hostnames, one per lineList all nodes (including this one) in /etc/dcm/nodes. The binary and this file are pushed to the other nodes on the first sync, so you install them here only.
echo 'www-data ALL=(root) NOPASSWD: /usr/local/sbin/dcm-cli *' > /etc/sudoers.d/dcm-cli
chmod 440 /etc/sudoers.d/dcm-cli
visudo -c # validate before relying on itPHP-FPM ships with ProtectSystem=full, which makes /etc read-only for the service and everything it spawns (including sudo dcm-cli). Grant write access with a drop-in override — never edit the packaged unit file directly:
sudo systemctl edit php8.x-fpmAdd only the section and line you need:
[Service]
ReadWritePaths=/etc/dnsmasq.d /etc/dcmsystemd saves this to /etc/systemd/system/php8.x-fpm.service.d/override.conf (which survives package updates), then reload and restart:
sudo systemctl daemon-reload
sudo systemctl restart php8.x-fpmWhy a drop-in — and how to verify or revert it
-
Do not edit
/lib/systemd/system/php8.x-fpm.servicedirectly: that file is owned by thephp8.x-fpmpackage and is silently overwritten on the next update (e.g. from deb.sury.org), breaking your setup again without warning. A drop-in override under/etc/systemd/system/is the supported way to customise a package-provided unit and survives updates.Verify the merged result at any time:
sudo systemctl cat php8.x-fpm
Revert to the package defaults:
sudo systemctl revert php8.x-fpm sudo systemctl daemon-reload
List every customised, overridden or masked unit on the system:
sudo systemd-delta
From the source directory (see Get the source):
mkdir -p /var/www/dcm
cp -r www/* /var/www/dcm/
chown -R www-data:www-data /var/www/dcmThe UI writes dnsmasq files directly (as www-data): the hosts and upstream files, and — on the Configuration page — one <directive>.conf drop-in per setting in /etc/dnsmasq.d/. Creating and removing those drop-ins needs write access to the directory itself, so make it group-writable by www-data (setgid, so new drop-ins inherit the group); give www-data the files it edits and keep the rest root-owned:
# the Configuration page creates/removes <directive>.conf drop-ins here
chown root:www-data /etc/dnsmasq.d && chmod 2775 /etc/dnsmasq.d
# UI-edited drop-ins (per-directive config + upstream.conf) -> owned by www-data
chown www-data:www-data /etc/dnsmasq.d/*.conf
chown www-data:www-data /etc/dnsmasq.d/hosts/local /etc/dnsmasq.d/hosts/vms /etc/dnsmasq.d/hosts/isolated
# generated per node, read-only in the UI by design -> stays root-owned
chown root:root /etc/dnsmasq.d/listen.conf(www-data already runs dcm-cli as root via sudo, so making the drop-in directory group-writable is not an additional exposure.)
Apache vhost (/etc/apache2/sites-available/dcm.conf) — adjust the domain and TLS to your environment:
<VirtualHost *:443>
ServerName dns.example.net
ServerAlias dcm.example.net
DocumentRoot /var/www/dcm
DirectoryIndex index.php
SSLEngine on
SSLCertificateFile /path/to/fullchain.pem
SSLCertificateKeyFile /path/to/privkey.pem
<Directory /var/www/dcm>
Options -Indexes
AllowOverride None
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/dcm_error.log
CustomLog ${APACHE_LOG_DIR}/dcm_access.log combined
</VirtualHost>a2ensite dcm
systemctl reload apache2Use a wildcard TLS certificate. The UI node must never be exposed to the internet, so it cannot answer an ACME HTTP-01 challenge. Obtain a wildcard certificate via a DNS-01 challenge (or copy one from a host that already manages it) and point
SSLCertificateFile/SSLCertificateKeyFileat it.
Make the UI reachable by name. Once dcm is the resolver, the vhost's
ServerName/ServerAliasresolve only if dnsmasq knows them. Add an entry mapping each UI hostname to the UI node's IP on the Hosts page (or inhosts/local).
Authentication is intentionally left out —
inc/auth.phpis a no-op stub. Until real auth is added, keep the vhost behind HTTP Basic auth, a VPN, or a trusted network.
dcm-cli sync runs rsync and ssh as root, connecting to root@<other-node>. So root on the UI node needs key-based access to root on every other node.
This may already be in place — test it first:
sudo ssh -o BatchMode=yes root@pollux true && echo OKIf it prints OK, you are done. Otherwise set it up:
Set up root SSH keys from scratch
-
Become root on the UI node and create a key if root does not have one yet (empty passphrase, so the unattended sync can use it):
sudo -i [ -f ~/.ssh/id_ed25519 ] || ssh-keygen -t ed25519 -N "" -f ~/.ssh/id_ed25519
Install root@castor's public key on each other node — this prompts once for that node's root password and appends the key to its
/root/.ssh/authorized_keys:ssh-copy-id root@pollux
Verify, then drop back to your normal user:
ssh -o BatchMode=yes root@pollux true && echo OK exit
If
ssh-copy-idis refused because the other node forbids root login, temporarily setPermitRootLogin yesin its/etc/ssh/sshd_config,systemctl restart ssh, copy the key, then tighten to key-only:PermitRootLogin prohibit-password
sudo dcm-cli sync
sudo dcm-cli restart allsync writes the local listen.conf, then rsyncs the config, the node list and the dcm-cli binary to every other node and regenerates their listen.conf (the one file never copied between nodes). restart all then restarts dnsmasq on every node so the freshly synced config takes effect.
| Path | Owner / mode | Notes |
|---|---|---|
/usr/local/sbin/dcm-cli |
root 755
|
CLI backend, identical on every node |
/etc/dcm/nodes |
root | node short-hostnames, one per line |
/var/lib/dcm/cluster-build |
root 644
|
each node's dnsmasq version and build, written by health, read by the UI |
/etc/sudoers.d/dcm-cli |
root 440
|
lets www-data run dcm-cli as root |
/var/www/dcm |
www-data | web frontend |
/etc/dnsmasq.d |
root:www-data 2775
|
drop-in dir; UI creates/removes <directive>.conf here |
/etc/dnsmasq.d/*.conf |
www-data | per-directive drop-ins + upstream.conf, edited in the UI |
/etc/dnsmasq.d/hosts/{local,vms,isolated} |
www-data | host records, edited in the UI |
/etc/dnsmasq.d/listen.conf |
root | per-node, generated, never synced |
- The web interface — the pages you now have, and what the bell means
-
Hosts files — fill
hosts/localandhosts/vms -
CLI reference — what
sync,diffandhealthactually do
© 2026 [ernolf] Raphael Gradenwitz · GPL-3.0-or-later · Report an issue
Getting started
Managing the cluster
Under the hood
What comes next