Run Wiki.js as a rootless Podman Quadlet
This commit is contained in:
1 parent
af2e7394b8
commit
c2ecde0341
13 files changed
+1006
-118
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
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
# Postgres is its own playbook (postgres/playbook.yml) and a rootless Quadlet
|
||||
# unit there. This handler runs as the connection user, so `systemctl --user`
|
||||
# works without setting XDG_RUNTIME_DIR.
|
||||
#
|
||||
# Required by postgres/roles/attach, which notifies it whenever this app's
|
||||
# drop-in changes: a new drop-in changes the generated unit, so Postgres must be
|
||||
# recreated to join the network — Quadlet has no live equivalent of
|
||||
# `podman network connect`. That is a few seconds of downtime for every attached
|
||||
# app, not just this one. See postgres/readme.md, "The trade-off".
|
||||
- name: Restart Postgres
|
||||
ansible.builtin.systemd_service:
|
||||
name: "{{ postgres_unit }}"
|
||||
scope: user
|
||||
state: restarted
|
||||
daemon_reload: true
|
||||
listen: Restart Postgres
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
# Attach this app's network to the shared Postgres unit.
|
||||
#
|
||||
# Postgres knows nothing about its clients, exactly like Caddy: a service joins
|
||||
# by dropping 50-hiwikijs.conf and the shared 10-network-aliases.conf next to
|
||||
# postgres.container. The canonical way to do that is the shared include below —
|
||||
# it writes the same bytes every service writes, asserts that hiwikijs.network
|
||||
# already exists, and notifies the "Restart Postgres" handler defined in this
|
||||
# role.
|
||||
#
|
||||
# This runs in the roles phase, which is after the register include in
|
||||
# playbook.yml's pre_tasks has created hiwikijs.network.
|
||||
|
||||
- name: Attach Wiki.js to Postgres
|
||||
ansible.builtin.include_tasks: "{{ playbook_dir }}/../postgres/roles/attach/tasks/main.yaml"
|
||||
|
||||
# On a first attach both drop-ins are new, so Postgres has to be recreated to
|
||||
# join hiwikijs_net — and the shared 10-network-aliases.conf is what creates the
|
||||
# `db` alias DB_HOST asks for. That restart is a *handler*, which by default runs
|
||||
# at the end of the play, after the wikijs role has already started the app. The
|
||||
# app would then come up before `db` resolves, fail its own HTTP probe, and fail
|
||||
# the play on a machine where nothing is actually wrong.
|
||||
#
|
||||
# Flushing applies the new drop-in before the app starts. On later runs the
|
||||
# drop-ins are unchanged, nothing is pending, and this is a no-op.
|
||||
#
|
||||
# Note that flush_handlers flushes *everything* pending, which includes the
|
||||
# "Restart Caddy" that the register include notified in pre_tasks — and that is
|
||||
# the right thing here: the Caddy drop-in is new too, and Caddy has to be
|
||||
# recreated to join the network before it can proxy to hiwikijs.
|
||||
- name: Apply the Postgres network change before starting Wiki.js
|
||||
ansible.builtin.meta: flush_handlers
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
# All handlers run as the connection user ({{ podman_user }}), so systemctl
|
||||
# --user works without setting XDG_RUNTIME_DIR.
|
||||
|
||||
- name: Reload systemd user units
|
||||
ansible.builtin.command: systemctl --user daemon-reload
|
||||
changed_when: true
|
||||
listen: Reload systemd user units
|
||||
|
||||
# A changed environment file or unit file changes the generated unit, so Wiki.js
|
||||
# is recreated. The wiki itself is in Postgres and the two data directories are
|
||||
# bind mounts, so a recreate loses nothing — but it is a few seconds of downtime
|
||||
# for the one site.
|
||||
- name: Restart Wiki.js
|
||||
ansible.builtin.systemd_service:
|
||||
name: "{{ unit_name }}"
|
||||
scope: user
|
||||
state: restarted
|
||||
daemon_reload: true
|
||||
listen: Restart Wiki.js
|
||||
@@ -0,0 +1,145 @@
|
||||
---
|
||||
# Wiki.js 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.
|
||||
|
||||
# /wiki/data/content is the image's declared VOLUME, and /wiki/data/uploads is
|
||||
# where Wiki.js's default "Local File System" storage target writes. Both are
|
||||
# empty on monoco, because the whole wiki — pages, history, users, and the 5.8 MB
|
||||
# of uploaded assets in the `assetData` table — lives in Postgres (see readme.md).
|
||||
#
|
||||
# They are created here anyway. The database is the store, but nothing the app
|
||||
# does write may end up in the container's writable layer, where no backup would
|
||||
# ever find it — that is the audit's finding #1 (docs/podman-compat-audit.md
|
||||
# §3.1), and the reason the old hiwikijs-backup.sh archived nothing.
|
||||
- name: Ensure the Wiki.js data directories exist
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
owner: "{{ podman_user }}"
|
||||
group: "{{ podman_user }}"
|
||||
mode: '0755'
|
||||
loop:
|
||||
- "{{ content_dir }}"
|
||||
- "{{ uploads_dir }}"
|
||||
|
||||
# The app's database coordinates are consumed by systemd (EnvironmentFile= in the
|
||||
# Quadlet unit), not by Ansible, so this has to be written before the unit starts.
|
||||
# Mode 0600: DB_PASS carries the Postgres password.
|
||||
- name: Write the environment file
|
||||
ansible.builtin.template:
|
||||
src: wikijs.env.j2
|
||||
dest: "{{ env_file }}"
|
||||
owner: "{{ podman_user }}"
|
||||
group: "{{ podman_user }}"
|
||||
mode: '0600'
|
||||
notify: Restart Wiki.js
|
||||
|
||||
- name: Install the Wiki.js Quadlet unit
|
||||
ansible.builtin.template:
|
||||
src: wikijs.container.j2
|
||||
dest: "{{ quadlet_dir }}/{{ container_name }}.container"
|
||||
owner: "{{ podman_user }}"
|
||||
group: "{{ podman_user }}"
|
||||
mode: '0644'
|
||||
notify:
|
||||
- Reload systemd user units
|
||||
- Restart Wiki.js
|
||||
|
||||
- name: Start Wiki.js
|
||||
block:
|
||||
- name: Start Wiki.js
|
||||
ansible.builtin.systemd_service:
|
||||
name: "{{ unit_name }}"
|
||||
scope: user
|
||||
state: started
|
||||
daemon_reload: true
|
||||
rescue:
|
||||
# systemd reports "control process exited with error code" and nothing else,
|
||||
# which is useless for the usual failures (an unattached network, an
|
||||
# unreachable database, a bad volume). Pull the real reason out.
|
||||
- name: Collect Wiki.js diagnostics
|
||||
ansible.builtin.command: "{{ item }}"
|
||||
loop:
|
||||
- "systemctl --user status {{ unit_name }} --no-pager"
|
||||
- "journalctl --user -u {{ unit_name }} --no-pager -n 40"
|
||||
- "podman logs --tail 40 {{ container_name }}"
|
||||
register: wikijs_diag
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Show Wiki.js diagnostics
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ wikijs_diag.results | map(attribute='stdout') | list }}"
|
||||
|
||||
- name: Fail
|
||||
ansible.builtin.fail:
|
||||
msg: "Wiki.js did not start — see the diagnostics above."
|
||||
|
||||
# Quadlet honours [Install] WantedBy= when it generates the unit, so
|
||||
# `systemctl --user enable` is neither needed nor possible on a generated unit.
|
||||
- name: Verify Wiki.js is running
|
||||
ansible.builtin.command: "systemctl --user is-active {{ unit_name }}"
|
||||
register: wikijs_active
|
||||
changed_when: false
|
||||
failed_when: wikijs_active.stdout != 'active'
|
||||
|
||||
# The database is the whole wiki, and it arrives over the network that
|
||||
# roles/postgres attached Postgres to. Check the name the environment file
|
||||
# actually asks for, plus the container name.
|
||||
#
|
||||
# This runs before the HTTP probe on purpose: an unresolvable database is the one
|
||||
# failure that would otherwise show up as two minutes of retries and a bare curl
|
||||
# error. It is also checked *before* the app is expected to answer, because the
|
||||
# network is attached when the container is created, not when the app is ready.
|
||||
#
|
||||
# getent is verified present in this image (/usr/bin/getent, via the musl
|
||||
# libc-utils package) and exits 2 with no output when the name does not resolve,
|
||||
# which is what the assert below keys off.
|
||||
- name: Confirm Wiki.js can resolve its database
|
||||
ansible.builtin.command: >-
|
||||
podman exec {{ container_name }} getent hosts {{ item }}
|
||||
loop:
|
||||
- "{{ db_host }}"
|
||||
- "{{ postgres_container }}"
|
||||
register: wikijs_db_lookup
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Report database resolution
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
{{ item.item }} resolves as
|
||||
{{ item.stdout | default('(no answer)') }}
|
||||
loop: "{{ wikijs_db_lookup.results }}"
|
||||
|
||||
- name: Fail if the database did not resolve
|
||||
ansible.builtin.assert:
|
||||
that: wikijs_db_lookup.results | rejectattr('rc', 'eq', 0) | list | length == 0
|
||||
fail_msg: >-
|
||||
Wiki.js cannot resolve its database ({{ db_host }} / {{ postgres_container }}).
|
||||
That means hiwikijs_net was never attached to the postgres unit — see
|
||||
roles/postgres and postgres/readme.md, "How services attach themselves".
|
||||
|
||||
# `is-active` says the container is running; it does not say the app can serve.
|
||||
# Wiki.js runs its own schema migrations against Postgres before it binds port
|
||||
# {{ container_port }}, and `Restart=always` means even a crash-looping container
|
||||
# is reported active.
|
||||
#
|
||||
# `podman healthcheck run` is not available here: the image declares no
|
||||
# HEALTHCHECK (verified — `docker inspect hiwikijs` reports null), unlike
|
||||
# Vaultwarden's. So probe HTTP directly with the image's own /usr/bin/curl; -f
|
||||
# turns the 4xx/5xx Wiki.js serves while the database is unreachable into a
|
||||
# failed retry instead of a success.
|
||||
- name: Wait for Wiki.js to serve HTTP
|
||||
ansible.builtin.command: >-
|
||||
podman exec {{ container_name }}
|
||||
curl -fsS -o /dev/null http://127.0.0.1:{{ container_port }}/
|
||||
register: wikijs_http
|
||||
until: wikijs_http.rc == 0
|
||||
retries: 24
|
||||
delay: 5
|
||||
changed_when: false
|
||||
failed_when: wikijs_http.rc != 0
|
||||
@@ -0,0 +1,62 @@
|
||||
# Wiki.js — rootless Podman Quadlet unit.
|
||||
# Managed by Ansible (wikijs/roles/wikijs). Do not edit by hand.
|
||||
#
|
||||
# This unit joins {{ network_name }}.network, which is created by
|
||||
# caddy/roles/register (the same include that attaches Caddy to it). The
|
||||
# postgres container is joined to that network by postgres/roles/attach, which is
|
||||
# what makes DB_HOST={{ db_host }} in the environment file resolve.
|
||||
|
||||
[Unit]
|
||||
Description=Wiki.js
|
||||
Documentation=https://docs.requarks.io/
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Container]
|
||||
ContainerName={{ container_name }}
|
||||
Image={{ image }}
|
||||
|
||||
# The image runs as node, uid {{ wikijs_uid }} — verified with
|
||||
# `docker exec hiwikijs id` — and /usr/local/bin/docker-entrypoint.sh is a bare
|
||||
# `exec "$@"` that chowns nothing. Mapping that uid onto {{ podman_user }} keeps
|
||||
# {{ host_directory }} owned by {{ podman_user }} on the host, so plain tar/rsync
|
||||
# backups keep working and no subuid-owned files appear.
|
||||
UserNS=keep-id:uid={{ wikijs_uid }},gid={{ wikijs_gid }}
|
||||
|
||||
# DB_TYPE/DB_HOST/DB_PORT/DB_USER/DB_PASS/DB_NAME live here rather than as
|
||||
# Environment= lines above: this unit is world-readable in ~/.config, and DB_PASS
|
||||
# carries the Postgres password. Read by systemd, not by Ansible, so the file has
|
||||
# to exist before this unit starts.
|
||||
#
|
||||
# This is the exact equivalent of the Docker-era `env_file:`. The image's
|
||||
# /wiki/config.yml contains $(DB_...) placeholders that Wiki.js resolves from the
|
||||
# process environment itself — there is no envsubst in the image and the
|
||||
# entrypoint does not generate the file — so a systemd-provided environment and a
|
||||
# Docker-provided one are interchangeable here.
|
||||
EnvironmentFile={{ env_file }}
|
||||
|
||||
# No PublishPort, on purpose: this is what the Docker-era container did too
|
||||
# (`PortBindings` is empty on monoco), and it is why Caddy, not the host, is what
|
||||
# exposes the wiki. The upstream is reachable only from the app networks that
|
||||
# joined this one.
|
||||
Network={{ network_name }}.network
|
||||
|
||||
# The image's declared VOLUME. On Docker this silently became an anonymous volume
|
||||
# under /var/lib/docker/volumes — invisible to every backup script, which is why
|
||||
# docs/podman-compat-audit.md §3.1 called it out. Here it is a real directory
|
||||
# under {{ host_directory }}.
|
||||
Volume={{ content_dir }}:/wiki/data/content
|
||||
|
||||
# Not a declared volume, but where Wiki.js's default "Local File System" storage
|
||||
# target writes uploads. Mounting it keeps those bytes out of the container's
|
||||
# writable layer. Empty today: the wiki's assets are rows in Postgres.
|
||||
Volume={{ uploads_dir }}:/wiki/data/uploads
|
||||
|
||||
[Service]
|
||||
Restart=always
|
||||
# The first start runs Wiki.js's own schema migrations against Postgres before it
|
||||
# begins answering on {{ container_port }}.
|
||||
TimeoutStartSec=120
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -0,0 +1,24 @@
|
||||
# Environment for {{ container_name }}.container.
|
||||
# Managed by Ansible (wikijs/roles/wikijs). Do not edit by hand.
|
||||
#
|
||||
# Mode 0600 on purpose: the Quadlet unit that reads this file is world-readable,
|
||||
# and DB_PASS carries the Postgres password. Note that `podman inspect` still
|
||||
# prints the resulting environment to anyone who can run podman as this user.
|
||||
#
|
||||
# These six variables are exactly the ones the Docker-era container ran with
|
||||
# (verified against `docker inspect hiwikijs`), so the already-restored `hiwikijs`
|
||||
# database and the app's own connection string agree with nothing new
|
||||
# introduced.
|
||||
#
|
||||
# The image's /wiki/config.yml resolves $(DB_...) from this environment. The
|
||||
# placeholders it also carries — $(SSL_ACTIVE), $(HA_ACTIVE), $(DB_FILEPATH),
|
||||
# $(DB_SSL), $(LETSENCRYPT_*), $(LOG_LEVEL:info) — resolve to empty or to their
|
||||
# defaults, which is exactly how the old container behaved with only these six
|
||||
# set. Adding SSL_ACTIVE or HA_ACTIVE here would change behaviour, not preserve
|
||||
# it: TLS is terminated by Caddy, and HA mode needs a second instance.
|
||||
DB_TYPE=postgres
|
||||
DB_HOST={{ db_host }}
|
||||
DB_PORT=5432
|
||||
DB_USER={{ pg_user }}
|
||||
DB_PASS={{ pg_password }}
|
||||
DB_NAME={{ pg_database }}
|
||||
Reference in new issue
Block a user