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:
1 parent
2aab1aec1e
commit
476a6d7cd0
85 files changed
+372
-2433
No files matched your search
+2
-38
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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 }}"
|
||||
|
||||
@@ -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
@@ -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 }}
|
||||
|
||||
Reference in new issue
Block a user