digitalboard.core/roles/bookstack/README.md
Tobias Wüst 9d539d0da4 feat(bookstack): add role for self-hosted BookStack deployment
Deploy BookStack with linuxserver.io images behind Traefik, including
Entra ID OIDC SSO support and a daily backup timer.

Stack:
- lscr.io/linuxserver/bookstack:version-v26.03.3
- lscr.io/linuxserver/mariadb:11.4.9
- Traefik labels for websecure entrypoint on internal network
- Healthcheck via mariadb-admin ping (LSIO image lacks healthcheck.sh)

Features:
- Persistent APP_KEY generated on first run, stored in volume dir
- Optional OIDC SSO via Microsoft Entra ID (configurable per-instance)
- Idempotent admin user creation with DB-based existence check
- Daily systemd timer backup (DB dump + uploads tar + APP_KEY)
  with configurable retention

Implementation notes:
- DB queries use --protocol=tcp with the app user because root@localhost
  uses unix_socket auth in the LSIO MariaDB image (no password) and
  root@% does not exist
- docker_container_exec uses argv: (list) instead of command: (string)
  to avoid argument-splitting issues
- Migration-wait task ensures users table exists before admin check,
  since /login returns 200 before Laravel migrations complete
- no_log: true on all tasks that reference DB or admin passwords
- artisan absolute path (/app/www/artisan) because LSIO image WORKDIR
  is not the app directory

Adds bookstack route to DMZ Traefik service registry.
2026-05-20 17:39:16 +02:00

145 lines
5.9 KiB
Markdown

# Ansible Role: bookstack
Deploys [BookStack](https://www.bookstackapp.com/) as a self-contained Docker
Compose stack behind Traefik, with its own MariaDB container, OIDC SSO
(Entra ID by default) and a daily systemd-timer driven backup of database
and uploads.
## Requirements
- Docker Engine + Compose plugin on the target host
- Traefik already running, with the external network referenced by
`bookstack_traefik_network` (default: `proxy`)
- `community.docker` collection on the controller
- DNS for `bookstack_domain` pointing at the Traefik host
## Required variables
The role asserts these are set; the play fails fast if any is empty:
| Variable | Description |
|---|---|
| `bookstack_db_root_password` | MariaDB root password |
| `bookstack_db_password` | MariaDB user password |
| `bookstack_admin_password` | Initial local admin password |
| `bookstack_oidc_client_id` | Entra ID App Registration ID (if OIDC on) |
| `bookstack_oidc_client_secret` | Entra ID client secret (if OIDC on) |
| `bookstack_entra_tenant_id` | Entra tenant UUID (if OIDC on) |
Provide via OpenBao lookup, Ansible Vault or `--extra-vars`. Never commit
real secrets.
## Optional variables
See `defaults/main.yml`. Frequently overridden:
- `bookstack_domain`, `bookstack_base_url`
- `bookstack_image`, `bookstack_db_image` (pin in production)
- `bookstack_oidc_enabled` (set `false` to disable OIDC entirely)
- `bookstack_oidc_auto_initiate` (`true` redirects straight to IdP)
- `bookstack_oidc_user_to_groups` (`true` syncs roles from Entra groups)
- `bookstack_backup_enabled`, `bookstack_backup_schedule`,
`bookstack_backup_retention_days`
## Entra ID app registration
1. Azure Portal → Entra ID → App registrations → New registration
2. Redirect URI (Web): `https://<bookstack_domain>/oidc/callback`
3. Front-channel logout URL: `https://<bookstack_domain>/logout`
4. Certificates & secrets → New client secret →
`bookstack_oidc_client_secret`
5. For group sync (`bookstack_oidc_user_to_groups: true`):
- Token configuration → Add groups claim → Security groups
- In BookStack, create roles whose **External Auth ID** equals the
Entra group Object ID, so the mapping resolves on first login.
## What the role does
| Phase | Action |
|---|---|
| Validate | `assert` all required secrets are set |
| Prepare | install packages, create volume dirs, generate persistent `APP_KEY`, verify Traefik network |
| Deploy | render `docker-compose.yml`, pull images, bring stack up |
| Configure | wait for the app, create the initial local admin via `php artisan bookstack:create-admin` (idempotent) |
| Backup | render `/usr/local/bin/bookstack-backup.sh` + systemd timer (daily 03:00, 14-day retention) |
## Example playbook
```yaml
- name: Deploy BookStack service
hosts: bookstack_servers
become: true
roles:
- digitalboard.core.bookstack
```
With inventory variables:
```yaml
# group_vars/bookstack_servers.yml
bookstack_domain: wiki.digitalboard.ch
bookstack_base_url: "https://wiki.digitalboard.ch"
bookstack_entra_tenant_id: "{{ lookup('community.hashi_vault.vault_kv2_get',
'digitalboard/bookstack',
mount_point='kv').data.data.tenant_id }}"
bookstack_oidc_client_id: "{{ lookup('community.hashi_vault.vault_kv2_get',
'digitalboard/bookstack',
mount_point='kv').data.data.client_id }}"
bookstack_oidc_client_secret: "{{ lookup('community.hashi_vault.vault_kv2_get',
'digitalboard/bookstack',
mount_point='kv').data.data.client_secret }}"
bookstack_db_root_password: "{{ lookup('community.hashi_vault.vault_kv2_get',
'digitalboard/bookstack',
mount_point='kv').data.data.db_root_password }}"
bookstack_db_password: "{{ lookup('community.hashi_vault.vault_kv2_get',
'digitalboard/bookstack',
mount_point='kv').data.data.db_password }}"
bookstack_admin_password: "{{ lookup('community.hashi_vault.vault_kv2_get',
'digitalboard/bookstack',
mount_point='kv').data.data.admin_password }}"
```
## Backup / restore
Backups land in `{{ bookstack_backup_dir }}` (default
`/srv/data/bookstack/backup`) with three files per run:
- `bookstack-db-<stamp>.sql.gz` — mariadb-dump
- `bookstack-files-<stamp>.tar.gz` — uploads, attachments
- `bookstack-appkey-<stamp>.txt` — APP_KEY (required for restore!)
Manual trigger: `systemctl start bookstack-backup.service`
Timer status: `systemctl list-timers bookstack-backup.timer`
Restore procedure:
1. Stop the stack: `docker compose down` in `bookstack_docker_compose_dir`
2. Restore the APP_KEY: copy the `.txt` content to
`{{ bookstack_docker_volume_dir }}/.app_key` (the key MUST match or
encrypted DB values become unreadable)
3. Start only the DB container, then load the dump:
```bash
gunzip -c bookstack-db-<stamp>.sql.gz \
| docker exec -i bookstack-db \
mariadb -u root -p"<root-pw>" bookstack
```
4. Extract the files: `tar -xzf bookstack-files-<stamp>.tar.gz -C
{{ bookstack_appdata_dir }}/www/`
5. Bring the stack back up: `docker compose up -d`
## Notes
- `bookstack_oidc_auto_initiate: false` (default) shows a login page
with an SSO button alongside the local login form. With `true`, users
go straight to the IdP — the local admin then has to use
`https://<domain>/login?email_login=1`.
- `bookstack_oidc_user_to_groups: true` only makes sense once BookStack
roles with the correct **External Auth IDs** (= Entra group Object
IDs) exist; otherwise users lose their role assignment on every login.
- Image tags default to pinned versions; bump them deliberately rather
than chasing `latest`.
- BookStack officially supports MySQL/MariaDB only — no PostgreSQL.
## License
MIT