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.
5.9 KiB
5.9 KiB
Ansible Role: bookstack
Deploys BookStack 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.dockercollection on the controller- DNS for
bookstack_domainpointing 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_urlbookstack_image,bookstack_db_image(pin in production)bookstack_oidc_enabled(setfalseto disable OIDC entirely)bookstack_oidc_auto_initiate(trueredirects straight to IdP)bookstack_oidc_user_to_groups(truesyncs roles from Entra groups)bookstack_backup_enabled,bookstack_backup_schedule,bookstack_backup_retention_days
Entra ID app registration
- Azure Portal → Entra ID → App registrations → New registration
- Redirect URI (Web):
https://<bookstack_domain>/oidc/callback - Front-channel logout URL:
https://<bookstack_domain>/logout - Certificates & secrets → New client secret →
bookstack_oidc_client_secret - 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
- name: Deploy BookStack service
hosts: bookstack_servers
become: true
roles:
- digitalboard.core.bookstack
With inventory variables:
# 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-dumpbookstack-files-<stamp>.tar.gz— uploads, attachmentsbookstack-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:
- Stop the stack:
docker compose downinbookstack_docker_compose_dir - Restore the APP_KEY: copy the
.txtcontent to{{ bookstack_docker_volume_dir }}/.app_key(the key MUST match or encrypted DB values become unreadable) - Start only the DB container, then load the dump:
gunzip -c bookstack-db-<stamp>.sql.gz \ | docker exec -i bookstack-db \ mariadb -u root -p"<root-pw>" bookstack - Extract the files:
tar -xzf bookstack-files-<stamp>.tar.gz -C {{ bookstack_appdata_dir }}/www/ - 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. Withtrue, users go straight to the IdP — the local admin then has to usehttps://<domain>/login?email_login=1.bookstack_oidc_user_to_groups: trueonly 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