feat(bookstack): add role for self-hosted BookStack deployment
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.
This commit is contained in:
parent
78095cca1d
commit
9d539d0da4
16 changed files with 659 additions and 2 deletions
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue