Skip to content

Upgrade and back up

Releases are listed in the changelog. Security-relevant node releases are flagged there.

Container: pull the new version and recreate the container; the volume keeps the identity, the last profile and lists, and the logs.

Terminal window
docker pull ghcr.io/operomtl/opdns-node:<version>
docker rm -f opdns-node
docker run ... # same command, new tag
# or, with Compose: edit the tag, then
docker compose up -d

The image runs as user opdns with the fixed ids 101:102, the ids the first images used, so an existing volume stays readable after an upgrade.

Binary: download and verify the new binary as on Install the static binary, replace /usr/local/bin/opdns-node, and sudo systemctl restart opdns-node. The systemd unit grants the port-binding capability itself, so nothing else is needed. If a release changes the unit, install the new opdns-node.service and run sudo systemctl daemon-reload first. Upgrade Unbound with your package manager; the container ships its own pinned Unbound.

A certificate renewal needs no restart: the node reloads renewed files by itself (certificates). Some configuration keys need none either (reload without a restart).

The node stops cleanly on SIGTERM: it drains its listeners, flushes pending log records, closes the link and stops Unbound.

  • The log database. When a release changes the schema of logs.sqlite, the node first copies the database to backups/pre-v<N>.db in the data directory (N is the old schema version; the copy is skipped, with a warning, when the disk has no room for it), then migrates it in one transaction. A failed migration leaves the database as it was. The node refuses a database written by a newer release, naming the pre-v<N>.db copy to go back to, so a downgrade never misreads it.
  • The configuration file. Its version key says which format it is (1 today). An older file is migrated when it is loaded; a newer one is refused (Configuration).
  • A corrupt database. A database found corrupt at start (after a power cut, say) is moved to backups/corrupt-<time>.db and a fresh one is created; the local page says so. DNS is not affected.

opdns-node backup writes one archive of everything the node cannot fetch again. It is safe while the node runs.

Terminal window
sudo -u opdns opdns-node backup /var/backups/opdns-node.tar.gz
# container:
docker exec opdns-node opdns-node backup /var/lib/opdns/opdns-node-backup.tar.gz
docker cp opdns-node:/var/lib/opdns/opdns-node-backup.tar.gz .

The archive is a .tar.gz written with mode 0600; the command refuses to overwrite an existing file. It holds:

From the data directory Holds
node.json node id and node token
profile.json the last good profile
lists_keys.json list signing keys learnt from the cloud or latest.json
profile_keys.json profile signing keys learnt from the cloud (Profile signing keys)
lists_announced.json the last list version the cloud announced
secrets/ the local API token
logs.sqlite a consistent snapshot of the query log database

lists/ and unbound/ are left out (they are downloaded or regenerated), and so are backups/ and the lock file.

Copying logs.sqlite by hand while the node runs is not safe: recent rows can still be in its -wal file. Use opdns-node backup, or stop the node first.

To move a node to a new host, or to a new container volume:

  1. Stop the old node, and take a fresh backup of it.

  2. Install the node on the new host without starting it.

  3. Restore into its (empty) data directory, then start it:

    Terminal window
    sudo -u opdns opdns-node restore /var/backups/opdns-node.tar.gz
    sudo systemctl start opdns-node

It comes back enrolled, with its profile, list and profile keys and logs. If the node token rotated after the backup was taken, the restored token is no longer valid: enrol the new host again instead. restore takes the data directory’s lock, so it fails while a node runs on that directory. It refuses a directory that already holds an identity or a log database unless you pass --force, accepts only the files backup writes, and checks the database’s integrity before it moves anything into place.

Only one node may use an identity at a time: a second copy running at once replaces the first one’s link session. Without an archive, enrol the new host again instead (Enrol).

Default /var/lib/opdns, created with mode 0700.

Path Holds In the backup
node.json node id and node token (secret, 0600) yes
profile.json last good profile yes
lists/ last good list artifacts no (re-fetched)
lists_keys.json list signing keys learnt from the cloud or latest.json yes
profile_keys.json profile signing keys learnt from the cloud; removed on unenrol yes
lists_announced.json the last list version the cloud announced, so a restart still tells a rollback from an old notice yes
archive/ standalone-profile-<time>.json: a standalone profile uploaded at enrolment no
secrets/ api_token, the generated local API token (secret, 0600; standalone mode), and api_token.shown once the page has shown it yes
logs.sqlite (+ -wal, -shm) query logs yes, as a snapshot
backups/ pre-v<N>.db copies taken before a schema migration, corrupt-<time>.db databases moved aside no
unbound/ generated config and root key no

The node refuses to start while node.json or a file under secrets/ is readable by every user of the host (anyone who can read them can act as the node). It writes them 0600; a looser mode comes from a copy or a restore by hand, and the error names the file and the fix:

Terminal window
sudo chmod 600 /var/lib/opdns/node.json /var/lib/opdns/secrets/*

A file readable by its group only is logged as a warning.