Cut the comment bloat from the Podman playbooks

Comment blocks now stay within three lines and explain only the non-obvious. Long histories, include examples, and commented-out code are gone. No task, key, or value changed; the only data change is comment text inside the generated unit files.
This commit is contained in:
tanshu committed 2026-10-18 22:10:57 +05:30
1 parent 2aab1aec1e
commit 476a6d7cd0
85 files changed
+372 -2433

No files matched your search

+2 -38
View File
@@ -1,33 +1,5 @@
---
# TriliumNext Notes as a rootless Podman Quadlet unit, behind the shared Caddy
# unit (see caddy/playbook.yml).
#
# Unlike bitwarden/ and tandoor/, this app has no database to attach: its state
# is one SQLite file (document.db, with its WAL sidecars) plus config.ini,
# session_secret.txt and an app-managed backup/ directory, all in a single
# directory — so there is no postgres role here and nothing to restore from the
# shared cluster.
#
# Connects as the container user and runs everything as that user: the Quadlet
# units belong to it, so the connection's PAM session is what makes
# `systemctl --user` work without any environment juggling.
#
# gather_facts is required: podman_user and the home directory come from the
# user_id / user_dir facts.
#
# The rootless-Podman prerequisites (installing podman, enabling lingering, the
# low-port sysctl, creating the Quadlet directory) are deliberately NOT applied
# here. They are the shared task file at caddy/roles/podman/tasks/main.yaml, and
# a host that has already run caddy/playbook.yml has them:
#
# - name: Rootless Podman prerequisites
# ansible.builtin.include_tasks: "{{ playbook_dir }}/../caddy/roles/podman/tasks/main.yaml"
#
# The variables this play needs are duplicated at the top of vars/default.yml, so
# it stands on its own. (Applying the commented include above would also need
# podman_unprivileged_port_start, which lives in caddy/vars/default.yml.)
#
# Run with: ansible-playbook -u tanshu trilium/playbook.yml
# Requires caddy to have run first. State is one SQLite file, not Postgres.
- name: Deploy TriliumNext Notes as a rootless Podman Quadlet
hosts: emily
gather_facts: true
@@ -35,18 +7,10 @@
- vars/default.yml
pre_tasks:
# This must happen before the app unit is started, not after: the generated
# unit references {{ network_name }}.network, and Quadlet fails *unit
# generation* (so `systemctl --user start` reports "unit not found") when a
# referenced .network file does not exist yet. The same include writes the
# Caddy-side drop-in that attaches Caddy to this network.
#
# network_name is a play var, so the include can see it directly.
# Quadlet needs the network unit before it generates the app unit.
- name: Register Trilium's network with Caddy
ansible.builtin.include_tasks: "{{ playbook_dir }}/../caddy/roles/register/tasks/main.yaml"
roles:
# The app: the data directory, the Quadlet unit, start + verify.
- trilium
# Appends this site's block to the shared Caddyfile and reloads Caddy.
- caddy
+4 -17
View File
@@ -1,27 +1,14 @@
---
# 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.
# A reload keeps existing connections. A restart would briefly drop every hosted site.
# The config path is inside the container, not caddyfile_path on the host.
- 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.
# Quadlet has no live equivalent of network connect, so Caddy is recreated and every hosted site restarts.
# Defined after the reload so the cheap reload runs first.
- name: Restart Caddy
ansible.builtin.systemd_service:
name: "{{ caddy_name }}.service"
-11
View File
@@ -1,15 +1,4 @@
---
# 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 }}"
+12 -63
View File
@@ -1,13 +1,5 @@
---
# TriliumNext Notes as a rootless Podman Quadlet unit.
#
# Everything here runs as {{ podman_user }} — the connection user. No become, no
# XDG_RUNTIME_DIR: the SSH connection gave us a real PAM session, so systemd's
# user manager is reachable through the standard environment.
# The image's data directory is one tree: document.db with its WAL sidecars,
# config.ini, session_secret.txt, and the three subdirectories below. It is
# created here as the copy target as much as for a fresh install.
# The image keeps document.db, config.ini, session_secret.txt and the subdirectories below in this one tree.
- name: Ensure the Trilium data directory exists
ansible.builtin.file:
path: "{{ data_dir }}"
@@ -16,12 +8,8 @@
group: "{{ podman_user }}"
mode: '0755'
# Trilium creates these itself on first start, at 0700 — which is exactly how
# they are on monoco. Creating them here means a fresh host and a migrated one
# look the same, and that the destinations named in readme.md exist before the
# copy. log/ is where the app writes its daily log — which the play reads back
# later as evidence that the container is writing to the host — and backup/ is
# where its own zipped document.db backups land.
# Trilium creates these at 0700 on first start. Creating them here makes a fresh host and a migrated one look the same.
# log/ holds the daily log the play reads back as evidence, and backup/ holds the app's own zipped backups.
- name: Ensure the app-managed subdirectories exist
ansible.builtin.file:
path: "{{ item }}"
@@ -34,11 +22,7 @@
- "{{ tmp_dir }}"
- "{{ backup_dir }}"
# Checked before the unit starts, because this is the one thing the play cannot
# tell after the fact: with no document.db the app serves a brand new, empty
# document, which is right for a fresh install and wrong for a migration whose
# copy has not run yet. Once the app has started it creates the file itself and
# the evidence is gone.
# Check before the unit starts, because an empty document it creates hides whether the copy ever landed.
- name: Check whether a document has been copied in
ansible.builtin.stat:
path: "{{ data_dir }}/document.db"
@@ -64,15 +48,7 @@
- Reload systemd user units
- Restart Trilium
# Quadlet honours [Install] WantedBy= when it generates the unit, so
# `systemctl --user enable` is neither needed nor possible on a generated unit.
#
# All three steps are in one block because they fail the same way and want the
# same evidence. `systemctl start` reports success as soon as it has handed the
# unit to systemd, so a container that dies a second later — `User=0` missing, a
# network that was never attached, an unwritable data directory — surfaces here
# as `activating`, or as a health check that never passes, with the real reason
# only in the journal. Restart=always makes that a crash loop, not an error.
# Quadlet honours [Install] WantedBy=, so the generated unit is never enabled.
- name: Start and verify Trilium
block:
- name: Start Trilium
@@ -82,9 +58,7 @@
state: started
daemon_reload: true
# Retried rather than checked once: the container is coming up, and podman
# reports `activating` for the interval between systemd starting it and the
# container's own readiness notification.
# Retried because podman reports `activating` until the container signals readiness.
- name: Verify Trilium is running
ansible.builtin.command: "systemctl --user is-active {{ unit_name }}"
register: trilium_active
@@ -93,11 +67,7 @@
delay: 5
changed_when: false
# `is-active` says the container is running; it does not say the app can
# serve. The unit defines the probe (HealthCmd=, see the template) because
# the image ships none, so this runs exactly that command once,
# synchronously, and fails while the app is still migrating or not yet
# listening.
# The image ships no health check, so the unit defines one and this runs it synchronously.
- name: Wait for Trilium to pass its health check
ansible.builtin.command: "podman healthcheck run {{ container_name }}"
register: trilium_health
@@ -106,10 +76,7 @@
delay: 5
changed_when: false
rescue:
# systemd reports "control process exited with error code" and nothing
# else, which is useless for the usual failures. The journal is where the
# entrypoint's own output is — that is where "su: Authentication failure"
# or a failed bind mount shows up.
# systemd only reports "control process exited with error code", so read the journal for the real reason.
- name: Collect Trilium diagnostics
ansible.builtin.command: "{{ item }}"
loop:
@@ -128,19 +95,8 @@
ansible.builtin.fail:
msg: "Trilium did not start — see the diagnostics above."
# The check that catches a missing or mistyped Volume= line, which is the one
# failure here that is silent: the app would run perfectly well with document.db
# in the container's writable layer — or in an anonymous volume, if the image
# declares one — and lose it on the next recreate.
#
# It has to be a mount check and not a file check. Audiobookshelf can assert that
# its database appeared on the host, because the app creates it; here the
# migration puts document.db on the host *before* the app ever runs, so its
# presence proves nothing about whether the container can see it.
#
# Both halves matter: the destination alone would still be satisfied by an
# anonymous volume mounted where the bind mount should be, which is exactly the
# silent case. `Source` is the path Quadlet passed to podman, verbatim.
# A missing or mistyped Volume= line would run fine and lose document.db on the next recreate, so check the mount.
# Both Source and Destination must match, since a destination-only check would pass an anonymous volume.
- name: Read the running container's configuration back
ansible.builtin.command: "podman inspect {{ container_name }}"
register: trilium_inspect
@@ -162,12 +118,7 @@
check the Volume= line in the generated unit
({{ quadlet_dir }}/{{ container_name }}.container).
# Evidence rather than a gate: with keep-id:uid=1000 mapped onto
# {{ podman_user }}, what the app writes lands in {{ data_dir }} owned by
# {{ podman_user }} — and the app writes a daily log the moment it starts. A log
# touched in the last few minutes is that, visible from the host. It is reported
# rather than asserted because a slow start can legitimately leave nothing here
# yet; the mount assertion above is the hard check.
# Evidence, not a gate: a log touched in the last five minutes shows the app writing into the host data directory, while a slow start may leave none.
- name: Look for a log written since the unit started
ansible.builtin.find:
paths: "{{ log_dir }}"
@@ -190,9 +141,7 @@
path: "{{ data_dir }}/document.db"
register: trilium_document_db_after
# A migrated document.db is a few megabytes; the empty one the app creates on a
# fresh install is kilobytes. Worth printing, because it is the difference
# between "the notes came across" and "the copy never ran".
# A migrated document.db is megabytes; the empty one from a fresh install is kilobytes.
- name: Report the document on the host
ansible.builtin.debug:
msg: >-
+12 -63
View File
@@ -1,94 +1,43 @@
---
# ---------------------------------------------------------------------------
# Rootless Podman / Quadlet
# ---------------------------------------------------------------------------
# Duplicated on purpose from caddy/vars/default.yml: a play cannot include that
# file wholesale, because it also sets `host_directory` — to the Caddy data
# directory — and every playbook defines that for itself. Keep these three in
# sync with caddy/vars/default.yml. (Its fourth, podman_unprivileged_port_start,
# belongs to the podman tasks, which this play does not run: Trilium publishes
# nothing host-side.)
#
# podman_user is derived from the connection: this play carries no become, so
# the Quadlet units belong to whoever Ansible connects as. Requires gather_facts
# (set in playbook.yml).
# Duplicated from caddy/vars/default.yml on purpose, so keep them in sync.
# podman_user comes from the connection user, so the play needs gather_facts.
podman_user: "{{ ansible_facts['user_id'] }}"
podman_data_dir: "{{ ansible_facts['user_dir'] }}/podman-data"
quadlet_dir: "{{ ansible_facts['user_dir'] }}/.config/containers/systemd"
# ---------------------------------------------------------------------------
# TriliumNext Notes
# ---------------------------------------------------------------------------
# The site is unchanged from the Docker era. The *service* name is not: this
# directory and the Docker-era playbook call it `trilium`, while the live
# container on monoco — which nobody deployed from here — is `trillium`, with
# the extra `l`. Everything on emily is `trilium`, following the app's real name
# and this playbook's own history; the only place the misspelling survives is
# the source path /var/lib/trillium in the copy commands in readme.md.
host: notes.tanshu.com
container_name: trilium
unit_name: "{{ container_name }}.service"
# Fully qualified on purpose: rootless Podman resolves short names against
# unqualified-search-registries, and there is no TTY to answer a prompt when
# the systemd unit starts.
#
# This is the *TriliumNext* image, not the retired `zadam/trilium` the old
# playbook pulled: monoco has been running TriliumNext since June 2025, and its
# document.db carries the schema migrations that came with it. Pulling the old
# image against that database would not work.
#
# `latest` has floated, and by more than a patch: monoco runs v0.95.0 (built
# 2025-06-15) and the pull on emily is v0.106.0 (2026-09-25). TriliumNext
# migrates document.db forward on first start, so the cutover is also an app
# upgrade -- one-way for the copy on emily, which is why the readme says to
# compare digests before the first start and to leave monoco's own tree alone.
# Pin a digest or a version tag here if the migration should move the runtime
# only and leave the app version to a separate, deliberate step.
# This is the TriliumNext image, not the retired zadam/trilium. Its document.db carries migrations the old image cannot run.
# `latest` has floated well beyond monoco's version, so pin a digest or tag if the runtime should not move.
image: docker.io/triliumnext/notes:latest
# The Quadlet file is <network_name>.network. caddy/roles/register pins the
# Podman network name to <network_name>_net (so: trilium.network,
# trilium_net).
# caddy/roles/register pins the Podman network to <network_name>_net, so the Quadlet file is trilium.network.
network_name: "{{ container_name }}"
# The image's data directory, and the whole of this service's state. Unlike
# audiobookshelf/ there is one mount target rather than four, because Trilium
# keeps document.db, config.ini and its own subdirectories together.
# The image's data directory and the whole of this service's state, all in one tree.
data_dir: "{{ podman_data_dir }}/{{ container_name }}"
container_data_dir: /home/node/trilium-data
# Subdirectories the app creates and owns itself, at mode 0700 — that is exactly
# what they are on monoco. Created by the role so a fresh host and a migrated one
# look the same, and so the copy target in readme.md exists:
#
# log/ one file per day, 91 MB of the 106 MB tree on monoco. Regenerable.
# tmp/ scratch, empty at rest.
# backup/ Trilium's own zipped document.db backups, 12 MB. Worth keeping.
# The app creates these at 0700 on monoco. Creating them here makes a fresh host and a migrated one look the same.
# log/ is regenerable, tmp/ is scratch, and backup/ holds the app's own zipped backups worth keeping.
log_dir: "{{ data_dir }}/log"
tmp_dir: "{{ data_dir }}/tmp"
backup_dir: "{{ data_dir }}/backup"
# The port the app listens on inside the container, from config.ini's
# `port = 8080`, which the Docker-era container used unchanged. Caddy proxies to
# it; nothing is published on the host.
# The port from config.ini. Caddy proxies to it, and nothing is published on the host.
container_port: 8080
# The unit's own HEALTHCHECK, because the image does not ship one: v0.106.0
# dropped the docker_healthcheck.cjs that v0.95.0 had, so `podman healthcheck
# run` otherwise fails with "no defined healthcheck". /api/health-check is the
# endpoint the app advertises itself, writing it to `healthcheck-url` in the data
# directory at startup. `curl` is in the image; `wget` is not.
# The image ships no health check, so the unit defines one from the endpoint the app advertises.
# `curl` is in the image; `wget` is not.
health_cmd: "curl -fsS -o /dev/null http://127.0.0.1:{{ container_port }}/api/health-check"
# The shared Caddy unit and its Caddyfile, both owned by caddy/playbook.yml.
caddy_name: caddy
caddyfile_path: "{{ podman_data_dir }}/{{ caddy_name }}/conf/Caddyfile"
# Caddy handles the WebSocket upgrade Trilium's live-updating editor needs, and
# imposes no request-body limit, so there is nothing to carry over from the old
# nginx front end.
# Caddy handles the WebSocket upgrade the live editor needs and imposes no request body limit.
snippet_block: |
{{ host }} {
reverse_proxy {{ container_name }}:{{ container_port }}