Run TriliumNext Notes as a rootless Podman Quadlet
Replaces the 2022 nginx + docker_container playbook with a Quadlet systemd user unit on emily, and moves state from /var/lib/trillium to ~/podman-data/trilium. There is no database to attach: the state is one SQLite file (document.db, with its WAL sidecars), config.ini, session_secret.txt and Trilium's own backup/ directory, so unlike bitwarden/ and tandoor/ this play has no postgres role and nothing to restore from the shared cluster. The old playbook was wrong about what monoco actually runs, in three ways that each break the deployment on their own. monoco has been on TriliumNext since June 2025 (`triliumnext/notes:latest`, v0.95.0), not the retired zadam/trilium the playbook pulls, and its document.db carries the schema migrations that came with the change; the data directory is /home/node/trilium-data, not /root/trilium-data; and the container sits on trillium_net behind Caddy, with nothing published on the host. All three were read off the live host. UserNS=keep-id:uid=1000,gid=1000, because the entrypoint starts as container-root and drops to the image's node user, and uid 1000 is also what owns the whole Docker-era tree -- so the copied data arrives already in the right shape and needs no chown pass. The unit defines no HealthCmd=: unlike audiobookshelf, this image ships its own HEALTHCHECK, and the play runs it with podman healthcheck run. The mount assertion is a mount check and not a file check, which is where this play has to differ from audiobookshelf's. Asserting that document.db appeared on the host works there because the app creates it; here the migration puts it on the host before the app has ever run, so the file proves nothing. It asserts against podman inspect instead, and checks Source as well as Destination -- a Destination-only check would be satisfied by an anonymous volume mounted where the bind mount should be, which is exactly the silent case. Bind mount, anonymous volume, no mount and a payload with no Mounts key were each run against the expression before this commit; only the first passes. The site is unchanged; the service name is not. The live container is `trillium`, with an extra l, and that exists only in the hand-made docker run that created it. Everything on emily is `trilium`, following the app and this directory, and the only place the misspelling survives is the copy source path /var/lib/trillium that the readme documents. The readme carries the monoco -> emily copy command by command: why the source needs sudo (log/, tmp/ and backup/ are 0700 and backup/ is state), why the copy cannot be hot (document.db is live SQLite in WAL mode), and why the digest comparison has to happen before the first start, since Trilium can migrate the document as it opens it. Of the 106 MB, 91 MB is regenerable logs, and the no-sudo variant that skips them says out loud what it drops. The integration goes through the shared include rather than a local copy -- caddy/roles/register for the network and the Caddy drop-in -- as in the other service playbooks, and it runs in pre_tasks because Quadlet fails unit generation, not merely startup, when a referenced .network file does not exist yet. The dead files/nginx.conf.j2 goes with the nginx front end: Caddy handles the WebSocket upgrade the live editor needs and imposes no request-body limit, so there is nothing to carry over.
This commit is contained in:
1 parent
8136c2ddaf
commit
09c964f34b
9 files changed
+851
-98
No files matched your search
@@ -0,0 +1,31 @@
|
||||
---
|
||||
# Caddy is its own playbook (caddy/playbook.yml) and a rootless Quadlet unit
|
||||
# there. These handlers run as the connection user, so `systemctl --user` and
|
||||
# `podman exec` work without any XDG_RUNTIME_DIR juggling.
|
||||
|
||||
# Notified by this role when the Caddyfile changes. A reload keeps existing
|
||||
# connections; a restart would briefly drop every hosted site.
|
||||
#
|
||||
# Note the config path: /etc/caddy/Caddyfile is the mount inside the container,
|
||||
# not {{ caddyfile_path }}, which is the host side of it.
|
||||
- name: Reload Caddy configuration
|
||||
ansible.builtin.command: >-
|
||||
podman exec {{ caddy_name }} caddy reload --config /etc/caddy/Caddyfile
|
||||
changed_when: true
|
||||
listen: "Reload Caddy"
|
||||
|
||||
# Notified by caddy/roles/register when this service's network unit, or the
|
||||
# drop-in that attaches Caddy to it, is written. Quadlet has no live equivalent
|
||||
# of `docker network connect`, so the container has to be recreated — a brief
|
||||
# restart of every hosted site, not just this one. See caddy/readme.md,
|
||||
# "Trade-off worth knowing".
|
||||
#
|
||||
# Defined after the reload above so that when both are notified the cheap
|
||||
# config reload happens first.
|
||||
- name: Restart Caddy
|
||||
ansible.builtin.systemd_service:
|
||||
name: "{{ caddy_name }}.service"
|
||||
scope: user
|
||||
state: restarted
|
||||
daemon_reload: true
|
||||
listen: Restart Caddy
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
# Append this site's block to the shared Caddyfile.
|
||||
#
|
||||
# The Caddyfile is a bind mount into the Caddy container, so Caddy sees the
|
||||
# change as soon as the file is written; the handler only asks Caddy to re-read
|
||||
# it. That is a reload, not a restart, so the other hosted sites are untouched.
|
||||
#
|
||||
# blockinfile is idempotent on its own via the marker, so there is no need to
|
||||
# slurp the file and compare first — which also matters because slurp *fails*
|
||||
# when the Caddyfile does not exist yet, on a host where the Caddy playbook has
|
||||
# not run.
|
||||
|
||||
- name: "Add the {{ host }} site block to the Caddyfile"
|
||||
ansible.builtin.blockinfile:
|
||||
path: "{{ caddyfile_path }}"
|
||||
marker: "# {mark} Ansible managed Caddy snippet for {{ host }}"
|
||||
block: "{{ snippet_block }}"
|
||||
create: yes
|
||||
owner: "{{ podman_user }}"
|
||||
group: "{{ podman_user }}"
|
||||
mode: '0644'
|
||||
notify: Reload Caddy
|
||||
Reference in new issue
Block a user