Run Wiki.js as a rootless Podman Quadlet

This commit is contained in:
tanshu committed 2026-10-18 21:35:55 +05:30
1 parent af2e7394b8
commit c2ecde0341
13 files changed
+1006 -118

No files matched your search

-6
View File
@@ -1,6 +0,0 @@
DB_TYPE=postgres
DB_NAME=hiwikijs
DB_USER=postgres
DB_PASS=123456
DB_HOST=db
DB_PORT=5432
-13
View File
@@ -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
View File
@@ -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
View File
@@ -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.
+31
View File
@@ -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
+22
View File
@@ -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
+17
View File
@@ -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
+32
View File
@@ -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
+20
View File
@@ -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
+145
View File
@@ -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
View File
@@ -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 }}
}