From c2ecde034169d2395208597aed7cc2283ec55a19 Mon Sep 17 00:00:00 2001 From: Amritanshu Date: Sun, 18 Oct 2026 21:35:55 +0530 Subject: [PATCH] Run Wiki.js as a rootless Podman Quadlet --- wikijs/files/.env | 6 - wikijs/files/nginx.conf.j2 | 13 - wikijs/playbook.yml | 113 ++-- wikijs/readme.md | 530 +++++++++++++++++- wikijs/roles/caddy/handlers/main.yaml | 31 + wikijs/roles/caddy/tasks/main.yaml | 22 + wikijs/roles/postgres/handlers/main.yaml | 17 + wikijs/roles/postgres/tasks/main.yaml | 32 ++ wikijs/roles/wikijs/handlers/main.yaml | 20 + wikijs/roles/wikijs/tasks/main.yaml | 145 +++++ .../wikijs/templates/wikijs.container.j2 | 62 ++ wikijs/roles/wikijs/templates/wikijs.env.j2 | 24 + wikijs/vars/default.yml | 109 +++- 13 files changed, 1006 insertions(+), 118 deletions(-) delete mode 100644 wikijs/files/.env delete mode 100644 wikijs/files/nginx.conf.j2 create mode 100644 wikijs/roles/caddy/handlers/main.yaml create mode 100644 wikijs/roles/caddy/tasks/main.yaml create mode 100644 wikijs/roles/postgres/handlers/main.yaml create mode 100644 wikijs/roles/postgres/tasks/main.yaml create mode 100644 wikijs/roles/wikijs/handlers/main.yaml create mode 100644 wikijs/roles/wikijs/tasks/main.yaml create mode 100644 wikijs/roles/wikijs/templates/wikijs.container.j2 create mode 100644 wikijs/roles/wikijs/templates/wikijs.env.j2 diff --git a/wikijs/files/.env b/wikijs/files/.env deleted file mode 100644 index 156e47c..0000000 --- a/wikijs/files/.env +++ /dev/null @@ -1,6 +0,0 @@ -DB_TYPE=postgres -DB_NAME=hiwikijs -DB_USER=postgres -DB_PASS=123456 -DB_HOST=db -DB_PORT=5432 diff --git a/wikijs/files/nginx.conf.j2 b/wikijs/files/nginx.conf.j2 deleted file mode 100644 index 4dc51f3..0000000 --- a/wikijs/files/nginx.conf.j2 +++ /dev/null @@ -1,13 +0,0 @@ -server { - - listen 80; - server_name {{ http_host }}; - - location / { - proxy_pass http://localhost:3443; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } -} diff --git a/wikijs/playbook.yml b/wikijs/playbook.yml index 1c6fb41..b138e36 100755 --- a/wikijs/playbook.yml +++ b/wikijs/playbook.yml @@ -1,69 +1,54 @@ -################################################# -# DO Community Playbooks: Docker -################################################# --- -- hosts: all - become: true +# Wiki.js as a rootless Podman Quadlet unit, behind the shared Caddy unit +# (caddy/playbook.yml) and with its data in the shared Postgres unit +# (postgres/playbook.yml). +# +# 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 wikijs/playbook.yml +- name: Deploy Wiki.js as a rootless Podman Quadlet + hosts: emily + gather_facts: true vars_files: - vars/default.yml - tasks: - - name: Pull Wiki.js image - docker_image: - name: "{{ image_name }}" - source: pull - state: present - force_source: yes - - - name: Ensure Host Directory exists - file: - path: "/var/lib/{{ host_directory }}" - state: directory - group: www-data - owner: www-data - mode: 0655 - - - name: Upload the .env file - template: - src: "files/.env" - dest: "/var/lib/{{ host_directory }}/.env" - - - name: Create Wiki.js container - docker_container: - name: "{{ container_name }}" - image: "{{ image_name }}" - state: started - restart_policy: "unless-stopped" - env_file: "/var/lib/{{ host_directory }}/.env" - links: - - "postgres:db" - published_ports: - - "127.0.0.1:3443:3000" - - - name: Check if Nginx conf file exists - stat: path="/etc/nginx/sites-available/{{ http_conf }}" - register: status - - - name: No need to reload Nginx - debug: msg= {{ "No need to reload Nginx as sites-available entries have already been created" }} - - - name: Set Nginx conf file - when: status.stat.exists == false - template: - src: "files/nginx.conf.j2" - dest: "/etc/nginx/sites-available/{{ http_conf }}" - - - name: Enable new site - when: status.stat.exists == false - file: - src: "/etc/nginx/sites-available/{{ http_conf }}" - dest: "/etc/nginx/sites-enabled/{{ http_conf }}" - state: link - notify: Reload Nginx - - handlers: - - name: Reload Nginx - service: - name: nginx - state: reloaded + 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, and the Postgres + # attach below needs the network unit to exist as well. + # + # network_name is a play var, so the include can see it directly. + - name: Register Wiki.js's network with Caddy + ansible.builtin.include_tasks: "{{ playbook_dir }}/../caddy/roles/register/tasks/main.yaml" + roles: + # Attaches this app's network to the shared Postgres unit, which is what + # makes DB_HOST=db resolve, and owns the "Restart Postgres" handler that + # postgres/roles/attach notifies. Runs first so the database is reachable + # before the app starts. + - postgres + # The app: content and upload directories, the 0600 environment file, the + # Quadlet unit, start + verify. + - wikijs + # Appends this site's block to the shared Caddyfile and reloads Caddy. + - caddy diff --git a/wikijs/readme.md b/wikijs/readme.md index 2be90cb..5ddb75c 100644 --- a/wikijs/readme.md +++ b/wikijs/readme.md @@ -1,45 +1,517 @@ -# Gitea on Ubuntu +# Wiki.js (rootless Podman + Quadlet) -This playbook will install Gitea on an Ubuntu 19.10 machine. -A number of containers will be created with the options specified in the `vars/default.yml` variable file. +Runs [Wiki.js](https://js.wiki/) on `emily` as a **rootless Podman** container +managed by a **systemd user unit** (Quadlet), behind the shared +[rootless Caddy](../caddy/readme.md), with its data in the shared +[rootless Postgres](../postgres/readme.md). Replaces the `docker_container` +deployment on `monoco`. + +The name is split on purpose: the container, unit, Podman network and Postgres +database are all `hiwikijs`, as they have been since the Docker-era playbook, and +the Caddyfile upstream is `hiwikijs:3000`. Only the directory is `wikijs` — the +playbook's name. Renaming it would touch the Caddyfile, the network name and the +database for no gain. + +**The database is the whole wiki.** Every page, every revision, every user and +every uploaded file lives in the `hiwikijs` database. The image's declared +`/wiki/data/content` volume is **empty** on `monoco`, `/wiki/data/uploads` is +empty too, and the 49 uploaded assets are rows in the `assetData` table (5.8 MB of +the 16 MB database). So this is the one service in the fleet where "copy the old +data" is a *verification* rather than a transfer — the commands that prove it are +in [Migrating from monoco](#migrating-from-monoco). + +This corrects the expectation set by +[docs/podman-compat-audit.md](../docs/podman-compat-audit.md) §3.1 and +[docs/backup-plan.md](../docs/backup-plan.md) §7.6, which treat the anonymous +volume as Wiki.js's store. What those documents are *right* about is the bug: +`~/bin/hiwikijs-backup.sh` archives `/var/lib/hiwikijs`, which is an empty decoy, +so the wiki had no usable file backup. The `Volume=` mounts below fix the +mechanism — nothing may live in the container's writable layer — but the bytes it +was meant to capture are in Postgres, and they already moved. + +## Ordering + +Both shared services are already migrated and running on `emily`: + +1. [caddy/playbook.yml](../caddy/playbook.yml) — the reverse proxy, which owns the + Caddyfile this site appends to, supplies the `caddy*.service` handlers, and is + the one play that applies the rootless-Podman prerequisites. +2. [postgres/playbook.yml](../postgres/playbook.yml) — deployed, and its cluster + brought across from monoco. **The `hiwikijs` database is already restored** + (16 MB, one of the 12 databases in + [postgres/migrate-from-monoco.md](../postgres/migrate-from-monoco.md)). +3. This playbook. + +Unlike the other services there is no file copy to do first, and there is no +ordering constraint between this playbook and the data: the app is stateless on +disk. Deploy it and point DNS at `emily`. + +**`wiki.hinchco.in` still resolves to `monoco`.** Until the record is repointed, +Caddy on `emily` cannot obtain a certificate for the site, and the old host is +still what answers. That DNS change — not this playbook — is what performs the +cutover. + +## Layout + +``` +wikijs/ +├── playbook.yml +├── vars/default.yml +└── roles/ + ├── postgres/ # attaches this app's network to Postgres (+ its handler) + ├── wikijs/ # data dirs, the 0600 env file, the Quadlet unit, start + verify + └── caddy/ # appends this site's block to the shared Caddyfile +``` + +Quadlet units and state on the host: + +| Path | What | +|---|---| +| `~/.config/containers/systemd/hiwikijs.container` | the generated unit (Ansible-templated) | +| `~/.config/containers/systemd/hiwikijs.network` | the app network, written by `caddy/roles/register` | +| `~/.config/containers/systemd/caddy.container.d/50-hiwikijs.conf` | attaches Caddy to that network | +| `~/.config/containers/systemd/postgres.container.d/50-hiwikijs.conf` | attaches Postgres to it | +| `~/.config/containers/systemd/postgres.container.d/10-network-aliases.conf` | the shared `db` alias `DB_HOST` asks for | +| `~/podman-data/hiwikijs/content/` | the image's `/wiki/data/content` (empty in practice) | +| `~/podman-data/hiwikijs/uploads/` | `/wiki/data/uploads`, the local storage target | +| `~/podman-data/hiwikijs/hiwikijs.env` | `DB_*`, mode `0600`, read by systemd | + +Note the network-name indirection, which is the same everywhere in this repo: the +*file* is `hiwikijs.network` and the *Podman network* is `hiwikijs_net`, pinned by +`NetworkName=` in the network unit. The Docker-era container used `hiwikijs_net`, +so nothing about the network name changes. + +## What is state, and what is not + +| Where | Keep? | Why | +|---|---|---| +| Postgres `hiwikijs` | **required** | 32 tables: `pages` (80), `pageHistory`, `users` (2), `comments`, `navigation`, `settings`, and `assetData` | +| `assetData` table | **required** | 49 rows, 5.8 MB — the actual bytes of every uploaded image. A file-level backup of the wiki would miss all of them | +| `~/podman-data/hiwikijs/content/` | empty | the image's declared VOLUME. Empty on monoco, and Wiki.js writes nothing to it in this configuration | +| `~/podman-data/hiwikijs/uploads/` | empty | the default local storage target. The wiki is configured to store assets in the database instead | +| `hiwikijs.env` | regenerated | the template reproduces the Docker-era `.env` exactly | +| `/var/lib/hiwikijs` on monoco | **no** | a decoy: an empty directory holding one root-owned 90-byte `.env`, never mounted into the container | + +## How it registers with Caddy + +Caddy is the base unit and knows nothing about its upstreams. This playbook calls +the shared include in +[caddy/roles/register](../caddy/roles/register/tasks/main.yaml), which writes the +network unit **and** the drop-in that pulls it into Caddy: + +```yaml +- name: Register Wiki.js's network with Caddy + ansible.builtin.include_tasks: "{{ playbook_dir }}/../caddy/roles/register/tasks/main.yaml" +``` + +It runs in `pre_tasks`, **before** the app unit is started, because +`hiwikijs.container` references `hiwikijs.network`: Quadlet fails *unit +generation* — so `systemctl --user start` reports "unit not found" — when a +referenced `.network` file does not exist yet. The Postgres attach below depends on +it for the same reason. + +The site block itself is appended to the shared Caddyfile by +[roles/caddy](roles/caddy/tasks/main.yaml) with `blockinfile`, giving exactly the +block the Docker-era Caddyfile had: + +``` +# BEGIN Ansible managed Caddy snippet for wiki.hinchco.in +wiki.hinchco.in { + reverse_proxy hiwikijs:3000 +} +# END Ansible managed Caddy snippet for wiki.hinchco.in +``` + +`caddyfile_path` changed from `/var/lib/caddy/conf/Caddyfile` to +`~/podman-data/caddy/conf/Caddyfile`; the marker and the upstream are unchanged, so +running this playbook against the old host's Caddyfile would be a no-op. + +The old `roles/caddy` slurped the Caddyfile and compared strings before appending. +That is gone: `blockinfile` is idempotent through its marker, and `slurp` *fails* +when the file does not exist yet, which is the case on a host where the Caddy +playbook has not run. + +## How it attaches to Postgres + +Same idea at the other end: Postgres knows nothing about its clients, and this +playbook uses the shared include in +[postgres/roles/attach](../postgres/roles/attach/tasks/main.yaml) rather than +writing those drop-ins itself. That writes two files, both merged into +`postgres.container` by Quadlet: + +| File | Content | +|---|---| +| `postgres.container.d/50-hiwikijs.conf` | `Network=hiwikijs.network` | +| `postgres.container.d/10-network-aliases.conf` | `NetworkAlias=db` | + +`DB_HOST=db` is what the Docker-era container used — it was `--link postgres:db` — +and `10-network-aliases.conf` is the Quadlet equivalent. `postgres`, the container +name, resolves too, and the play checks both. + +[roles/postgres](roles/postgres/tasks/main.yaml) wraps that include for two +reasons: + +- it defines the `Restart Postgres` handler the include notifies; and +- it **flushes handlers** immediately afterwards. On a first attach the drop-in is + new, so Postgres has to be *recreated* to join `hiwikijs_net` — and 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 wrong. Flushing applies the drop-in first; on later runs nothing is pending + and it is a no-op. + +`flush_handlers` flushes *everything* pending, which here includes the +`Restart Caddy` that the register include notified in `pre_tasks`. That is correct +rather than incidental: Caddy's drop-in is new in the same run, so Caddy has to be +recreated to join the network before it can proxy to `hiwikijs`. + +Note the cost stated in [postgres/readme.md](../postgres/readme.md): a new drop-in +recreates Postgres, which is a few seconds of downtime for **every** attached app, +not just this one. ## Settings -- `create_containers`: number of containers to create. -- `default_container_name`: default name for new containers. -- `default_container_image`: default image for new containers. -- `default_container_command`: default command to run on new containers. +| Variable | Value / purpose | +|---|---| +| `podman_user` | **Derived** from the connection (`ansible_facts['user_id']`), not set by hand | +| `podman_data_dir` | `~/podman-data` | +| `quadlet_dir` | `~/.config/containers/systemd` | +| `host` | `wiki.hinchco.in` — the site block Caddy serves | +| `container_name` | `hiwikijs`, and the Quadlet unit / container name | +| `image` | `ghcr.io/requarks/wiki:latest` — registry + org + repo, so no short-name lookup | +| `network_name` | `hiwikijs` → `hiwikijs.network` / `hiwikijs_net` | +| `wikijs_uid` / `wikijs_gid` | `1000` / `1000` — the image's `node` account, mapped onto `podman_user` by `keep-id` | +| `content_dir` | `~/podman-data/hiwikijs/content` → the image's `/wiki/data/content` | +| `uploads_dir` | `~/podman-data/hiwikijs/uploads` → `/wiki/data/uploads` | +| `env_file` | `~/podman-data/hiwikijs/hiwikijs.env`, mode `0600` | +| `container_port` | `3000`, the port inside the container that Caddy proxies to | +| `postgres_container` / `postgres_unit` | The shared database unit this app attaches to | +| `pg_user` / `pg_password` / `pg_database` | Credentials, and the database name — `hiwikijs` | +| `db_host` | `db`, the shared `NetworkAlias` | +| `caddy_name` / `caddyfile_path` | The shared Caddy unit and its Caddyfile | +The three `podman_*` / `quadlet_dir` vars are **duplicated** from +[caddy/vars/default.yml](../caddy/vars/default.yml), as in every service +playbook. That is a deliberate trade: a play cannot include the Caddy vars file +wholesale, because it also sets `host_directory` (to the *Caddy* data directory) +and every service defines that for itself. Keep them in sync. -## Running this Playbook +Like Caddy, `podman_user` is derived from the connection and the play carries no +`become`. Because the podman tasks are not run here, there is **no guard against +connecting as `root`**; the prerequisites, including that check, live in +`caddy/playbook.yml`. -Quick Steps: +## `keep-id:uid=1000`, not `uid=0` -### 1. Obtain the playbook -```shell -git clone https://github.com/do-community/ansible-playbooks.git -cd ansible-playbooks/docker_ubuntu1804 +Wiki.js is the odd one out among the converted services. Vaultwarden, Tandoor, +Caddy and Postgres all run as container-root and use `keep-id:uid=0,gid=0`. This +image sets `USER node`, and `node` is uid/gid **1000**: + +``` +docker exec hiwikijs id → uid=1000(node) gid=1000(node) ``` -### 2. Customize Options +`/usr/local/bin/docker-entrypoint.sh` is a bare `exec "$@"` — it chowns nothing, so +whatever uid the image starts as is the uid that writes the bind mounts. Mapping +uid 1000 onto `podman_user` is what keeps `~/podman-data/hiwikijs` owned by +`tanshu` and leaves no subuid-owned files to trip over backups +([docs/podman-migration.md](../docs/podman-migration.md) §4.2 lists `hiwikijs` +under exactly this mapping). -```shell -nano vars/default.yml -``` +`tanshu` is uid **1001**, not 1000 — +[docs/podman-migration.md](../docs/podman-migration.md) §4.1 is about precisely +this trap, and most uid-1000 data on monoco belongs to `ubuntu`. Nothing in this +playbook assumes the app can read a file just because `tanshu` owns it; the two +data directories are created as `tanshu` and the container writes as 1000, which +`keep-id` maps onto `tanshu`. The mapping is what makes that true, not the +ownership. -```yml -#vars/default.yml ---- -create_containers: 4 -default_container_name: docker -default_container_image: ubuntu -default_container_command: sleep 1d -``` - -### 3. Run the Playbook +## Running ```command -ansible-playbook -l [target] -i [inventory file] -u [remote user] playbook.yml +ansible-playbook -u tanshu wikijs/playbook.yml ``` -For more information on how to run this Ansible setup, please check this guide: [How to Use Ansible to Install and Set Up Docker on Ubuntu 18.04](https://www.digitalocean.com/community/tutorials/how-to-use-ansible-to-install-and-set-up-docker-on-ubuntu-18-04). +The play targets `emily` directly, so the inventory must contain it — the one in +`prepare-server/` does, and its `ansible.cfg` names it. + +This play applies **only** this app's units and data. Its `pre_tasks` run Caddy's +`register` include and nothing else. The rootless-Podman prerequisites — +installing podman, `loginctl enable-linger`, the low-port sysctl and the Quadlet +directory — are the shared task file at +[caddy/roles/podman/tasks/main.yaml](../caddy/roles/podman/tasks/main.yaml), and are +**not** run here. A host that has already run `caddy/playbook.yml` has them. + +`systemctl --user enable hiwikijs` is neither needed nor possible: the unit is +*generated*, and Quadlet applies `[Install] WantedBy=default.target` itself. + +## Migrating from monoco + +**There is nothing to copy.** The database already moved on 2026-10-15, and the +app has no file state. What follows is how to *confirm* that, and what to do in the +unlikely case the confirmation fails. + +The commands are meant to be run from the machine that holds the `monoco` and +`emily` SSH aliases. `rsync` cannot copy remote-to-remote, so anything that does +move is streamed through a pipe. + +### 1. Prove the file state is empty + +The image declares `VOLUME /wiki/data/content`, which Docker turned into an +anonymous volume under `/var/lib/docker/volumes//_data`. The running +container already has it mounted, so no hash and no `sudo` are needed to look: + +```bash +ssh monoco 'bash -s' <<'EOF' +docker exec hiwikijs sh -c ' + echo "--- /wiki/data/content (the image VOLUME) ---"; ls -la /wiki/data/content + echo "--- /wiki/data/uploads (local storage target) ---"; ls -la /wiki/data/uploads + echo "--- sizes ---"; du -sh /wiki/data/content /wiki/data/uploads' +EOF +``` + +Expected — both listings show only `.` and `..`, and both `du` lines say `4.0K`: + +``` +--- /wiki/data/content (the image VOLUME) --- +total 8 +drwxr-xr-x 2 node node 4096 Mar 24 2025 . +drwxr-xr-x 1 node node 4096 Jul 14 2025 .. +--- /wiki/data/uploads (local storage target) --- +total 8 +drwxr-xr-x 2 node node 4096 Jul 14 2025 . +drwxr-xr-x 1 node node 4096 Jul 14 2025 .. +--- sizes --- +4.0K /wiki/data/content +4.0K /wiki/data/uploads +``` + +`/var/lib/hiwikijs` on monoco is a decoy and is worth looking at once so nobody +copies it later. It holds a single root-owned `.env` and is **not** mounted into +the container — the container's environment came from `env_file:` at create time, +which is the Docker-side counterpart of the Quadlet unit's `EnvironmentFile=`, and +which the role's template reproduces: + +```bash +ssh monoco 'ls -la /var/lib/hiwikijs; cat /var/lib/hiwikijs/.env' +``` + +### 2. Prove the data is in the database, and already on emily + +This is the real check, and it is the whole migration. Run the same block on both +hosts and compare — the only difference is `docker exec` against `podman exec`: + +```bash +ssh monoco 'bash -s' <<'EOF' +EXEC="docker exec -e PGPASSWORD=123456 postgres" +$EXEC psql -U postgres -d hiwikijs -tAc 'select count(*) from pages' | sed 's/^/pages : /' +$EXEC psql -U postgres -d hiwikijs -tAc 'select count(*) from users' | sed 's/^/users : /' +$EXEC psql -U postgres -d hiwikijs -tAc 'select count(*), pg_size_pretty(sum(octet_length(data))::bigint) from "assetData"' | sed 's/^/assets : /' +$EXEC psql -U postgres -tAc "select pg_size_pretty(pg_database_size('hiwikijs'))" | sed 's/^/size : /' +echo "newest edit: $($EXEC psql -U postgres -d hiwikijs -tAc 'select max("updatedAt") from pages')" +EOF + +ssh emily 'bash -s' <<'EOF' +EXEC="podman exec -e PGPASSWORD=123456 postgres" +$EXEC psql -U postgres -d hiwikijs -tAc 'select count(*) from pages' | sed 's/^/pages : /' +$EXEC psql -U postgres -d hiwikijs -tAc 'select count(*) from users' | sed 's/^/users : /' +$EXEC psql -U postgres -d hiwikijs -tAc 'select count(*), pg_size_pretty(sum(octet_length(data))::bigint) from "assetData"' | sed 's/^/assets : /' +$EXEC psql -U postgres -tAc "select pg_size_pretty(pg_database_size('hiwikijs'))" | sed 's/^/size : /' +echo "newest edit: $($EXEC psql -U postgres -d hiwikijs -tAc 'select max("updatedAt") from pages')" +EOF +``` + +The `assetData` identifier is quoted because the table is genuinely camelCase — +created `"assetData"` by Wiki.js's migrations, so unquoted `assetdata` does not +find it. + +Both hosts print the same thing, which is what makes this a verified copy rather +than an assumption: + +``` +pages : 80 +users : 2 +assets : 49|5979 kB +size : 16 MB +newest edit: 2024-02-03T03:23:57.645Z +``` + +**The `newest edit` line is the one that matters for cutover.** The dump was taken +on 2026-10-15; if the newest `updatedAt` on monoco is *later* than on emily, the +wiki was edited after the dump and those revisions exist only on the old host. On +this wiki it is not: the newest revision is from **February 2024**, identical on +both hosts, so the copy is current and the wiki is effectively read-only. + +### 3. If the numbers differ: refresh the database + +Only needed if step 2 shows drift (a later `updatedAt` on monoco, or different +counts). Stop both apps first — nothing may write while the dump is taken, and +`drop database` needs the connections gone: + +```bash +ssh monoco 'docker stop hiwikijs' +ssh emily 'systemctl --user stop hiwikijs.service' + +# Dump just this database, stream it across, and keep a copy on emily. +set -o pipefail +ssh monoco 'docker exec -e PGPASSWORD=123456 postgres pg_dump -U postgres -Fc hiwikijs' \ + | ssh emily 'cat > ~/hiwikijs-refresh.dump' + +# Confirm the archive is readable before dropping anything. +ssh emily 'podman exec -i -e PGPASSWORD=123456 postgres pg_restore --list < ~/hiwikijs-refresh.dump | head -5' + +ssh emily 'bash -s' <<'EOF' +set -e +EXEC="podman exec -e PGPASSWORD=123456 postgres" +$EXEC psql -U postgres -d postgres -v ON_ERROR_STOP=1 -c 'drop database if exists hiwikijs' +$EXEC psql -U postgres -d postgres -v ON_ERROR_STOP=1 -c 'create database hiwikijs owner postgres' +podman exec -i -e PGPASSWORD=123456 postgres pg_restore --exit-on-error -U postgres -d hiwikijs < ~/hiwikijs-refresh.dump +EOF + +# Re-run step 2 — the counts must now match. +``` + +`-Fc` plus `pg_restore` rather than piping SQL into `psql`: it is the form +[postgres/readme.md](../postgres/readme.md) recommends for per-database work, +`--exit-on-error` stops on the first problem instead of half-loading, and +`pg_restore --list` catches a truncated transfer before the old database is +dropped. + +**Do not re-run [postgres/migrate-from-monoco.md](../postgres/migrate-from-monoco.md).** +That is a whole-cluster `pg_dumpall` into an *empty* cluster; against the live one +it fails on the first `CREATE DATABASE`, and its "start over" block destroys +`~/podman-data/postgres/18`. + +### 4. Repoint DNS, then stop the old container + +The record, not the playbook, is the cutover: + +```bash +# Before: monoco. After the change: emily. +dig +short wiki.hinchco.in +# 13.202.6.2 ← monoco + +# Change the A/AAAA record to emily's address, then confirm it has propagated +# before Caddy on emily can complete an ACME challenge for the site. +dig +short wiki.hinchco.in +# 35.154.14.59 ← emily +``` + +Then retire the old instance so the two copies cannot diverge: + +```bash +ssh monoco 'docker stop hiwikijs' +``` + +Rolling back means rolling back both halves, and the DNS change is the deciding +one: `docker start hiwikijs` on monoco only produces a working site once the record +points back at `13.202.6.2`, and monoco's own Postgres 17 cluster has to still be +intact for the wiki to have any content. That cluster is deliberately left alone as +the rollback — do not drop databases on it, exactly as +[postgres/migrate-from-monoco.md](../postgres/migrate-from-monoco.md) §7 says. + +Two loose ends when the old host is retired: + +- **Remove the `wiki.hinchco.in` block from monoco's Caddyfile.** Monoco's Caddy + keeps serving other sites, and once DNS points away it can no longer answer the + ACME challenge for this one — which shows up as renewal errors in + `docker logs caddy` forever. It does not affect the other sites. +- **`~/bin/hiwikijs-backup.sh` is not a backup.** It archives `/var/lib/hiwikijs`, + the empty decoy. The database is the wiki; anything scheduled belongs in a + **systemd user timer** doing the `podman exec … pg_dump` form from + [postgres/readme.md](../postgres/readme.md), "Backups". + +### 5. Deploy + +```bash +cd prepare-server && ansible-playbook -u tanshu ../wikijs/playbook.yml +``` + +## Verification + +```bash +systemctl --user status hiwikijs +journalctl --user -u hiwikijs -f +podman logs hiwikijs + +# What the playbook checks for you: +podman exec hiwikijs curl -fsS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3000/ +podman exec hiwikijs getent hosts db + +# No anonymous volume was created: the mount is the real directory. +podman inspect hiwikijs --format '{{range .Mounts}}{{.Type}} {{.Source}} -> {{.Destination}}{{println}}{{end}}' +# expect: bind /home/tanshu/podman-data/hiwikijs/content -> /wiki/data/content +# bind /home/tanshu/podman-data/hiwikijs/uploads -> /wiki/data/uploads + +# Nothing is published host-side, exactly like the Docker-era container. Podman +# lists the image's EXPOSE'd ports with a null host binding, which is Docker's +# `PortBindings: {}` — the port numbers appear, the host side does not. +podman inspect hiwikijs --format '{{json .NetworkSettings.Ports}}' +# expect: {"3000/tcp":null,"3443/tcp":null} — null means "not published" + +# Nothing may end up owned by root or by a subuid. +find ~/podman-data/hiwikijs ! -user tanshu # empty = correct + +# The site, end to end (after DNS points here). +curl -sSI https://wiki.hinchco.in +``` + +Then log in **in a browser**, open a page, and confirm an **image** renders. The +image is the check that matters: it is served from the `assetData` table, so it is +the one that proves the database came across whole. A `curl` of `/` cannot make +that check, and neither can a table count. + +Also reboot the host and confirm the unit returns unattended — without +`loginctl enable-linger tanshu` it will not, and the failure is silent. + +## Notes and gotchas + +- **`wiki.tanshu.com` is not this service.** The Caddyfile on monoco has a + `wiki.tanshu.com` block pointing at `affine:3010`; that comes from + [affine/vars/default.yml](../affine/vars/default.yml), which uses the hostname + for a different app. This playbook owns `wiki.hinchco.in` only. The two are + unrelated, and the similar name is worth knowing before "fixing" either. +- **There is no `HEALTHCHECK` in the image.** `docker inspect hiwikijs` reports + `.Config.Healthcheck` as `null`, so the `podman healthcheck run` form the + Vaultwarden role uses is unavailable here. The play probes HTTP with the image's + own `curl` instead, which is the next best thing: it says the app answers, not + merely that the process exists. +- **`getent` is there, despite the Alpine base.** The image ships + `/usr/bin/getent` (musl's `libc-utils`), so the database-resolution check the + other roles use works unchanged. It exits `2` with no output when a name does + not resolve, which is what the play's assert keys off — worth knowing, because + a bare `getent` failure is silent. +- **`latest` floats.** Neither the old playbook nor this one pins a version or a + digest, so an image bump is a normal `podman pull` away and may run new schema + migrations against the 16 MB database on first start. Take a `pg_dump` first when + that matters. +- **The 49 assets have no files.** `assetData` holds their bytes, so the wiki is + self-contained — but this is why `uploads/` is empty, and why a filesystem-level + backup of this service would produce an archive that restores to a wiki with no + images. Back up the database. +- **Client IPs.** The app sees Caddy's address on the app network, not the + visitor's. Whether the original address would survive the rootless port forward + is moot here: Caddy terminates the connection and proxies over `hiwikijs_net` + either way. Nothing in Wiki.js uses the client address for security. +- **Registering restarts all of Caddy**, and attaching restarts Postgres for + everyone — a brief restart of every hosted site, and a few seconds of downtime + for every attached app. See the trade-offs in + [caddy/readme.md](../caddy/readme.md) and [postgres/readme.md](../postgres/readme.md). +- **If the Caddyfile does not exist yet**, this playbook creates it containing only + the site block. That is a valid Caddyfile; the Caddy playbook's seeded + global-options block is then not added, because its seed only fires on a missing + or zero-length file. Deploy Caddy first on a fresh host. +- **The old `/var/lib/hiwikijs` directory is not migrated** and should not be: it + holds one `.env` whose contents the role's template reproduces. Nothing in the + Docker-era container ever mounted it. + +## Not done here + +- Rotating the shared `123456` Postgres password — it lives in `hiwikijs.env` at + mode `0600` rather than in the world-readable unit file, but it is still the + fleet-wide placeholder committed in this repo. +- Replacing `~/bin/hiwikijs-backup.sh` with a systemd user timer that dumps + `hiwikijs`, and removing the entry from root's crontab — root cannot + `podman exec` into another user's rootless containers. +- Upgrading Wiki.js to 3.x, which is a separate migration with its own schema + changes and not something an image tag bump should do unattended. diff --git a/wikijs/roles/caddy/handlers/main.yaml b/wikijs/roles/caddy/handlers/main.yaml new file mode 100644 index 0000000..d12aa38 --- /dev/null +++ b/wikijs/roles/caddy/handlers/main.yaml @@ -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 diff --git a/wikijs/roles/caddy/tasks/main.yaml b/wikijs/roles/caddy/tasks/main.yaml new file mode 100644 index 0000000..ffbe0d8 --- /dev/null +++ b/wikijs/roles/caddy/tasks/main.yaml @@ -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 diff --git a/wikijs/roles/postgres/handlers/main.yaml b/wikijs/roles/postgres/handlers/main.yaml new file mode 100644 index 0000000..31a5d44 --- /dev/null +++ b/wikijs/roles/postgres/handlers/main.yaml @@ -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 diff --git a/wikijs/roles/postgres/tasks/main.yaml b/wikijs/roles/postgres/tasks/main.yaml new file mode 100644 index 0000000..5ffda36 --- /dev/null +++ b/wikijs/roles/postgres/tasks/main.yaml @@ -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 diff --git a/wikijs/roles/wikijs/handlers/main.yaml b/wikijs/roles/wikijs/handlers/main.yaml new file mode 100644 index 0000000..327de54 --- /dev/null +++ b/wikijs/roles/wikijs/handlers/main.yaml @@ -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 diff --git a/wikijs/roles/wikijs/tasks/main.yaml b/wikijs/roles/wikijs/tasks/main.yaml new file mode 100644 index 0000000..770d5b0 --- /dev/null +++ b/wikijs/roles/wikijs/tasks/main.yaml @@ -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 diff --git a/wikijs/roles/wikijs/templates/wikijs.container.j2 b/wikijs/roles/wikijs/templates/wikijs.container.j2 new file mode 100644 index 0000000..6357441 --- /dev/null +++ b/wikijs/roles/wikijs/templates/wikijs.container.j2 @@ -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 diff --git a/wikijs/roles/wikijs/templates/wikijs.env.j2 b/wikijs/roles/wikijs/templates/wikijs.env.j2 new file mode 100644 index 0000000..eeb1973 --- /dev/null +++ b/wikijs/roles/wikijs/templates/wikijs.env.j2 @@ -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 }} diff --git a/wikijs/vars/default.yml b/wikijs/vars/default.yml index fadd349..961ff7a 100644 --- a/wikijs/vars/default.yml +++ b/wikijs/vars/default.yml @@ -1,9 +1,106 @@ --- -http_host: "wiki.hinchco.in" -http_conf: "wiki.hinchco.in.conf" -http_url: "https://wiki.hinchco.in" +# --------------------------------------------------------------------------- +# 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: Wiki.js 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). +podman_user: "{{ ansible_facts['user_id'] }}" +podman_data_dir: "{{ ansible_facts['user_dir'] }}/podman-data" +quadlet_dir: "{{ ansible_facts['user_dir'] }}/.config/containers/systemd" -container_name: wikijs -image_name: ghcr.io/requarks/wiki:latest +# --------------------------------------------------------------------------- +# Wiki.js +# --------------------------------------------------------------------------- +# The service keeps the Docker-era name everywhere — container, unit, Podman +# network and Postgres database are all `hiwikijs`, and the Caddyfile upstream is +# `hiwikijs:3000`. Only the directory is `wikijs`, because that is the playbook's +# name. Renaming the container would touch the Caddyfile, the network name and +# the database for no gain. +host: wiki.hinchco.in -host_directory: hiwikijs +container_name: hiwikijs +unit_name: "{{ container_name }}.service" + +# Fully qualified on purpose: registry + org + repo, so rootless Podman never +# consults unqualified-search-registries. There is no TTY to answer a short-name +# prompt when the systemd unit starts. +image: ghcr.io/requarks/wiki:latest + +# The Quadlet file is .network. caddy/roles/register pins the +# Podman network name to _net (so: hiwikijs.network, +# hiwikijs_net — the name the Docker-era container used). +network_name: "{{ container_name }}" + +host_directory: "{{ podman_data_dir }}/{{ container_name }}" + +# The uid the app actually runs as: the image's `USER node`, and `node` is uid +# 1000 there (verified: `docker exec hiwikijs id` → uid=1000(node) gid=1000). +# keep-id maps that uid onto podman_user, so anything written into the mounts +# below stays owned by podman_user on the host and never by a subuid. +# +# Note this is *not* root: unlike bitwarden and tandoor (uid 0), Wiki.js runs +# unprivileged, so the mapping goes through 1000. `tanshu` is uid 1001 — "the +# data is already mine" is false, which is the trap in +# docs/podman-migration.md §4.1. +wikijs_uid: 1000 +wikijs_gid: 1000 + +# The image declares VOLUME /wiki/data/content. On Docker that silently became an +# *anonymous* volume under /var/lib/docker/volumes, which is why nothing under +# /var/lib ever held the wiki and why the old backup script archived an empty +# directory (docs/podman-compat-audit.md §3.1). Binding it to a real path is the +# fix. It is empty on monoco today — see readme.md. +content_dir: "{{ host_directory }}/content" + +# Not a declared volume, but it is where Wiki.js's default "Local File System" +# storage target writes uploads. Empty on monoco today — the wiki's 49 uploaded +# assets are rows in Postgres (`assetData`, 5.8 MB of the 16 MB database) — so +# this is insurance: if the storage target is ever switched to local files, the +# bytes land in a directory that gets backed up instead of in the container's +# writable layer, where nothing would ever find them. +uploads_dir: "{{ host_directory }}/uploads" + +# DB_PASS carries the Postgres password and the Quadlet unit is world-readable in +# ~/.config, so it goes in this 0600 file instead and is read by systemd through +# EnvironmentFile=. +env_file: "{{ host_directory }}/{{ container_name }}.env" + +# From the image's own config.yml (`port: 3000`, `bindIP: 0.0.0.0`). Caddy +# proxies to it over the app network; nothing is published host-side. +container_port: 3000 + +# The shared Postgres unit this app attaches itself to (postgres/playbook.yml). +# roles/postgres owns this app's drop-ins there and the "Restart Postgres" +# handler that applies them. +postgres_container: postgres +postgres_unit: "{{ postgres_container }}.service" + +# The credentials the Docker-era playbook embedded in its .env file, kept as-is +# so the already-restored `hiwikijs` database and the app's own connection string +# agree. Postgres is authoritative for this service; nothing here is new. +pg_user: postgres +pg_password: '123456' +pg_database: hiwikijs + +# `db`, not `postgres`: that is literally what the Docker-era container ran with +# (it was `--link postgres:db`), and postgres/roles/attach writes the shared +# NetworkAlias=db drop-in that makes it resolve on hiwikijs_net. The container +# name resolves too, and the play checks both. +db_host: db + +# 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" + +snippet_block: | + {{ host }} { + reverse_proxy {{ container_name }}:{{ container_port }} + }