# 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` | OIDC client ID (if OIDC on) | | `bookstack_oidc_client_secret` | OIDC client secret (if OIDC on) | When OIDC is on, the role also asserts that `bookstack_oidc_issuer` resolves to a concrete URL. For Entra ID this means setting `bookstack_entra_tenant_id` (the default issuer interpolates it; an unset tenant leaves `//v2.0` and fails the assert). For other IdPs (Authentik, Keycloak) set `bookstack_oidc_issuer` directly instead. 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_extra_domains` (extra Host-rule hostnames, e.g. an internal `*.int.*` FQDN for a DMZ reverseproxy) - `bookstack_extra_hosts` (container `/etc/hosts` overrides for split-horizon IdP access; entries as `host:ip`) - `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:///oidc/callback` 3. Front-channel logout URL: `https:///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-.sql.gz` — mariadb-dump - `bookstack-files-.tar.gz` — uploads, attachments - `bookstack-appkey-.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-.sql.gz \ | docker exec -i bookstack-db \ mariadb -u root -p"" bookstack ``` 4. Extract the files: `tar -xzf bookstack-files-.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:///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-0