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
@@ -1,6 +0,0 @@
|
||||
DB_TYPE=postgres
|
||||
DB_NAME=hiwikijs
|
||||
DB_USER=postgres
|
||||
DB_PASS=123456
|
||||
DB_HOST=db
|
||||
DB_PORT=5432
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+49
-64
@@ -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
|
||||
+501
-29
@@ -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/<hash>/_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.
|
||||
@@ -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 }}
|
||||
+103
-6
@@ -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_name>.network. caddy/roles/register pins the
|
||||
# Podman network name to <network_name>_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 }}
|
||||
}
|
||||
Reference in new issue
Block a user