Compare commits
3 commits
2c2dbbc648
...
5e53ff3e28
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e53ff3e28 | ||
|
|
1dbeece5f0 | ||
| 9d539d0da4 |
17 changed files with 858 additions and 2 deletions
|
|
@ -19,6 +19,7 @@ readme: README.md
|
||||||
authors:
|
authors:
|
||||||
- Bert-Jan Fikse <bert-jan@whatwedo.ch>
|
- Bert-Jan Fikse <bert-jan@whatwedo.ch>
|
||||||
- Tobias Wüst <tobias.wuest@wksbern.ch>
|
- Tobias Wüst <tobias.wuest@wksbern.ch>
|
||||||
|
- Simon Bärlocher <simon@whatwedo.ch>
|
||||||
|
|
||||||
### OPTIONAL but strongly recommended
|
### OPTIONAL but strongly recommended
|
||||||
# A short summary description of the collection
|
# A short summary description of the collection
|
||||||
|
|
|
||||||
145
roles/bookstack/README.md
Normal file
145
roles/bookstack/README.md
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
# 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
|
||||||
85
roles/bookstack/defaults/main.yml
Normal file
85
roles/bookstack/defaults/main.yml
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
#SPDX-License-Identifier: MIT-0
|
||||||
|
---
|
||||||
|
# defaults file for bookstack
|
||||||
|
|
||||||
|
# Base directory configuration (inherited from base role or defined here)
|
||||||
|
docker_compose_base_dir: /etc/docker/compose
|
||||||
|
docker_volume_base_dir: /srv/data
|
||||||
|
|
||||||
|
# bookstack-specific configuration
|
||||||
|
bookstack_service_name: bookstack
|
||||||
|
bookstack_docker_compose_dir: "{{ docker_compose_base_dir }}/{{ bookstack_service_name }}"
|
||||||
|
bookstack_docker_volume_dir: "{{ docker_volume_base_dir }}/{{ bookstack_service_name }}"
|
||||||
|
bookstack_appdata_dir: "{{ bookstack_docker_volume_dir }}/appdata"
|
||||||
|
bookstack_db_data_dir: "{{ bookstack_docker_volume_dir }}/db"
|
||||||
|
bookstack_backup_dir: "{{ bookstack_docker_volume_dir }}/backup"
|
||||||
|
|
||||||
|
# Service configuration
|
||||||
|
bookstack_domain: "wiki.local.test"
|
||||||
|
bookstack_base_url: "https://{{ bookstack_domain }}"
|
||||||
|
|
||||||
|
# Images — pin via inventory in production
|
||||||
|
bookstack_image: "lscr.io/linuxserver/bookstack:version-v26.03.3"
|
||||||
|
bookstack_db_image: "lscr.io/linuxserver/mariadb:11.4.9"
|
||||||
|
|
||||||
|
# Traefik configuration
|
||||||
|
bookstack_traefik_network: "proxy"
|
||||||
|
bookstack_traefik_certresolver: "le"
|
||||||
|
|
||||||
|
# Timezone / UID
|
||||||
|
bookstack_tz: "Europe/Zurich"
|
||||||
|
bookstack_puid: "1000"
|
||||||
|
bookstack_pgid: "1000"
|
||||||
|
|
||||||
|
# Database configuration
|
||||||
|
bookstack_db_name: "bookstack"
|
||||||
|
bookstack_db_user: "bookstack"
|
||||||
|
|
||||||
|
# REQUIRED SECRETS — empty defaults force `assert` to fail until set.
|
||||||
|
# Provide via OpenBao lookup, Ansible Vault, or extra-vars.
|
||||||
|
# Never commit real secrets to version control.
|
||||||
|
#
|
||||||
|
# Generate with:
|
||||||
|
# bookstack_db_root_password: openssl rand -base64 32 | tr -d '/+='
|
||||||
|
# bookstack_db_password: openssl rand -base64 32 | tr -d '/+='
|
||||||
|
# bookstack_admin_password: openssl rand -base64 24 | tr -d '/+='
|
||||||
|
bookstack_db_root_password: ""
|
||||||
|
bookstack_db_password: ""
|
||||||
|
bookstack_admin_password: ""
|
||||||
|
bookstack_oidc_client_secret: ""
|
||||||
|
|
||||||
|
# APP_KEY is generated automatically on first run and persisted on the host.
|
||||||
|
# Set explicitly only if restoring an existing instance.
|
||||||
|
bookstack_app_key: ""
|
||||||
|
|
||||||
|
# Initial local admin (fallback account, lives alongside OIDC)
|
||||||
|
bookstack_admin_name: "Admin"
|
||||||
|
bookstack_admin_email: "admin@local.test"
|
||||||
|
bookstack_artisan_path: "/app/www/artisan"
|
||||||
|
|
||||||
|
# Mail configuration
|
||||||
|
bookstack_mail_driver: "smtp"
|
||||||
|
bookstack_mail_host: "smtp.local.test"
|
||||||
|
bookstack_mail_port: 587
|
||||||
|
bookstack_mail_encryption: "tls"
|
||||||
|
bookstack_mail_from: "bookstack@local.test"
|
||||||
|
bookstack_mail_from_name: "BookStack"
|
||||||
|
bookstack_mail_username: ""
|
||||||
|
bookstack_mail_password: ""
|
||||||
|
|
||||||
|
# OIDC configuration (Entra ID by default; override `bookstack_oidc_issuer`
|
||||||
|
# for Keycloak or any other provider)
|
||||||
|
bookstack_oidc_enabled: false
|
||||||
|
bookstack_oidc_name: "SSO"
|
||||||
|
bookstack_entra_tenant_id: ""
|
||||||
|
bookstack_oidc_issuer: "https://login.microsoftonline.com/{{ bookstack_entra_tenant_id }}/v2.0"
|
||||||
|
bookstack_oidc_client_id: ""
|
||||||
|
bookstack_oidc_auto_initiate: false
|
||||||
|
bookstack_oidc_user_to_groups: false
|
||||||
|
bookstack_oidc_groups_claim: "groups"
|
||||||
|
bookstack_oidc_additional_scopes: "openid profile email"
|
||||||
|
|
||||||
|
# Backup configuration
|
||||||
|
bookstack_backup_enabled: true
|
||||||
|
bookstack_backup_retention_days: 14
|
||||||
|
bookstack_backup_schedule: "*-*-* 03:00:00"
|
||||||
19
roles/bookstack/handlers/main.yml
Normal file
19
roles/bookstack/handlers/main.yml
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
#SPDX-License-Identifier: MIT-0
|
||||||
|
---
|
||||||
|
# handlers file for bookstack
|
||||||
|
|
||||||
|
- name: stop bookstack
|
||||||
|
community.docker.docker_compose_v2:
|
||||||
|
project_src: "{{ bookstack_docker_compose_dir }}"
|
||||||
|
state: stopped
|
||||||
|
listen: restart bookstack
|
||||||
|
|
||||||
|
- name: start bookstack
|
||||||
|
community.docker.docker_compose_v2:
|
||||||
|
project_src: "{{ bookstack_docker_compose_dir }}"
|
||||||
|
state: present
|
||||||
|
listen: restart bookstack
|
||||||
|
|
||||||
|
- name: reload systemd
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
daemon_reload: true
|
||||||
194
roles/bookstack/meta/argument_specs.yml
Normal file
194
roles/bookstack/meta/argument_specs.yml
Normal file
|
|
@ -0,0 +1,194 @@
|
||||||
|
---
|
||||||
|
argument_specs:
|
||||||
|
main:
|
||||||
|
short_description: Deploy BookStack (LSIO image + MariaDB) via Docker Compose.
|
||||||
|
description:
|
||||||
|
- Renders a Compose stack for the linuxserver.io BookStack image
|
||||||
|
with a sibling MariaDB container behind Traefik, then bootstraps
|
||||||
|
the initial admin user via C(php artisan bookstack:create-admin)
|
||||||
|
and optionally enables OIDC SSO (Entra ID by default).
|
||||||
|
- "Persists the Laravel C(APP_KEY) on the host so the same key is
|
||||||
|
re-used across deploys (a fresh key would orphan all encrypted
|
||||||
|
database values: 2FA secrets, API tokens, OIDC client_secret)."
|
||||||
|
- Ships an optional systemd timer that backs up the database dump,
|
||||||
|
uploads tarball and APP_KEY daily with configurable retention.
|
||||||
|
options:
|
||||||
|
docker_compose_base_dir:
|
||||||
|
type: path
|
||||||
|
default: /etc/docker/compose
|
||||||
|
docker_volume_base_dir:
|
||||||
|
type: path
|
||||||
|
default: /srv/data
|
||||||
|
bookstack_service_name:
|
||||||
|
type: str
|
||||||
|
default: bookstack
|
||||||
|
bookstack_docker_compose_dir:
|
||||||
|
type: path
|
||||||
|
bookstack_docker_volume_dir:
|
||||||
|
type: path
|
||||||
|
bookstack_appdata_dir:
|
||||||
|
type: path
|
||||||
|
bookstack_db_data_dir:
|
||||||
|
type: path
|
||||||
|
bookstack_backup_dir:
|
||||||
|
type: path
|
||||||
|
|
||||||
|
bookstack_domain:
|
||||||
|
type: str
|
||||||
|
default: wiki.local.test
|
||||||
|
description: Hostname used in the Traefik Host rule.
|
||||||
|
bookstack_base_url:
|
||||||
|
type: str
|
||||||
|
description: Defaults to C("https://{{ bookstack_domain }}").
|
||||||
|
|
||||||
|
bookstack_image:
|
||||||
|
type: str
|
||||||
|
default: "lscr.io/linuxserver/bookstack:version-v26.03.3"
|
||||||
|
bookstack_db_image:
|
||||||
|
type: str
|
||||||
|
default: "lscr.io/linuxserver/mariadb:11.4.9"
|
||||||
|
|
||||||
|
bookstack_traefik_network:
|
||||||
|
type: str
|
||||||
|
default: proxy
|
||||||
|
bookstack_traefik_certresolver:
|
||||||
|
type: str
|
||||||
|
default: le
|
||||||
|
|
||||||
|
bookstack_tz:
|
||||||
|
type: str
|
||||||
|
default: Europe/Zurich
|
||||||
|
bookstack_puid:
|
||||||
|
type: str
|
||||||
|
default: "1000"
|
||||||
|
bookstack_pgid:
|
||||||
|
type: str
|
||||||
|
default: "1000"
|
||||||
|
|
||||||
|
bookstack_db_name:
|
||||||
|
type: str
|
||||||
|
default: bookstack
|
||||||
|
bookstack_db_user:
|
||||||
|
type: str
|
||||||
|
default: bookstack
|
||||||
|
bookstack_db_root_password:
|
||||||
|
type: str
|
||||||
|
required: true
|
||||||
|
description: MariaDB C(root) password. Override per-inventory.
|
||||||
|
bookstack_db_password:
|
||||||
|
type: str
|
||||||
|
required: true
|
||||||
|
description: MariaDB C(bookstack_db_user) password. Override per-inventory.
|
||||||
|
|
||||||
|
bookstack_admin_password:
|
||||||
|
type: str
|
||||||
|
required: true
|
||||||
|
description:
|
||||||
|
- Password for the local admin user that the role creates via
|
||||||
|
C(bookstack:create-admin). Lives alongside any OIDC users.
|
||||||
|
|
||||||
|
bookstack_app_key:
|
||||||
|
type: str
|
||||||
|
default: ''
|
||||||
|
description:
|
||||||
|
- When empty the role generates a persistent C(APP_KEY) on first
|
||||||
|
run and stores it under C({{ bookstack_docker_volume_dir }}/.app_key).
|
||||||
|
Override only when restoring an existing instance — a mismatching
|
||||||
|
key orphans all encrypted database values.
|
||||||
|
|
||||||
|
bookstack_admin_name:
|
||||||
|
type: str
|
||||||
|
default: Admin
|
||||||
|
bookstack_admin_email:
|
||||||
|
type: str
|
||||||
|
default: admin@local.test
|
||||||
|
bookstack_artisan_path:
|
||||||
|
type: path
|
||||||
|
default: /app/www/artisan
|
||||||
|
description:
|
||||||
|
- Path to BookStack's C(artisan) script inside the container. The
|
||||||
|
LSIO image's C(WORKDIR) is not the app directory, so this must
|
||||||
|
be absolute.
|
||||||
|
|
||||||
|
bookstack_mail_driver:
|
||||||
|
type: str
|
||||||
|
choices: [smtp, log, sendmail, mailgun, ses, postmark]
|
||||||
|
default: smtp
|
||||||
|
bookstack_mail_host:
|
||||||
|
type: str
|
||||||
|
default: smtp.local.test
|
||||||
|
bookstack_mail_port:
|
||||||
|
type: int
|
||||||
|
default: 587
|
||||||
|
bookstack_mail_encryption:
|
||||||
|
type: str
|
||||||
|
choices: [tls, ssl, '']
|
||||||
|
default: tls
|
||||||
|
bookstack_mail_from:
|
||||||
|
type: str
|
||||||
|
default: bookstack@local.test
|
||||||
|
bookstack_mail_from_name:
|
||||||
|
type: str
|
||||||
|
default: BookStack
|
||||||
|
bookstack_mail_username:
|
||||||
|
type: str
|
||||||
|
default: ''
|
||||||
|
bookstack_mail_password:
|
||||||
|
type: str
|
||||||
|
default: ''
|
||||||
|
|
||||||
|
bookstack_oidc_enabled:
|
||||||
|
type: bool
|
||||||
|
default: false
|
||||||
|
bookstack_oidc_name:
|
||||||
|
type: str
|
||||||
|
default: SSO
|
||||||
|
description: Display name of the SSO button on the login page.
|
||||||
|
bookstack_entra_tenant_id:
|
||||||
|
type: str
|
||||||
|
default: ''
|
||||||
|
description: Entra tenant UUID. Required when C(bookstack_oidc_enabled=true).
|
||||||
|
bookstack_oidc_issuer:
|
||||||
|
type: str
|
||||||
|
description:
|
||||||
|
- OIDC issuer URL. Defaults to the Entra v2 issuer template
|
||||||
|
built from C(bookstack_entra_tenant_id). Override for
|
||||||
|
Keycloak or any other provider.
|
||||||
|
bookstack_oidc_client_id:
|
||||||
|
type: str
|
||||||
|
default: ''
|
||||||
|
description: Required when C(bookstack_oidc_enabled=true).
|
||||||
|
bookstack_oidc_client_secret:
|
||||||
|
type: str
|
||||||
|
default: ''
|
||||||
|
description: Required when C(bookstack_oidc_enabled=true).
|
||||||
|
bookstack_oidc_auto_initiate:
|
||||||
|
type: bool
|
||||||
|
default: false
|
||||||
|
description:
|
||||||
|
- When true users are redirected straight to the IdP and the
|
||||||
|
local login is reachable only via C(?email_login=1).
|
||||||
|
bookstack_oidc_user_to_groups:
|
||||||
|
type: bool
|
||||||
|
default: false
|
||||||
|
description:
|
||||||
|
- When true BookStack syncs roles from the IdP groups claim
|
||||||
|
on every login. Requires BookStack roles whose
|
||||||
|
C(External Auth ID) matches the IdP group's Object ID.
|
||||||
|
bookstack_oidc_groups_claim:
|
||||||
|
type: str
|
||||||
|
default: groups
|
||||||
|
bookstack_oidc_additional_scopes:
|
||||||
|
type: str
|
||||||
|
default: openid profile email
|
||||||
|
|
||||||
|
bookstack_backup_enabled:
|
||||||
|
type: bool
|
||||||
|
default: true
|
||||||
|
bookstack_backup_retention_days:
|
||||||
|
type: int
|
||||||
|
default: 14
|
||||||
|
bookstack_backup_schedule:
|
||||||
|
type: str
|
||||||
|
default: "*-*-* 03:00:00"
|
||||||
|
description: systemd C(OnCalendar) expression for the backup timer.
|
||||||
25
roles/bookstack/meta/main.yml
Normal file
25
roles/bookstack/meta/main.yml
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
galaxy_info:
|
||||||
|
author: digitalboard
|
||||||
|
description: Deploy BookStack as a self-contained Docker Compose stack behind Traefik
|
||||||
|
company: digitalboard
|
||||||
|
license: MIT
|
||||||
|
|
||||||
|
min_ansible_version: "2.14"
|
||||||
|
|
||||||
|
platforms:
|
||||||
|
- name: Debian
|
||||||
|
versions:
|
||||||
|
- bookworm
|
||||||
|
- name: Ubuntu
|
||||||
|
versions:
|
||||||
|
- jammy
|
||||||
|
- noble
|
||||||
|
|
||||||
|
galaxy_tags:
|
||||||
|
- docker
|
||||||
|
- bookstack
|
||||||
|
- wiki
|
||||||
|
- documentation
|
||||||
|
- digitalboard
|
||||||
|
|
||||||
|
dependencies: []
|
||||||
223
roles/bookstack/tasks/main.yml
Normal file
223
roles/bookstack/tasks/main.yml
Normal file
|
|
@ -0,0 +1,223 @@
|
||||||
|
#SPDX-License-Identifier: MIT-0
|
||||||
|
---
|
||||||
|
# tasks file for bookstack
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# 1. VALIDATE REQUIRED SECRETS
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
- name: Assert required secrets are set
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- bookstack_db_root_password | length > 0
|
||||||
|
- bookstack_db_password | length > 0
|
||||||
|
- bookstack_admin_password | length > 0
|
||||||
|
- (not bookstack_oidc_enabled) or (bookstack_oidc_client_id | length > 0)
|
||||||
|
- (not bookstack_oidc_enabled) or (bookstack_oidc_client_secret | length > 0)
|
||||||
|
- (not bookstack_oidc_enabled) or (bookstack_entra_tenant_id | length > 0)
|
||||||
|
fail_msg: >-
|
||||||
|
One or more required secrets are unset. Provide them via OpenBao
|
||||||
|
lookup, Ansible Vault or --extra-vars. See README for the full list.
|
||||||
|
quiet: true
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# 2. PREPARATION: Packages, directories, APP_KEY
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
- name: Ensure required packages are installed
|
||||||
|
ansible.builtin.package:
|
||||||
|
name:
|
||||||
|
- python3-docker
|
||||||
|
- python3-requests
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Create docker compose directory
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ bookstack_docker_compose_dir }}"
|
||||||
|
state: directory
|
||||||
|
mode: '0755'
|
||||||
|
|
||||||
|
- name: Create BookStack data directories
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ item }}"
|
||||||
|
state: directory
|
||||||
|
owner: "{{ bookstack_puid }}"
|
||||||
|
group: "{{ bookstack_pgid }}"
|
||||||
|
mode: '0755'
|
||||||
|
loop:
|
||||||
|
- "{{ bookstack_docker_volume_dir }}"
|
||||||
|
- "{{ bookstack_appdata_dir }}"
|
||||||
|
- "{{ bookstack_db_data_dir }}"
|
||||||
|
- "{{ bookstack_backup_dir }}"
|
||||||
|
|
||||||
|
- name: Verify Traefik network exists
|
||||||
|
community.docker.docker_network_info:
|
||||||
|
name: "{{ bookstack_traefik_network }}"
|
||||||
|
register: _traefik_net
|
||||||
|
failed_when: not _traefik_net.exists
|
||||||
|
|
||||||
|
- name: Check whether APP_KEY has been generated before
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: "{{ bookstack_docker_volume_dir }}/.app_key"
|
||||||
|
register: _app_key_file
|
||||||
|
|
||||||
|
- name: Generate persistent APP_KEY on first run
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
set -o pipefail
|
||||||
|
umask 077
|
||||||
|
echo "base64:$(openssl rand -base64 32)" > {{ bookstack_docker_volume_dir }}/.app_key
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
creates: "{{ bookstack_docker_volume_dir }}/.app_key"
|
||||||
|
when:
|
||||||
|
- not _app_key_file.stat.exists
|
||||||
|
- bookstack_app_key | length == 0
|
||||||
|
|
||||||
|
- name: Write inventory-provided APP_KEY
|
||||||
|
ansible.builtin.copy:
|
||||||
|
content: "{{ bookstack_app_key }}\n"
|
||||||
|
dest: "{{ bookstack_docker_volume_dir }}/.app_key"
|
||||||
|
mode: '0600'
|
||||||
|
when:
|
||||||
|
- not _app_key_file.stat.exists
|
||||||
|
- bookstack_app_key | length > 0
|
||||||
|
no_log: true
|
||||||
|
|
||||||
|
- name: Read APP_KEY back into a fact
|
||||||
|
ansible.builtin.slurp:
|
||||||
|
src: "{{ bookstack_docker_volume_dir }}/.app_key"
|
||||||
|
register: _app_key_slurp
|
||||||
|
no_log: true
|
||||||
|
|
||||||
|
- name: Register APP_KEY fact
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
bookstack_resolved_app_key: "{{ _app_key_slurp.content | b64decode | trim }}"
|
||||||
|
no_log: true
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# 3. DEPLOY: Render compose, bring stack up
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
- name: Render docker-compose.yml for BookStack
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: docker-compose.yml.j2
|
||||||
|
dest: "{{ bookstack_docker_compose_dir }}/docker-compose.yml"
|
||||||
|
mode: '0640'
|
||||||
|
notify: restart bookstack
|
||||||
|
|
||||||
|
- name: Start BookStack containers
|
||||||
|
community.docker.docker_compose_v2:
|
||||||
|
project_src: "{{ bookstack_docker_compose_dir }}"
|
||||||
|
state: present
|
||||||
|
pull: always
|
||||||
|
wait: true
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# 4. CONFIGURE: Wait for app and seed initial admin user
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
- name: Wait for BookStack to be ready
|
||||||
|
ansible.builtin.command:
|
||||||
|
cmd: docker exec {{ bookstack_service_name }} curl -sf -o /dev/null -w "%{http_code}" http://localhost/login
|
||||||
|
register: _bookstack_health
|
||||||
|
retries: 30
|
||||||
|
delay: 5
|
||||||
|
until: _bookstack_health.stdout == "200"
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Wait for BookStack migrations to be complete
|
||||||
|
community.docker.docker_container_exec:
|
||||||
|
container: "{{ bookstack_service_name }}-db"
|
||||||
|
argv:
|
||||||
|
- mariadb
|
||||||
|
- --protocol=tcp
|
||||||
|
- -h
|
||||||
|
- 127.0.0.1
|
||||||
|
- -u
|
||||||
|
- "{{ bookstack_db_user }}"
|
||||||
|
- "-p{{ bookstack_db_password }}"
|
||||||
|
- "{{ bookstack_db_name }}"
|
||||||
|
- -Nse
|
||||||
|
- "SHOW TABLES LIKE 'users';"
|
||||||
|
register: _users_table
|
||||||
|
retries: 30
|
||||||
|
delay: 5
|
||||||
|
until: _users_table.stdout | trim == 'users'
|
||||||
|
changed_when: false
|
||||||
|
no_log: true
|
||||||
|
|
||||||
|
- name: Check whether the initial admin already exists
|
||||||
|
community.docker.docker_container_exec:
|
||||||
|
container: "{{ bookstack_service_name }}-db"
|
||||||
|
argv:
|
||||||
|
- mariadb
|
||||||
|
- --protocol=tcp
|
||||||
|
- -h
|
||||||
|
- 127.0.0.1
|
||||||
|
- -u
|
||||||
|
- "{{ bookstack_db_user }}"
|
||||||
|
- "-p{{ bookstack_db_password }}"
|
||||||
|
- "{{ bookstack_db_name }}"
|
||||||
|
- -Nse
|
||||||
|
- "SELECT COUNT(*) FROM users WHERE email = '{{ bookstack_admin_email }}';"
|
||||||
|
register: _admin_exists
|
||||||
|
changed_when: false
|
||||||
|
no_log: true
|
||||||
|
|
||||||
|
- name: Create initial admin user
|
||||||
|
community.docker.docker_container_exec:
|
||||||
|
container: "{{ bookstack_service_name }}"
|
||||||
|
argv:
|
||||||
|
- php
|
||||||
|
- "{{ bookstack_artisan_path }}"
|
||||||
|
- bookstack:create-admin
|
||||||
|
- "--email={{ bookstack_admin_email }}"
|
||||||
|
- "--name={{ bookstack_admin_name }}"
|
||||||
|
- "--password={{ bookstack_admin_password }}"
|
||||||
|
when: (_admin_exists.stdout | trim | int) == 0
|
||||||
|
no_log: true
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# 5. BACKUP: systemd timer for daily DB + uploads dump
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
- name: Render backup script
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: backup.sh.j2
|
||||||
|
dest: /usr/local/bin/bookstack-backup.sh
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: '0750'
|
||||||
|
when: bookstack_backup_enabled | bool
|
||||||
|
|
||||||
|
- name: Render backup systemd service
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: bookstack-backup.service.j2
|
||||||
|
dest: /etc/systemd/system/bookstack-backup.service
|
||||||
|
mode: '0644'
|
||||||
|
when: bookstack_backup_enabled | bool
|
||||||
|
notify: reload systemd
|
||||||
|
|
||||||
|
- name: Render backup systemd timer
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: bookstack-backup.timer.j2
|
||||||
|
dest: /etc/systemd/system/bookstack-backup.timer
|
||||||
|
mode: '0644'
|
||||||
|
when: bookstack_backup_enabled | bool
|
||||||
|
notify: reload systemd
|
||||||
|
|
||||||
|
- name: Enable and start backup timer
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: bookstack-backup.timer
|
||||||
|
enabled: true
|
||||||
|
state: started
|
||||||
|
daemon_reload: true
|
||||||
|
when: bookstack_backup_enabled | bool
|
||||||
|
|
||||||
|
- name: Disable backup timer when feature is off
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: bookstack-backup.timer
|
||||||
|
enabled: false
|
||||||
|
state: stopped
|
||||||
|
when: not (bookstack_backup_enabled | bool)
|
||||||
|
failed_when: false
|
||||||
41
roles/bookstack/templates/backup.sh.j2
Normal file
41
roles/bookstack/templates/backup.sh.j2
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# {{ ansible_managed }}
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
BACKUP_DIR="{{ bookstack_backup_dir }}"
|
||||||
|
RETENTION_DAYS={{ bookstack_backup_retention_days }}
|
||||||
|
APPDATA_DIR="{{ bookstack_appdata_dir }}"
|
||||||
|
STAMP="$(date +%Y%m%d-%H%M%S)"
|
||||||
|
|
||||||
|
mkdir -p "$BACKUP_DIR"
|
||||||
|
|
||||||
|
# --- DB dump (mariadb-dump from inside the DB container) ---
|
||||||
|
# Use the app user via TCP because root@localhost is unix_socket-auth only
|
||||||
|
# in the LSIO MariaDB image and root@% does not exist.
|
||||||
|
docker exec {{ bookstack_service_name }}-db \
|
||||||
|
mariadb-dump \
|
||||||
|
--protocol=tcp -h 127.0.0.1 \
|
||||||
|
-u "{{ bookstack_db_user }}" -p"{{ bookstack_db_password }}" \
|
||||||
|
--single-transaction --routines --triggers --quick \
|
||||||
|
"{{ bookstack_db_name }}" \
|
||||||
|
| gzip -9 > "$BACKUP_DIR/bookstack-db-$STAMP.sql.gz"
|
||||||
|
|
||||||
|
# --- File uploads (images, attachments) ---
|
||||||
|
# LSIO BookStack stores user uploads under /config/www/{uploads,storage/uploads,files}.
|
||||||
|
tar --warning=no-file-changed \
|
||||||
|
-czf "$BACKUP_DIR/bookstack-files-$STAMP.tar.gz" \
|
||||||
|
-C "$APPDATA_DIR/www" \
|
||||||
|
uploads storage/uploads files 2>/dev/null || true
|
||||||
|
|
||||||
|
# --- APP_KEY backup (critical for restore!) ---
|
||||||
|
install -m 0600 "{{ bookstack_docker_volume_dir }}/.app_key" \
|
||||||
|
"$BACKUP_DIR/bookstack-appkey-$STAMP.txt"
|
||||||
|
|
||||||
|
# --- Retention ---
|
||||||
|
find "$BACKUP_DIR" -type f \
|
||||||
|
\( -name 'bookstack-db-*.sql.gz' \
|
||||||
|
-o -name 'bookstack-files-*.tar.gz' \
|
||||||
|
-o -name 'bookstack-appkey-*.txt' \) \
|
||||||
|
-mtime +"$RETENTION_DAYS" -delete
|
||||||
|
|
||||||
|
echo "Backup complete: $STAMP"
|
||||||
12
roles/bookstack/templates/bookstack-backup.service.j2
Normal file
12
roles/bookstack/templates/bookstack-backup.service.j2
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
# {{ ansible_managed }}
|
||||||
|
[Unit]
|
||||||
|
Description=BookStack backup (DB + uploads)
|
||||||
|
Requires=docker.service
|
||||||
|
After=docker.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/local/bin/bookstack-backup.sh
|
||||||
|
Nice=10
|
||||||
|
IOSchedulingClass=best-effort
|
||||||
|
IOSchedulingPriority=7
|
||||||
11
roles/bookstack/templates/bookstack-backup.timer.j2
Normal file
11
roles/bookstack/templates/bookstack-backup.timer.j2
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
# {{ ansible_managed }}
|
||||||
|
[Unit]
|
||||||
|
Description=Daily BookStack backup
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar={{ bookstack_backup_schedule }}
|
||||||
|
Persistent=true
|
||||||
|
RandomizedDelaySec=300
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
87
roles/bookstack/templates/docker-compose.yml.j2
Normal file
87
roles/bookstack/templates/docker-compose.yml.j2
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
#---------------------------------------------------------------------#
|
||||||
|
# BookStack - Self-hosted wiki / knowledge base. #
|
||||||
|
#---------------------------------------------------------------------#
|
||||||
|
---
|
||||||
|
services:
|
||||||
|
{{ bookstack_service_name }}:
|
||||||
|
image: {{ bookstack_image }}
|
||||||
|
container_name: {{ bookstack_service_name }}
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
PUID: "{{ bookstack_puid }}"
|
||||||
|
PGID: "{{ bookstack_pgid }}"
|
||||||
|
TZ: "{{ bookstack_tz }}"
|
||||||
|
APP_URL: "{{ bookstack_base_url }}"
|
||||||
|
APP_KEY: "{{ bookstack_resolved_app_key }}"
|
||||||
|
DB_HOST: "{{ bookstack_service_name }}-db"
|
||||||
|
DB_PORT: "3306"
|
||||||
|
DB_DATABASE: "{{ bookstack_db_name }}"
|
||||||
|
DB_USERNAME: "{{ bookstack_db_user }}"
|
||||||
|
DB_PASSWORD: "{{ bookstack_db_password }}"
|
||||||
|
MAIL_DRIVER: "{{ bookstack_mail_driver }}"
|
||||||
|
MAIL_HOST: "{{ bookstack_mail_host }}"
|
||||||
|
MAIL_PORT: "{{ bookstack_mail_port }}"
|
||||||
|
MAIL_USERNAME: "{{ bookstack_mail_username }}"
|
||||||
|
MAIL_PASSWORD: "{{ bookstack_mail_password }}"
|
||||||
|
MAIL_ENCRYPTION: "{{ bookstack_mail_encryption }}"
|
||||||
|
MAIL_FROM: "{{ bookstack_mail_from }}"
|
||||||
|
MAIL_FROM_NAME: "{{ bookstack_mail_from_name }}"
|
||||||
|
{% if bookstack_oidc_enabled %}
|
||||||
|
AUTH_METHOD: "oidc"
|
||||||
|
AUTH_AUTO_INITIATE: "{{ bookstack_oidc_auto_initiate | string | lower }}"
|
||||||
|
OIDC_NAME: "{{ bookstack_oidc_name }}"
|
||||||
|
OIDC_DISPLAY_NAME_CLAIMS: "name"
|
||||||
|
OIDC_CLIENT_ID: "{{ bookstack_oidc_client_id }}"
|
||||||
|
OIDC_CLIENT_SECRET: "{{ bookstack_oidc_client_secret }}"
|
||||||
|
OIDC_ISSUER: "{{ bookstack_oidc_issuer }}"
|
||||||
|
OIDC_ISSUER_DISCOVER: "true"
|
||||||
|
OIDC_END_SESSION_ENDPOINT: "true"
|
||||||
|
OIDC_ADDITIONAL_SCOPES: "{{ bookstack_oidc_additional_scopes }}"
|
||||||
|
OIDC_USER_TO_GROUPS: "{{ bookstack_oidc_user_to_groups | string | lower }}"
|
||||||
|
OIDC_GROUPS_CLAIM: "{{ bookstack_oidc_groups_claim }}"
|
||||||
|
{% endif %}
|
||||||
|
volumes:
|
||||||
|
- {{ bookstack_appdata_dir }}:/config
|
||||||
|
networks:
|
||||||
|
- {{ bookstack_traefik_network }}
|
||||||
|
- internal
|
||||||
|
depends_on:
|
||||||
|
{{ bookstack_service_name }}-db:
|
||||||
|
condition: service_healthy
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.docker.network={{ bookstack_traefik_network }}"
|
||||||
|
- "traefik.http.routers.{{ bookstack_service_name }}.rule=Host(`{{ bookstack_domain }}`)"
|
||||||
|
- "traefik.http.routers.{{ bookstack_service_name }}.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.{{ bookstack_service_name }}.tls=true"
|
||||||
|
- "traefik.http.routers.{{ bookstack_service_name }}.tls.certresolver={{ bookstack_traefik_certresolver }}"
|
||||||
|
- "traefik.http.services.{{ bookstack_service_name }}.loadbalancer.server.port=80"
|
||||||
|
|
||||||
|
{{ bookstack_service_name }}-db:
|
||||||
|
image: {{ bookstack_db_image }}
|
||||||
|
container_name: {{ bookstack_service_name }}-db
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
PUID: "{{ bookstack_puid }}"
|
||||||
|
PGID: "{{ bookstack_pgid }}"
|
||||||
|
TZ: "{{ bookstack_tz }}"
|
||||||
|
MYSQL_ROOT_PASSWORD: "{{ bookstack_db_root_password }}"
|
||||||
|
MYSQL_DATABASE: "{{ bookstack_db_name }}"
|
||||||
|
MYSQL_USER: "{{ bookstack_db_user }}"
|
||||||
|
MYSQL_PASSWORD: "{{ bookstack_db_password }}"
|
||||||
|
volumes:
|
||||||
|
- {{ bookstack_db_data_dir }}:/config
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "mariadb-admin ping -h 127.0.0.1 -u root --password=\"$$MYSQL_ROOT_PASSWORD\" --silent"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 12
|
||||||
|
start_period: 60s
|
||||||
|
|
||||||
|
networks:
|
||||||
|
{{ bookstack_traefik_network }}:
|
||||||
|
external: true
|
||||||
|
internal:
|
||||||
|
driver: bridge
|
||||||
1
roles/bookstack/tests/inventory
Normal file
1
roles/bookstack/tests/inventory
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
localhost
|
||||||
5
roles/bookstack/tests/test.yml
Normal file
5
roles/bookstack/tests/test.yml
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
- hosts: localhost
|
||||||
|
remote_user: root
|
||||||
|
roles:
|
||||||
|
- bookstack
|
||||||
3
roles/bookstack/vars/main.yml
Normal file
3
roles/bookstack/vars/main.yml
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
#SPDX-License-Identifier: MIT-0
|
||||||
|
---
|
||||||
|
# vars file for bookstack
|
||||||
|
|
@ -15,7 +15,11 @@ import sys
|
||||||
|
|
||||||
# Make the filter importable without having Ansible auto-discovery in
|
# Make the filter importable without having Ansible auto-discovery in
|
||||||
# the way (it would only run during a real `ansible-playbook` invocation).
|
# the way (it would only run during a real `ansible-playbook` invocation).
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
sys.path.insert(
|
||||||
|
0,
|
||||||
|
os.path.join(os.path.dirname(__file__), '..', '..', '..', '..',
|
||||||
|
'plugins', 'filter')
|
||||||
|
)
|
||||||
|
|
||||||
import pytest # noqa: E402
|
import pytest # noqa: E402
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -136,7 +136,7 @@
|
||||||
|
|
||||||
- name: Compute Homarr app layouts
|
- name: Compute Homarr app layouts
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
homarr_layout: "{{ homarr_apps | homarr_compute_layouts }}"
|
homarr_layout: "{{ homarr_apps | digitalboard.core.homarr_compute_layouts }}"
|
||||||
|
|
||||||
- name: Show computed app layouts
|
- name: Show computed app layouts
|
||||||
ansible.builtin.debug:
|
ansible.builtin.debug:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue