Bundle of cross-role changes for the gymb services deployment: - Traefik routers: OR-combine opnform/homarr/bookstack Host rules with new *_extra_domains (internal *.int.* FQDNs for a DMZ reverseproxy), and emit tls.certresolver only when traefik_cert_mode == acme (drawio, homarr, opnform, send). - Split-horizon: bookstack_extra_hosts / opnform_extra_hosts add container /etc/hosts overrides so containers reach the IdP public FQDN over the LAN. - bookstack: assert the OIDC issuer resolves concretely (reject "//v2.0"), allowing non-Entra IdPs that override bookstack_oidc_issuer. - homarr: derive the bcrypt salt from the password digest so the admin hash is idempotent — no spurious template changes / container restarts. - opnform: PATCH an existing OIDC connection instead of skipping (applies corrected inventory on re-run); add OIDC_FORCE_LOGIN (enabled only after bootstrap) and an optional direct-SSO ingress entrypoint. Docs: READMEs and meta/argument_specs.yml updated for all new variables.
6.4 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 |
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_urlbookstack_extra_domains(extra Host-rule hostnames, e.g. an internal*.int.*FQDN for a DMZ reverseproxy)bookstack_extra_hosts(container/etc/hostsoverrides for split-horizon IdP access; entries ashost:ip)bookstack_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