Rolling a world back meant hand-work over ssh: stop the service, move the live save aside, unzip the right archive, chown into the container's subuid range, relabel, start. That is the wrong shape of task to do by hand, and it is always done under time pressure -- by construction, because the archive you want is being deleted while you work. PZ keeps BackupsCount=10 per set and writes one every BackupsPeriod=30 minutes, so a periodic backup is reachable for about five hours and then gone. On 2026-09-05 the snapshot the admins asked for (05:16, four minutes before the incident) had about 90 minutes of life left when the request came in. Same shape as the wipe: the Discord bot writes a trigger file into its own rw volume, zomboid-restore.path notices it, and zomboid-restore.service runs the script as the podman user. The bot gets no ssh, no systemd, and keeps only its existing read-only mount of the Zomboid volume. Two details carry most of the correctness. Resolution is by mtime, not by index. The rotation renames the files -- today's backup_7.zip is backup_8.zip half an hour from now, and a new backup_7.zip holds a different world -- so an index is valid only while the listing is fresh, which is not long enough to survive a human reading a confirmation prompt. The trigger names a set and an mtime; the script resolves the path itself, whitelists the filename, and refuses if nothing matches. It never accepts a path. Everything that can fail is checked before the server is touched. A rotated-out target, an archive with no debbzoid world in it, a bad action, a traversal attempt in the set name: each aborts with the server still running and writes a result file the bot reports back. The live world is moved aside rather than deleted, so a restore is undoable and the last three are kept. One thing PZ does not advertise: its backups do not cover the whole save directory. blam/, a mod's own state, is in none of them -- not the 05:16 archive and not the newest one. Restoring only what the archive holds therefore lands the world slightly *behind* the target rather than on it, so anything present in the displaced world and absent from the archive is carried across. The gregtime tag moves to 3.17.0 for the bot half of this -- `backups`, `restore <n>`, `restore confirm`, `restore undo`, gated to the same two admins as the wipe. That image is built and running on the host; its source is not committed yet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016QdWYhwUtwM2NQGukiRh12
Deploy Home
There's no place like home!
Just as Dorothy managed the simple task of clicking her heels together, the desire for an equally simple one-button push deployment was in my heart. Thus, this repository was made.
Ansible
Ansible, along with double encrypted secrets, deploys the necessary configurations to make the home fit for certain needs and desires. Namely, having access to my home from anywhere, securely, and a self-hosted CI server that easily ties into existing workflows.
Makefile
The makefile is primarily used as a wrapper script to ensure that necessary
files, such as the secret vault password file, are provisioned as part of this.
One such addition to the task is utilizing dependency pinning through the
utilization of Python's virtualenv to lock down the specific dependency
versions within the requirements.txt file. This, ideally, prevents any
deployment issues with dependency version woes (e.g. version conflicts, major
updates in newest versions, etc.)
| Target Name | Description |
|---|---|
lint |
(default) Runs yamllint and ansible-lint on all YAML files in ansible/ |
deploy |
Deploys everything, or only tasks specified in TAGS= environment variable |
check |
Runs deploy in a "dry-run", showing diff-style outputs on tasks indicating changes |
vault |
Opens the Ansible vault file for editing |