digitalboard.core/roles/homarr/tasks/main.yml
Tobias Wüst 951b1822fe
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.
2026-05-26 15:30:21 +02:00

164 lines
No EOL
5.3 KiB
YAML

#SPDX-License-Identifier: MIT-0
---
# tasks file for homarr
# =====================================================================
# 0. VALIDATION
# =====================================================================
- name: Validate encryption key
ansible.builtin.assert:
that:
- homarr_secret_encryption_key | length == 64
- homarr_secret_encryption_key is match('^[a-fA-F0-9]+$')
fail_msg: >-
homarr_secret_encryption_key must be a 64-character hex string.
Generate with: openssl rand -hex 32
Provide via OpenBao, Ansible Vault or extra-vars.
success_msg: Encryption key validation passed
- name: Validate OIDC configuration when enabled
ansible.builtin.assert:
that:
- homarr_oidc_client_secret | length > 0
fail_msg: >-
homarr_oidc_client_secret must be set when 'oidc' is in homarr_auth_providers.
Set via OpenBao or remove 'oidc' from homarr_auth_providers.
when: "'oidc' in homarr_auth_providers"
- name: Validate homarr_apps have unique ids
ansible.builtin.assert:
that:
- homarr_apps | map(attribute='id') | list | length ==
homarr_apps | map(attribute='id') | unique | list | length
fail_msg: >-
homarr_apps contains duplicate ids.
Each app must have a unique 'id'. Got:
{{ homarr_apps | map(attribute='id') | list }}
success_msg: All app ids are unique
when: homarr_apps | length > 0
# =====================================================================
# 1. PREPARATION: packages and directories before container start
# =====================================================================
- name: Ensure required packages are installed
ansible.builtin.package:
name:
- sqlite3
- python3-docker
state: present
- name: Create docker compose directory
ansible.builtin.file:
path: "{{ homarr_docker_compose_dir }}"
state: directory
mode: '0755'
- name: Create Homarr data directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "1000"
group: "1000"
mode: "0755"
loop:
- "{{ homarr_appdata_dir }}"
- "{{ homarr_appdata_dir }}/db"
- name: Check if database already exists
ansible.builtin.stat:
path: "{{ homarr_db }}"
register: db_exists
# =====================================================================
# 2. START CONTAINER
# =====================================================================
- name: Create docker-compose file for homarr
ansible.builtin.template:
src: docker-compose.yml.j2
dest: "{{ homarr_docker_compose_dir }}/docker-compose.yml"
mode: '0644'
- name: Start homarr containers
community.docker.docker_compose_v2:
project_src: "{{ homarr_docker_compose_dir }}"
state: present
# =====================================================================
# 3. WAIT FOR DATABASE
# =====================================================================
- name: Wait for database to be created by Homarr
ansible.builtin.wait_for:
path: "{{ homarr_db }}"
state: present
timeout: 60
when: not db_exists.stat.exists
- name: Wait for database schema to be initialized
ansible.builtin.command:
cmd: sqlite3 "{{ homarr_db }}" "SELECT name FROM sqlite_master WHERE type='table' AND name='board';"
register: schema_check
until: schema_check.stdout == "board"
retries: 30
delay: 2
changed_when: false
when: not db_exists.stat.exists
# =====================================================================
# 4. GENERATE BCRYPT HASH (on controller, not on target)
# =====================================================================
- name: Generate bcrypt hash for admin password
ansible.builtin.shell:
cmd: python3 -c "import bcrypt, sys; print(bcrypt.hashpw(sys.stdin.read().encode(), bcrypt.gensalt(rounds=10)).decode())"
stdin: "{{ homarr_admin_password }}"
stdin_add_newline: false
delegate_to: localhost
become: false
register: bcrypt_result
changed_when: false
no_log: true
- name: Set bcrypt hash fact
ansible.builtin.set_fact:
homarr_bcrypt_hash: "{{ bcrypt_result.stdout }}"
no_log: true
# =====================================================================
# 5. COMPUTE APP LAYOUTS
# =====================================================================
# Packing is done by the homarr_compute_layouts filter plugin (Python)
# rather than inline Jinja, so the seed template stays readable and the
# packing algorithm can be unit-tested in isolation.
- name: Compute Homarr app layouts
ansible.builtin.set_fact:
homarr_layout: "{{ homarr_apps | digitalboard.core.homarr_compute_layouts }}"
- name: Show computed app layouts
ansible.builtin.debug:
var: homarr_layout
verbosity: 1
# =====================================================================
# 6. SEED DATABASE (only if local admin user does not exist yet)
# =====================================================================
- name: Check if local admin user exists
ansible.builtin.command:
cmd: sqlite3 "{{ homarr_db }}" "SELECT id FROM user WHERE id='user-local-admin';"
register: admin_exists
changed_when: false
failed_when: false
- name: Seed Homarr database
ansible.builtin.command:
cmd: sqlite3 "{{ homarr_db }}"
stdin: "{{ lookup('template', 'homarr_seed.sql.j2') }}"
register: seed_result
changed_when: seed_result.rc == 0
when: admin_exists.stdout == ""
notify: restart homarr