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.
145 lines
5.9 KiB
Markdown
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
|