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
30f3c16b59
commit
951b1822fe
16 changed files with 664 additions and 2 deletions
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue