Merge branch 'feature/homarr'

This commit is contained in:
Tobias Wüst 2026-05-13 15:40:52 +02:00
commit 9fc34dfb29
Signed by: Tobias-Wuest
GPG key ID: 2D8992B0F4CA97E8
6 changed files with 534 additions and 384 deletions

View file

@ -1,38 +1,196 @@
Role Name # homarr
=========
A brief description of the role goes here. Deploy [Homarr](https://github.com/homarr-labs/homarr) as a self-contained
Docker Compose stack behind Traefik, with seeded admin user, OIDC group
and customizable application tiles.
Requirements ## What this role does
------------
Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required. - Deploys the official Homarr container with Traefik labels
- Seeds the SQLite database with:
- server settings (locale, analytics, crawling, default board)
- a default board with the three layouts (desktop/tablet/mobile)
- a local admin user with bcrypt-hashed password
- OIDC and credentials admin groups with full permissions
- application tiles defined in `homarr_apps`, auto-laid-out across all
three screen sizes
- Skips the onboarding wizard so the instance is usable right after deploy
- Restarts the container via handler when the seed or compose file changes
Role Variables ## What this role does NOT do
--------------
A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well. - Does not configure OIDC end-to-end — set `homarr_oidc_*` variables and
configure the corresponding client in your identity provider
- Does not migrate existing Homarr databases — only seeds empty ones
- Does not create users beyond the single local admin (OIDC users are
provisioned on first login)
Dependencies ## Required variables
------------
A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles. Provide via OpenBao, Ansible Vault, or extra-vars. **Never commit real
secrets to version control.**
Example Playbook | Variable | Format | Generate with |
---------------- |---|---|---|
| `homarr_secret_encryption_key` | 64-char hex string | `openssl rand -hex 32` |
| `homarr_admin_password` | strong password | `openssl rand -base64 24` |
| `homarr_oidc_client_secret` | from your identity provider | — |
Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too: The `assert` task at the top of the role will fail fast if the encryption
key is missing or malformed.
- hosts: servers ## Configurable variables
See `defaults/main.yml` for the full list. Most useful overrides:
| Variable | Default | Purpose |
|---|---|---|
| `homarr_domain` | `homarr.local.test` | Traefik Host rule |
| `homarr_base_url` | `https://home.local.test` | NEXTAUTH_URL / BASE_URL |
| `homarr_auth_providers` | `credentials` | `credentials`, `oidc`, or both |
| `homarr_oidc_issuer` | empty | Identity provider issuer URL |
| `homarr_oidc_client_id` | empty | OIDC client id |
| `homarr_oidc_admin_group` | `homarr-admins` | Group granting admin role |
| `homarr_apps` | `[]` | List of application tiles, see below |
## Application tiles
`homarr_apps` is a list of tile definitions that are seeded into the
default board. Each entry needs:
| Field | Required | Description |
|---|---|---|
| `id` | yes | Unique slug, used as `app-<id>` and `item-<id>` |
| `name` | yes | Display name |
| `icon` | yes | Icon URL |
| `href` | yes | Click target |
| `width` | yes | Tile width in grid cells (110) |
| `description` | no | Tooltip / subtitle |
| `height` | no | Tile height (default `1`) |
The role validates that all `id` values are unique.
### Auto-layout
Tiles are packed left-to-right into three layouts:
- **Desktop**: 10 columns
- **Tablet**: 6 columns
- **Mobile**: 2 columns
When a tile does not fit the remaining width of a row, it wraps to the
next row. Tile width is clamped to the grid width (a tile with
`width: 8` becomes `width: 6` on tablet and `width: 2` on mobile).
### Example
```yaml
homarr_apps:
- id: nextcloud
name: Nextcloud
description: Cloud Storage & Collaboration
icon: https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/nextcloud.png
href: https://cloud.example.com
width: 2
- id: keycloak
name: Keycloak
description: Identity & Access Management
icon: https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/keycloak.png
href: https://auth.example.com
width: 2
```
## First login
After the role completes, log in at `{{ homarr_base_url }}` with:
- Username: value of `homarr_admin_username` (default `admin`)
- Password: value of `homarr_admin_password`
OIDC users are provisioned on first login if their identity provider
group matches `homarr_oidc_admin_group`. They receive admin permissions
automatically through the seeded `group-oidc-admins` group.
## Example playbook
```yaml
- name: Deploy Homarr service
hosts: homarr_servers
become: true
roles: roles:
- { role: username.rolename, x: 42 } - digitalboard.core.homarr
```
License With inventory variables:
-------
BSD ```yaml
# inventories/<env>/group_vars/homarr_servers.yml
homarr_domain: home.digitalboard.ch
homarr_base_url: "https://home.digitalboard.ch"
Author Information homarr_auth_providers: "credentials,oidc"
------------------ homarr_oidc_issuer: "https://auth.digitalboard.ch/realms/Digitalboard"
homarr_oidc_client_id: "homarr-digitalboard"
homarr_oidc_client_name: "Digitalboard"
An optional section for the role authors to include contact information, or a website (HTML is not allowed). homarr_secret_encryption_key: >-
{{ lookup('community.hashi_vault.vault_kv2_get',
'digitalboard/homarr',
mount_point='kv').data.data.encryption_key }}
homarr_admin_password: >-
{{ lookup('community.hashi_vault.vault_kv2_get',
'digitalboard/homarr',
mount_point='kv').data.data.admin_password }}
homarr_oidc_client_secret: >-
{{ lookup('community.hashi_vault.vault_kv2_get',
'digitalboard/homarr',
mount_point='kv').data.data.oidc_client_secret }}
homarr_apps:
- id: nextcloud
name: Nextcloud
description: Cloud Storage
icon: https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/nextcloud.png
href: https://cloud.digitalboard.ch
width: 2
- id: keycloak
name: Keycloak
description: Identity & Access Management
icon: https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/keycloak.png
href: https://auth.digitalboard.ch
width: 2
```
## Re-running the role
The role is idempotent for the typical re-run case:
- The seed only runs when no local admin user exists in the database
- The compose file and seed template are deployed via `template`,
which only changes content when the inputs change
- The restart handler only fires when one of those templates changes
If you need to re-seed an existing database (for example after deleting
the database file to apply schema changes), the role will detect the
fresh database and seed it again on the next run.
## Troubleshooting
**Login fails after deploy.** Verify that the bcrypt hash was written
correctly:
```bash
sqlite3 /srv/data/homarr/homarr/appdata/db/db.sqlite \
"SELECT id, name, email, length(password), provider FROM user;"
```
Expected: one row with `user-local-admin`, password length 60,
provider `credentials`.
**Encryption key validation fails.** The key must be exactly 64
characters and contain only hex digits (`[a-fA-F0-9]`). Both upper-
and lowercase are accepted.
**App tiles overlap.** Check `homarr_apps` for duplicate `id` values.
The role validates this, but if you bypass the check, the seed will
still run and Homarr will display only one of the duplicates.

View file

@ -1,3 +1,4 @@
homarr_apps: [ ]
#SPDX-License-Identifier: MIT-0 #SPDX-License-Identifier: MIT-0
--- ---
# defaults file for homarr # defaults file for homarr
@ -8,43 +9,67 @@ docker_volume_base_dir: /srv/data
# homarr-specific configuration # homarr-specific configuration
homarr_base_path: /srv/data/homarr homarr_base_path: /srv/data/homarr
homarr_service_name: homarr homarr_docker_compose_dir: "{{ docker_compose_base_dir }}/homarr"
homarr_docker_compose_dir: "{{ docker_compose_base_dir }}/{{ homarr_service_name }}" homarr_docker_volume_dir: "{{ docker_volume_base_dir }}/homarr"
homarr_docker_volume_dir: "{{ docker_volume_base_dir }}/{{ homarr_service_name }}" homarr_appdata_dir: "{{ homarr_docker_volume_dir }}/homarr/appdata"
homarr_appdata_dir: "{{ homarr_docker_volume_dir }}/{{ homarr_service_name }}/appdata" homarr_db: "{{ homarr_appdata_dir }}/db/db.sqlite"
homarr_db_dir: "{{ homarr_appdata_dir }}/db/db.sqlite"
# Service configuration # Service configuration
homarr_domain: "homarr.local.test" homarr_domain: "homarr.local.test"
homarr_image: "ghcr.io/homarr-labs/homarr:latest" homarr_image: "ghcr.io/homarr-labs/homarr:latest"
homarr_secret_encryption_key: "4fc2f54f54be3f4439b728da81b743fb0ee6317fd1a24f4096611f68019fa5a7"
homarr_port: 7575 homarr_port: 7575
homarr_use_docker: false homarr_use_docker: false
# URL wird für BASE_URL, NEXTAUTH_URL und die Completion-Message verwendet # REQUIRED: 64-character hex string used to encrypt integration credentials.
# Generate with: openssl rand -hex 32
# Provide via OpenBao lookup, Ansible Vault, or extra-vars.
# Never commit a real key to version control.
homarr_secret_encryption_key: ""
# URL — used for BASE_URL, NEXTAUTH_URL and the completion message
homarr_base_url: "https://home.local.test" homarr_base_url: "https://home.local.test"
# OIDC Konfiguration # Auth providers (comma-separated): credentials, oidc, ldap
oidc_issuer: "https://auth.digitalboard.ch/realms/Digitalboard" homarr_auth_providers: "credentials"
oidc_client_id: "homarr-digitalboard"
oidc_client_name: "Digitalboard"
oidc_scopes: "openid profile email groups"
oidc_groups_attribute: "groups"
oidc_client_secret: "mein-test-secret-aus-keycloak"
oidc_auto_login: "false"
# OIDC Admin-Gruppe (muss in Keycloak existieren) # OIDC configuration (only used when 'oidc' is in homarr_auth_providers)
oidc_admin_group: "homarr-admins" homarr_oidc_issuer: ""
homarr_oidc_client_id: ""
homarr_oidc_client_name: ""
homarr_oidc_scopes: "openid profile email groups"
homarr_oidc_groups_attribute: "groups"
homarr_oidc_client_secret: ""
homarr_oidc_auto_login: "false"
# Board Konfiguration # OIDC admin group (must exist in the identity provider)
default_board_name: "Home" homarr_oidc_admin_group: "homarr-admins"
default_board_public: true
# Board configuration
homarr_default_board_name: "Home"
homarr_default_board_public: true
# Traefik configuration # Traefik configuration
homarr_traefik_network: "proxy" homarr_traefik_network: "proxy"
homarr_use_ssl: true homarr_use_ssl: true
# Lokaler Admin # Local admin (override in inventory or via vault)
homarr_admin_username: "admin" homarr_admin_username: "admin"
homarr_admin_email: "admin@digitalboard.ch" homarr_admin_email: "admin@example.com"
homarr_admin_password: "ChangeMe123!" homarr_admin_password: "ChangeMe123!"
# Applications shown on the default board.
# Override in your project/inventory vars. Each app needs:
# id, name, icon, href, width (1-10). Optional: description, height (default 1).
# Apps are automatically packed left-to-right into the desktop grid (10 cols),
# scaled to tablet (6 cols) and mobile (2 cols).
#
# Example:
# homarr_apps:
# - id: nextcloud
# name: Nextcloud
# description: Cloud Storage & Collaboration
# icon: https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/nextcloud.png
# href: https://cloud.example.com
# width: 2
# height: 1
homarr_apps: []

View file

@ -1,3 +1,8 @@
#SPDX-License-Identifier: MIT-0 #SPDX-License-Identifier: MIT-0
--- ---
# handlers file for homarr # handlers file for homarr
- name: restart homarr
community.docker.docker_compose_v2:
project_src: "{{ homarr_docker_compose_dir }}"
state: restarted

View file

@ -3,7 +3,43 @@
# tasks file for homarr # tasks file for homarr
# ===================================================================== # =====================================================================
# 1. VORBEREITUNG: Pakete und Verzeichnisse VOR Container-Start # 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 - name: Ensure required packages are installed
@ -11,7 +47,6 @@
name: name:
- sqlite3 - sqlite3
- python3-docker - python3-docker
- python3-bcrypt
state: present state: present
- name: Create docker compose directory - name: Create docker compose directory
@ -33,11 +68,11 @@
- name: Check if database already exists - name: Check if database already exists
ansible.builtin.stat: ansible.builtin.stat:
path: "{{ homarr_db_dir }}" path: "{{ homarr_db }}"
register: db_exists register: db_exists
# ===================================================================== # =====================================================================
# 2. CONTAINER STARTEN # 2. START CONTAINER
# ===================================================================== # =====================================================================
- name: Create docker-compose file for homarr - name: Create docker-compose file for homarr
@ -52,345 +87,62 @@
state: present state: present
# ===================================================================== # =====================================================================
# 3. AUF DATENBANK WARTEN # 3. WAIT FOR DATABASE
# ===================================================================== # =====================================================================
- name: Wait for database to be created by Homarr - name: Wait for database to be created by Homarr
ansible.builtin.wait_for: ansible.builtin.wait_for:
path: "{{ homarr_db_dir }}" path: "{{ homarr_db }}"
state: present state: present
timeout: 60 timeout: 60
when: not db_exists.stat.exists when: not db_exists.stat.exists
- name: Wait for database schema to be initialized - name: Wait for database schema to be initialized
ansible.builtin.shell: | ansible.builtin.command:
i=0 cmd: sqlite3 "{{ homarr_db }}" "SELECT name FROM sqlite_master WHERE type='table' AND name='board';"
while [ $i -lt 30 ]; do
if sqlite3 "{{ homarr_db_dir }}" "SELECT name FROM sqlite_master WHERE type='table' AND name='board';" 2>/dev/null | grep -q board; then
exit 0
fi
sleep 2
i=$((i + 1))
done
exit 1
register: schema_check register: schema_check
until: schema_check.stdout == "board"
retries: 30
delay: 2
changed_when: false changed_when: false
when: not db_exists.stat.exists when: not db_exists.stat.exists
- name: Ensure python3-bcrypt is installed # =====================================================================
ansible.builtin.package: # 4. GENERATE BCRYPT HASH (on controller, not on target)
name: python3-bcrypt # =====================================================================
state: present
- name: Generate bcrypt hash for admin password - name: Generate bcrypt hash for admin password
ansible.builtin.shell: | ansible.builtin.shell:
python3 -c " cmd: python3 -c "import bcrypt, sys; print(bcrypt.hashpw(sys.stdin.read().encode(), bcrypt.gensalt(rounds=10)).decode())"
import bcrypt stdin: "{{ homarr_admin_password }}"
password = '{{ homarr_admin_password }}'.encode() stdin_add_newline: false
salt = bcrypt.gensalt(rounds=10) delegate_to: localhost
hashed = bcrypt.hashpw(password, salt) become: false
print(salt.decode()) register: bcrypt_result
print(hashed.decode())
"
register: bcrypt_output
changed_when: false changed_when: false
no_log: true no_log: true
- name: Set bcrypt hash fact
ansible.builtin.set_fact:
homarr_bcrypt_hash: "{{ bcrypt_result.stdout }}"
no_log: true
# ===================================================================== # =====================================================================
# 4. DATENBANK SEEDEN (nur wenn Onboarding noch nicht abgeschlossen) # 5. SEED DATABASE (only if local admin user does not exist yet)
# ===================================================================== # =====================================================================
- name: Check if onboarding is already completed - name: Check if local admin user exists
ansible.builtin.shell: | ansible.builtin.command:
sqlite3 "{{ homarr_db_dir }}" "SELECT step FROM onboarding WHERE step='finish';" 2>/dev/null cmd: sqlite3 "{{ homarr_db }}" "SELECT id FROM user WHERE id='user-local-admin';"
register: onboarding_status register: admin_exists
changed_when: false changed_when: false
failed_when: false failed_when: false
- name: Seed Homarr database - name: Seed Homarr database
ansible.builtin.shell: | ansible.builtin.command:
sqlite3 "{{ homarr_db_dir }}" << 'SEEDSQL' cmd: sqlite3 "{{ homarr_db }}"
-- SERVER SETTINGS stdin: "{{ lookup('template', 'homarr_seed.sql.j2') }}"
INSERT OR REPLACE INTO serverSetting (setting_key, value)
VALUES
('analytics', '{"json": {"enableGeneral": false, "enableWidgetData": false, "enableIntegrationData": false}}'),
('culture', '{"json": {"defaultLocale": "de"}}'),
('crawling', '{"json": {"crawlingEnabled": false}}'),
('board', '{"json": {"defaultBoardId": "board-default"}}');
-- ONBOARDING ÜBERSPRINGEN
UPDATE onboarding SET step = 'finish', previous_step = 'settings';
-- =====================================================================
-- GRUPPEN (müssen VOR groupMember existieren)
-- =====================================================================
-- OIDC-ADMIN GRUPPE
INSERT OR IGNORE INTO "group" (id, name, owner_id, position)
VALUES ('group-oidc-admins', '{{ oidc_admin_group | default("homarr-admins") }}', NULL, 0);
INSERT OR IGNORE INTO groupPermission (group_id, permission)
VALUES
('group-oidc-admins', 'admin'),
('group-oidc-admins', 'board-create'),
('group-oidc-admins', 'board-full-access'),
('group-oidc-admins', 'integration-create'),
('group-oidc-admins', 'integration-full-access');
-- CREDENTIALS-ADMIN GRUPPE
INSERT OR IGNORE INTO "group" (id, name, owner_id, position)
VALUES ('group-credentials-admin', 'credentials-admin', NULL, 1);
INSERT OR IGNORE INTO groupPermission (group_id, permission)
VALUES
('group-credentials-admin', 'admin'),
('group-credentials-admin', 'board-create'),
('group-credentials-admin', 'board-full-access'),
('group-credentials-admin', 'integration-create'),
('group-credentials-admin', 'integration-full-access');
-- =====================================================================
-- LOKALER ADMIN USER (Passwort wird via CLI gesetzt)
-- =====================================================================
INSERT OR IGNORE INTO user (id, name, email, password, salt, email_verified, provider)
VALUES (
'user-local-admin',
'{{ homarr_admin_username | default("admin") }}',
'{{ homarr_admin_email | default("admin@digitalboard.ch") }}',
'{{ bcrypt_output.stdout_lines[1] }}',
'{{ bcrypt_output.stdout_lines[0] }}',
1,
'credentials'
);
-- ADMIN-USER DEN GRUPPEN ZUWEISEN (Gruppen existieren jetzt)
INSERT OR IGNORE INTO groupMember (group_id, user_id)
VALUES
('group-credentials-admin', 'user-local-admin'),
('group-oidc-admins', 'user-local-admin');
-- =====================================================================
-- BOARD
-- =====================================================================
INSERT OR IGNORE INTO board (
id, name, is_public,
primary_color, secondary_color, opacity,
background_image_attachment, background_image_repeat, background_image_size,
item_radius, disable_status
)
VALUES (
'board-default',
'{{ default_board_name | default("Dashboard") }}',
{% if default_board_public | default(true) %}1{% else %}0{% endif %},
'#fa5252',
'#fd7e14',
100,
'fixed',
'no-repeat',
'cover',
'lg',
0
);
-- LAYOUTS
INSERT OR IGNORE INTO layout (id, name, board_id, column_count, breakpoint)
VALUES
('layout-desktop', 'Desktop', 'board-default', 10, 0),
('layout-tablet', 'Tablet', 'board-default', 6, 768),
('layout-mobile', 'Mobile', 'board-default', 2, 480);
-- HOME BOARD FÜR ADMIN SETZEN (Board existiert jetzt)
UPDATE user SET home_board_id = 'board-default', mobile_home_board_id = 'board-default'
WHERE id = 'user-local-admin';
-- SEKTION
DELETE FROM section_layout WHERE section_id = 'section-apps';
DELETE FROM item_layout WHERE section_id = 'section-apps';
DELETE FROM section WHERE id = 'section-apps';
INSERT INTO section (id, board_id, kind, x_offset, y_offset, name, options)
VALUES (
'section-apps',
'board-default',
'empty',
0,
0,
'Anwendungen',
'{"json": {}}'
);
INSERT OR REPLACE INTO section_layout (section_id, layout_id, parent_section_id, x_offset, y_offset, width, height)
VALUES
('section-apps', 'layout-desktop', NULL, 0, 0, 10, 3),
('section-apps', 'layout-tablet', NULL, 0, 0, 6, 4),
('section-apps', 'layout-mobile', NULL, 0, 0, 2, 6);
-- BOARD-BERECHTIGUNG
INSERT OR IGNORE INTO boardGroupPermission (board_id, group_id, permission)
VALUES
('board-default', 'group-oidc-admins', 'full-access'),
('board-default', 'group-credentials-admin', 'full-access');
-- =====================================================================
-- APPS
-- =====================================================================
-- Nextcloud
INSERT OR IGNORE INTO app (id, name, description, icon_url, href)
VALUES (
'app-nextcloud',
'Nextcloud',
'Cloud Storage & Collaboration',
'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/nextcloud.png',
'https://cloud.digitalboard.ch'
);
INSERT OR IGNORE INTO item (id, board_id, kind, options, advanced_options)
VALUES (
'item-nextcloud',
'board-default',
'app',
'{"json": {"appId": "app-nextcloud"}}',
'{"json": {}}'
);
INSERT OR REPLACE INTO item_layout (item_id, section_id, layout_id, x_offset, y_offset, width, height)
VALUES
('item-nextcloud', 'section-apps', 'layout-desktop', 0, 0, 2, 1),
('item-nextcloud', 'section-apps', 'layout-tablet', 0, 0, 2, 1),
('item-nextcloud', 'section-apps', 'layout-mobile', 0, 0, 1, 1);
-- Keycloak
INSERT OR IGNORE INTO app (id, name, description, icon_url, href)
VALUES (
'app-keycloak',
'Keycloak',
'Identity & Access Management',
'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/keycloak.png',
'https://auth.digitalboard.ch'
);
INSERT OR IGNORE INTO item (id, board_id, kind, options, advanced_options)
VALUES (
'item-keycloak',
'board-default',
'app',
'{"json": {"appId": "app-keycloak"}}',
'{"json": {}}'
);
INSERT OR REPLACE INTO item_layout (item_id, section_id, layout_id, x_offset, y_offset, width, height)
VALUES
('item-keycloak', 'section-apps', 'layout-desktop', 2, 0, 2, 1),
('item-keycloak', 'section-apps', 'layout-tablet', 2, 0, 2, 1),
('item-keycloak', 'section-apps', 'layout-mobile', 1, 0, 1, 1);
-- Mailman
INSERT OR IGNORE INTO app (id, name, description, icon_url, href)
VALUES (
'app-mailman',
'Mailman',
'Mailing List Manager',
'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/mailman.png',
'https://lists.digitalboard.ch'
);
INSERT OR IGNORE INTO item (id, board_id, kind, options, advanced_options)
VALUES (
'item-mailman',
'board-default',
'app',
'{"json": {"appId": "app-mailman"}}',
'{"json": {}}'
);
INSERT OR REPLACE INTO item_layout (item_id, section_id, layout_id, x_offset, y_offset, width, height)
VALUES
('item-mailman', 'section-apps', 'layout-desktop', 4, 0, 2, 1),
('item-mailman', 'section-apps', 'layout-tablet', 4, 0, 2, 1),
('item-mailman', 'section-apps', 'layout-mobile', 0, 1, 1, 1);
SEEDSQL
args:
executable: /bin/bash
register: seed_result register: seed_result
changed_when: seed_result.rc == 0 changed_when: seed_result.rc == 0
when: onboarding_status.stdout is not defined or 'finish' not in onboarding_status.stdout when: admin_exists.stdout == ""
notify: restart homarr
# =====================================================================
# 5. RESTART, HEALTH-CHECK, DANN CLI
# =====================================================================
- name: Restart Homarr to apply database changes
ansible.builtin.shell:
cmd: docker compose restart
chdir: "{{ homarr_docker_compose_dir }}"
when: seed_result is changed
- name: Wait for Homarr to be ready
ansible.builtin.shell:
cmd: docker compose exec -T {{ homarr_service_name }} wget -qO /dev/null "http://localhost:7575" 2>&1
chdir: "{{ homarr_docker_compose_dir }}"
retries: 30
delay: 5
register: homarr_ready
until: homarr_ready.rc == 0
changed_when: false
- name: Display completion message
ansible.builtin.debug:
msg: |
============================================================
Homarr deployed!
URL: {{ homarr_base_url }}
Admin-User: {{ homarr_admin_username }}
Admin-Passwort: {{ homarr_admin_password }}
(Bitte sofort ändern!)
============================================================
when: not db_exists.stat.exists
# =====================================================================
# 6. ABSCHLUSS
# =====================================================================
- name: Display admin credentials
ansible.builtin.debug:
msg: |
============================================
LOKALER ADMIN PASSWORT (bitte sofort ändern!)
{{ admin_password_output.stdout }}
============================================
when:
- admin_password_output is defined
- admin_password_output.stdout is defined
- admin_password_output.stdout | length > 0
- name: Display completion message
ansible.builtin.debug:
msg: |
============================================================
Homarr wurde erfolgreich deployed!
============================================================
URL: {{ homarr_base_url }}
OIDC Konfiguration:
- Issuer: {{ oidc_issuer }}
- Client ID: {{ oidc_client_id }}
- Admin-Gruppe: {{ oidc_admin_group }}
Nächste Schritte:
1. Stelle sicher, dass in Keycloak die Gruppe "{{ oidc_admin_group }}" existiert
2. Füge deinen User zur Gruppe "{{ oidc_admin_group }}" hinzu
3. Öffne {{ homarr_base_url }}
4. Du solltest automatisch via OIDC eingeloggt werden
Das Standard-Board "{{ default_board_name }}" wurde erstellt mit:
- Nextcloud App
- Keycloak App
- Mailman App
Weitere Apps kannst du direkt in der Homarr UI hinzufügen.
============================================================

View file

@ -1,9 +1,9 @@
#---------------------------------------------------------------------# #---------------------------------------------------------------------#
# Homarr - A simple, yet powerful dashboard for your server. # # Homarr A simple, yet powerful dashboard for your server. #
#---------------------------------------------------------------------# #---------------------------------------------------------------------#
services: services:
{{ homarr_service_name }}: homarr:
container_name: {{ homarr_service_name }} container_name: homarr
image: {{ homarr_image }} image: {{ homarr_image }}
restart: unless-stopped restart: unless-stopped
volumes: volumes:
@ -16,28 +16,27 @@ services:
BASE_URL: "{{ homarr_base_url }}" BASE_URL: "{{ homarr_base_url }}"
NEXTAUTH_URL: "{{ homarr_base_url }}" NEXTAUTH_URL: "{{ homarr_base_url }}"
SECRET_ENCRYPTION_KEY: "{{ homarr_secret_encryption_key }}" SECRET_ENCRYPTION_KEY: "{{ homarr_secret_encryption_key }}"
# Auth: Credentials + OIDC AUTH_PROVIDERS: "{{ homarr_auth_providers }}"
AUTH_PROVIDERS: "credentials,oidc" AUTH_OIDC_ISSUER: "{{ homarr_oidc_issuer }}"
AUTH_OIDC_ISSUER: "{{ oidc_issuer }}" AUTH_OIDC_CLIENT_ID: "{{ homarr_oidc_client_id }}"
AUTH_OIDC_CLIENT_ID: "{{ oidc_client_id }}" AUTH_OIDC_CLIENT_SECRET: "{{ homarr_oidc_client_secret }}"
AUTH_OIDC_CLIENT_SECRET: "{{ oidc_client_secret }}" AUTH_OIDC_CLIENT_NAME: "{{ homarr_oidc_client_name | default('Keycloak') }}"
AUTH_OIDC_CLIENT_NAME: "{{ oidc_client_name | default('Keycloak') }}" AUTH_OIDC_SCOPE_OVERWRITE: "{{ homarr_oidc_scopes | default('openid email profile groups') }}"
AUTH_OIDC_SCOPE_OVERWRITE: "{{ oidc_scopes | default('openid email profile groups') }}" AUTH_OIDC_GROUPS_ATTRIBUTE: "{{ homarr_oidc_groups_attribute | default('groups') }}"
AUTH_OIDC_GROUPS_ATTRIBUTE: "{{ oidc_groups_attribute | default('groups') }}" AUTH_OIDC_AUTO_LOGIN: "{{ homarr_oidc_auto_login | default('false') }}"
AUTH_OIDC_AUTO_LOGIN: "{{ oidc_auto_login | default('false') }}"
networks: networks:
- {{ homarr_traefik_network }} - {{ homarr_traefik_network }}
labels: labels:
- traefik.enable=true - traefik.enable=true
- traefik.docker.network={{ homarr_traefik_network }} - traefik.docker.network={{ homarr_traefik_network }}
- traefik.http.routers.{{ homarr_service_name }}.rule=Host(`{{ homarr_domain }}`) - traefik.http.routers.homarr.rule=Host(`{{ homarr_domain }}`)
{% if homarr_use_ssl %} {% if homarr_use_ssl %}
- traefik.http.routers.{{ homarr_service_name }}.entrypoints=websecure - traefik.http.routers.homarr.entrypoints=websecure
- traefik.http.routers.{{ homarr_service_name }}.tls=true - traefik.http.routers.homarr.tls=true
{% else %} {% else %}
- traefik.http.routers.{{ homarr_service_name }}.entrypoints=web - traefik.http.routers.homarr.entrypoints=web
{% endif %} {% endif %}
- traefik.http.services.{{ homarr_service_name }}.loadbalancer.server.port={{ homarr_port }} - traefik.http.services.homarr.loadbalancer.server.port={{ homarr_port }}
networks: networks:
{{ homarr_traefik_network }}: {{ homarr_traefik_network }}:
external: true external: true

View file

@ -0,0 +1,211 @@
{#-
Auto-layout packing macro.
Greedy left-to-right packing of apps into a grid with `cols` columns.
Returns the list of apps with computed x/y/w/h fields.
Width is clamped to cols (so an app wider than the grid is downsized
rather than overflowing). Height is taken as-is.
-#}
{%- macro pack(apps, cols) -%}
{%- set ns = namespace(x=0, y=0, row_h=0, out=[]) -%}
{%- for app in apps -%}
{%- set w = [app.width, cols] | min -%}
{%- set h = app.height | default(1) -%}
{%- if ns.x + w > cols -%}
{%- set ns.x = 0 -%}
{%- set ns.y = ns.y + ns.row_h -%}
{%- set ns.row_h = 0 -%}
{%- endif -%}
{%- set _ = ns.out.append({'id': app.id, 'x': ns.x, 'y': ns.y, 'w': w, 'h': h}) -%}
{%- set ns.x = ns.x + w -%}
{%- if h > ns.row_h -%}
{%- set ns.row_h = h -%}
{%- endif -%}
{%- endfor -%}
{{- ns.out | to_json -}}
{%- endmacro -%}
{%- set desktop_layout = pack(homarr_apps, 10) | from_json -%}
{%- set tablet_layout = pack(homarr_apps, 6) | from_json -%}
{%- set mobile_layout = pack(homarr_apps, 2) | from_json -%}
BEGIN TRANSACTION;
-- =====================================================================
-- SERVER SETTINGS
-- =====================================================================
INSERT OR REPLACE INTO serverSetting (setting_key, value)
VALUES
('analytics', '{"json": {"enableGeneral": false, "enableWidgetData": false, "enableIntegrationData": false, "enableUserData": false}}'),
('culture', '{"json": {"defaultLocale": "de"}}'),
('crawling', '{"json": {"crawlingEnabled": false}}'),
('board', '{"json": {"homeBoardId": "board-default", "mobileHomeBoardId": "board-default", "enableStatusByDefault": true, "forceDisableStatus": false, "defaultBoardId": "board-default"}}');
-- Skip onboarding wizard
UPDATE onboarding SET step = 'finish', previous_step = 'settings';
-- =====================================================================
-- GROUPS (must exist before groupMember)
-- =====================================================================
-- OIDC admin group
INSERT OR IGNORE INTO "group" (id, name, owner_id, position)
VALUES ('group-oidc-admins', '{{ homarr_oidc_admin_group }}', NULL, 0);
INSERT OR IGNORE INTO groupPermission (group_id, permission)
VALUES
('group-oidc-admins', 'admin'),
('group-oidc-admins', 'board-create'),
('group-oidc-admins', 'board-full-access'),
('group-oidc-admins', 'integration-create'),
('group-oidc-admins', 'integration-full-access');
-- Credentials admin group
INSERT OR IGNORE INTO "group" (id, name, owner_id, position)
VALUES ('group-credentials-admin', 'credentials-admin', NULL, 1);
INSERT OR IGNORE INTO groupPermission (group_id, permission)
VALUES
('group-credentials-admin', 'admin'),
('group-credentials-admin', 'board-create'),
('group-credentials-admin', 'board-full-access'),
('group-credentials-admin', 'integration-create'),
('group-credentials-admin', 'integration-full-access');
-- =====================================================================
-- LOCAL ADMIN USER
-- =====================================================================
INSERT OR IGNORE INTO user (id, name, email, password, email_verified, provider)
VALUES (
'user-local-admin',
'{{ homarr_admin_username }}',
'{{ homarr_admin_email }}',
'{{ homarr_bcrypt_hash }}',
1,
'credentials'
);
-- Assign admin user to groups
INSERT OR IGNORE INTO groupMember (group_id, user_id)
VALUES
('group-credentials-admin', 'user-local-admin'),
('group-oidc-admins', 'user-local-admin');
-- =====================================================================
-- BOARD
-- =====================================================================
INSERT OR IGNORE INTO board (
id, name, is_public,
primary_color, secondary_color, opacity,
background_image_attachment, background_image_repeat, background_image_size,
item_radius, disable_status
)
VALUES (
'board-default',
'{{ homarr_default_board_name }}',
{% if homarr_default_board_public %}1{% else %}0{% endif %},
'#fa5252',
'#fd7e14',
100,
'fixed',
'no-repeat',
'cover',
'lg',
0
);
-- Layouts
INSERT OR IGNORE INTO layout (id, name, board_id, column_count, breakpoint)
VALUES
('layout-desktop', 'Desktop', 'board-default', 10, 0),
('layout-tablet', 'Tablet', 'board-default', 6, 768),
('layout-mobile', 'Mobile', 'board-default', 2, 480);
-- Set home board for admin user (board exists now)
UPDATE user SET home_board_id = 'board-default', mobile_home_board_id = 'board-default'
WHERE id = 'user-local-admin';
-- =====================================================================
-- SECTION
-- =====================================================================
DELETE FROM section_layout WHERE section_id = 'section-apps';
DELETE FROM item_layout WHERE section_id = 'section-apps';
DELETE FROM section WHERE id = 'section-apps';
INSERT INTO section (id, board_id, kind, x_offset, y_offset, name, options)
VALUES (
'section-apps',
'board-default',
'empty',
0,
0,
'Applications',
'{"json": {}}'
);
INSERT OR REPLACE INTO section_layout (section_id, layout_id, parent_section_id, x_offset, y_offset, width, height)
VALUES
('section-apps', 'layout-desktop', NULL, 0, 0, 10, 3),
('section-apps', 'layout-tablet', NULL, 0, 0, 6, 4),
('section-apps', 'layout-mobile', NULL, 0, 0, 2, 6);
-- Board permissions
INSERT OR IGNORE INTO boardGroupPermission (board_id, group_id, permission)
VALUES
('board-default', 'group-oidc-admins', 'full-access'),
('board-default', 'group-credentials-admin', 'full-access');
-- =====================================================================
-- APPS (auto-generated from homarr_apps variable)
-- =====================================================================
{% if homarr_apps | length > 0 %}
{% for app in homarr_apps %}
INSERT OR IGNORE INTO app (id, name, description, icon_url, href)
VALUES (
'app-{{ app.id }}',
'{{ app.name }}',
'{{ app.description | default("") }}',
'{{ app.icon }}',
'{{ app.href }}'
);
INSERT OR IGNORE INTO item (id, board_id, kind, options, advanced_options)
VALUES (
'item-{{ app.id }}',
'board-default',
'app',
'{"json": {"appId": "app-{{ app.id }}"}}',
'{"json": {}}'
);
{% endfor %}
INSERT OR REPLACE INTO item_layout (item_id, section_id, layout_id, x_offset, y_offset, width, height)
VALUES
{% for entry in desktop_layout %}
('item-{{ entry.id }}', 'section-apps', 'layout-desktop', {{ entry.x }}, {{ entry.y }}, {{ entry.w }}, {{ entry.h }}){% if not loop.last %},{% endif %}
{% endfor %}
;
INSERT OR REPLACE INTO item_layout (item_id, section_id, layout_id, x_offset, y_offset, width, height)
VALUES
{% for entry in tablet_layout %}
('item-{{ entry.id }}', 'section-apps', 'layout-tablet', {{ entry.x }}, {{ entry.y }}, {{ entry.w }}, {{ entry.h }}){% if not loop.last %},{% endif %}
{% endfor %}
;
INSERT OR REPLACE INTO item_layout (item_id, section_id, layout_id, x_offset, y_offset, width, height)
VALUES
{% for entry in mobile_layout %}
('item-{{ entry.id }}', 'section-apps', 'layout-mobile', {{ entry.x }}, {{ entry.y }}, {{ entry.w }}, {{ entry.h }}){% if not loop.last %},{% endif %}
{% endfor %}
;
{% endif %}
COMMIT;