Skip to content

lodge

The lodge extension adds lodge, the tailnet web shell for Frostyard’s apps, to a Frostyard bootc image. It serves Notes, Mail, Read, and Porter from one address on your tailnet. Its own page is lodge. A server that runs the suite’s services runs Floe. Every image carries the extension’s definition, switched off. The extension itself is not in the image: Updex turns it on and downloads it.

You need:

  • A Frostyard bootc image built on Base.
  • Root access to enable, update, and remove the extension.
  • Network access to https://repository.frostyard.org/ext/lodge/.
  • Free space in /var for the image.

lodge has no published test results on Frostyard images yet. Try it on a machine you can spare first.

Make the configuration before you enable lodge. With Updex v2.1.3 and later, the enable starts lodge at once. Make the configuration directory:

Terminal window
sudo install -d -m 0755 /etc/lodge

Put a tagged, pre-approved Tailscale key as TS_AUTHKEY in /etc/lodge/lodge.env, owned by root, mode 0600, before the first start. Without one, sudo lodgectl status shows each node’s login URL. Remove TS_AUTHKEY from the file after the first start.

See what Updex found, then enable the feature and download it now:

Terminal window
updex features list --component=lodge
sudo updex features enable lodge --now

lodge appears as a component, with its feature disabled. The enable command downloads the image, checks its checksum and signature, links it, and refreshes the merge. With Updex v2.1.3 and later, the refresh also creates the users, groups, and directories that the extension declares, before systemd reloads. Upholds= then starts the unit, with no reboot. updex --version shows your version.

The lab has not yet proven this for lodge: no lab run has enabled it live yet. If the unit fails, follow the start-limit-hit steps below, or reboot.

If updex features list does not show the feature, the image is older than the extension. Update the image first.

With an Updex older than v2.1.3, the unit cannot start after the enable: its user and groups do not exist yet. Reboot. At boot the image merges the extensions, runs systemd-sysusers and systemd-tmpfiles --create, reloads systemd, and Upholds= starts the unit. To start it without a reboot:

Terminal window
sudo systemd-sysusers
sudo systemd-tmpfiles --create
sudo systemctl reset-failed lodge.service
sudo systemctl start lodge.service

Check the merge and the unit:

Terminal window
systemd-sysext status
systemctl status lodge.service
sudo lodgectl status

systemd-sysext status lists lodge, systemctl status shows the unit active, and lodgectl status shows lodge’s nodes, apps, sessions, providers, and agent socket.

If the unit shows failed with start-limit-hit, systemd stopped starting it. Run sudo systemctl reset-failed lodge.service, then sudo systemctl start lodge.service, or reboot. Then run the checks again.

Allow people and devices:

Terminal window
sudo lodgectl allow login <login>
sudo lodgectl allow node <node>

To add agents and inference providers, copy /usr/share/lodge/config.example.json to /etc/lodge/config.json, edit it, then run sudo lodgectl reload.

If enable --now exits with a refresh error (refresh_error in JSON), the download worked, but the activation did not. If the output says activation did not finish, the extension is merged, but its users and groups can be missing: reboot. Otherwise the merge state is unknown: reboot. A sudo systemd-sysext refresh by hand does not create the extension’s users and groups. Then run the checks above again.

lodge.service runs on the host, and it is the only unit. There is no socket unit.

The drop-in /usr/lib/systemd/system/multi-user.target.d/10-lodge.conf has Upholds= for the unit, and the preset /usr/lib/systemd/system-preset/40-lodge.preset enables it. It starts at boot. If it stops, even with exit status 0, systemd starts it again. To stop it for longer, remove the extension or mask the unit.

extension-release.lodge sets EXTENSION_RELOAD_MANAGER=1, so a merge or refresh reloads systemd’s unit files.

lodge.service runs /usr/bin/lodge as the user lodge, with its state in /var/lib/lodge.

The sysusers file /usr/lib/sysusers.d/lodge.conf creates the user and group lodge, and the groups lodge-apps and lodge-agents, with lodge in both. The tmpfiles file /usr/lib/tmpfiles.d/lodge.conf creates /run/lodge, owned by lodge, and /run/lodge/apps, owned by root, and /var/lib/lodge, owned by lodge, mode 0700. lodge.service has no RuntimeDirectory=, so stopping lodge does not remove the apps’ sockets.

Each app extension’s sysusers file declares the user lodge too, with the same line, so that the app’s daemon finds the user without lodge’s extension.

The configuration belongs to you, and the extension ships no file in /etc: /etc/lodge/config.json and /etc/lodge/lodge.env. lodge starts without them. A missing config.json means no agents and no providers.

The web shell is /usr/share/lodge/shell/, and the commands are /usr/bin/lodge and /usr/bin/lodgectl. The extension ships no app: each app extension adds its own under /usr/share/lodge/apps/.

Back up lodge’s state before you update. lodge migrates its database when it starts, and an older lodge refuses a newer database. A rollback does not undo a migration. A restore of the state also keeps the names of lodge’s tailnet nodes.

Stop lodge for the backup. Systemd starts an upheld unit again, so mask it while you copy. While lodge is stopped, no browser and no agent reaches any app. The archive holds secrets, so keep it root-only:

Terminal window
sudo systemctl mask --now lodge.service
sudo sh -c 'umask 077; tar -czpf /root/lodge-state-$(date -u +%Y%m%dT%H%MZ).tar.gz /var/lib/lodge'
sudo systemctl unmask lodge.service
sudo systemctl start lodge.service

tar exits 0, and systemctl status lodge.service shows the unit active again.

Check for an update, then install it:

Terminal window
updex features check --component=lodge
sudo updex features update --component=lodge

By default, the update merges the new files at once, and systemd reloads its unit files. Updex v2.1.3 and later also create new users, groups, and directories at once. An older Updex leaves them for the next boot. The running lodge keeps its old binary until it restarts. A reboot applies everything.

The new files wait for the next boot when you pass --no-refresh, when the automatic update timer installs the update, or when the output says refresh skipped: Updex skips the refresh when it cannot refresh safely. Then reboot. See Update.

With an Updex older than v2.1.3, run these commands first. updex --version shows your version.

Terminal window
sudo systemd-sysusers
sudo systemd-tmpfiles --create
sudo systemctl daemon-reload

To run the new version now, restart lodge:

Terminal window
sudo systemctl restart lodge.service

Check the version that runs:

Terminal window
journalctl -u lodge.service -n 20
sudo lodgectl status

The journal shows lodge starting with the new version and commit. lodgectl status shows the state of each app.

Roll back to the previous version:

Terminal window
sudo updex features rollback lodge

The rollback takes effect at the next boot. To apply it now, add --now, then restart lodge as above. See Roll back.

When the older version runs, read the journal:

Terminal window
journalctl -u lodge.service -n 50

If it shows the database is at version N, newer than this lodge's M, the newer lodge migrated the database. Restore the state from your backup. Replace ARCHIVE with the path of the archive:

Terminal window
sudo systemctl mask --now lodge.service
sudo rm -rf /var/lib/lodge
sudo tar -xzpf ARCHIVE -C / var/lib/lodge
sudo systemctl unmask lodge.service
sudo systemctl start lodge.service

The restore loses the tokens, the allow-list changes, and the provider keys and sign-ins made after the backup. If lodge refreshed a codex sign-in after the backup, the restored one is refused: sudo lodgectl status shows the provider unavailable, signed out. To sign in again, run the lodgectl provider login NAME --client-id ID command that the status gives on a computer with a browser, without sudo. Copy the login file it writes to lodge’s host, run sudo lodgectl provider import NAME < FILE, then delete the file on both computers.

Remove lodge:

Terminal window
sudo updex features disable lodge --now --force

Updex unmerges all extensions on the host, deletes the image, and merges the rest again. Reboot when the command finishes. Without --force, Updex refuses while the image is merged.

Item What you get
Package frostyard-lodge, the .deb attached to the app’s GitHub release, installed into the image with dpkg. The build checks the download against a pinned SHA-256, and checks that its dependencies are met before it installs. Its GitHub build-provenance attestation is checked when the pin is set and in every CI build, before the image is built
Feature Lodge (tailnet web shell for Frostyard's first-party apps)
Commands /usr/bin/lodge and /usr/bin/lodgectl
Service lodge.service, which runs /usr/bin/lodge as the user lodge
State /var/lib/lodge, mode 0700
User and group lodge, groups lodge-apps and lodge-agents, with lodge in both, from /usr/lib/sysusers.d/lodge.conf
Runtime directories /usr/lib/tmpfiles.d/lodge.conf: /run/lodge, owned by lodge, and /run/lodge/apps, owned by root
Web shell /usr/share/lodge/shell/
Example configuration /usr/share/lodge/config.example.json
Activation /usr/lib/systemd/system/multi-user.target.d/10-lodge.conf with Upholds=, and /usr/lib/systemd/system-preset/40-lodge.preset
Version The image file name carries the frostyard-lodge package version, which is the app’s release version
Image file lodge_<version>_13_x86-64.raw.zst, where 13 is the OS version ID; .raw.xz, .raw.gz, and .raw also work
Download From https://repository.frostyard.org/ext/lodge/, signature required (Verify=true), into /var/lib/extensions.d/

Each build checks the commands, the unit, the sysusers and tmpfiles files, the web shell’s index.html, the example configuration, and the preset, activation drop-in and extension-release files. A build that is missing one fails. Each build also checks that every upheld unit has no Condition*= line, or is a oneshot that stays active.

Reviewed 2026-10-29. snosi commit e423112908540aa3a8045fa9d8022a3742e3cd17; core commit cd2720e0c55d08f3ba8f28a3b55dd5796a5c425a; lodge v0.1.0 commit a43d3be1de45704e0514d51da4c4b32d575f7c6c; Updex v2.1.3 commit 2661e987faac986f2594d1d9bbbadf1bf3053f5b.