Compare commits
14 commits
a6f301ee54
...
30f3c16b59
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30f3c16b59 | ||
|
|
fb81f60f9d | ||
|
|
48d12a1b4a | ||
| 03af64ca2c | |||
| 53e80ad7be | |||
| 78095cca1d | |||
| 61193e26f4 | |||
| 27ed51ee95 | |||
| e0cb1ac68c | |||
| bbbd1c8940 | |||
| 1c7ecabcaf | |||
| 422b196831 | |||
| d3bdb1fdec | |||
| 029b1a86d4 |
11 changed files with 652 additions and 229 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,3 +1,5 @@
|
||||||
/.idea/
|
/.idea/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
plugins/lookup/__pycache__/
|
plugins/lookup/__pycache__/
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ and customizable application tiles.
|
||||||
- a local admin user with bcrypt-hashed password
|
- a local admin user with bcrypt-hashed password
|
||||||
- OIDC and credentials admin groups with full permissions
|
- OIDC and credentials admin groups with full permissions
|
||||||
- application tiles defined in `homarr_apps`, auto-laid-out across all
|
- application tiles defined in `homarr_apps`, auto-laid-out across all
|
||||||
three screen sizes
|
three screen sizes via the bundled `homarr_compute_layouts` filter
|
||||||
- Skips the onboarding wizard so the instance is usable right after deploy
|
- Skips the onboarding wizard so the instance is usable right after deploy
|
||||||
- Restarts the container via handler when the seed or compose file changes
|
- Restarts the container via handler when the seed or compose file changes
|
||||||
|
|
||||||
|
|
@ -100,6 +100,53 @@ homarr_apps:
|
||||||
width: 2
|
width: 2
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Layout filter plugin
|
||||||
|
|
||||||
|
The grid-packing algorithm that places tiles on the desktop, tablet
|
||||||
|
and mobile layouts lives in `filter_plugins/homarr_layout.py` rather
|
||||||
|
than inside the Jinja seed template. This keeps the SQL template
|
||||||
|
readable and lets the algorithm be unit-tested in isolation.
|
||||||
|
|
||||||
|
The filter is invoked once from `tasks/main.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Compute Homarr app layouts
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
homarr_layout: "{{ homarr_apps | homarr_compute_layouts }}"
|
||||||
|
```
|
||||||
|
|
||||||
|
This produces a `homarr_layout` fact with two keys, both consumed by
|
||||||
|
`templates/homarr_seed.sql.j2`:
|
||||||
|
|
||||||
|
| Key | Shape | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `apps` | list, same order as `homarr_apps` | each entry enriched with `desktop`, `tablet`, `mobile` sub-dicts of `{x, y, w, h}` |
|
||||||
|
| `section_height` | dict with `desktop`, `tablet`, `mobile` | minimum height of the parent section so all tiles fit |
|
||||||
|
|
||||||
|
The filter signature accepts custom column counts if Homarr ever
|
||||||
|
changes the breakpoint widths:
|
||||||
|
|
||||||
|
```jinja
|
||||||
|
{{ homarr_apps | homarr_compute_layouts(desktop_cols=12, tablet_cols=8, mobile_cols=4) }}
|
||||||
|
```
|
||||||
|
|
||||||
|
To debug a layout without running the full deploy, run the play with
|
||||||
|
`-vv` — the `Show computed app layouts` task dumps the full
|
||||||
|
`homarr_layout` fact.
|
||||||
|
|
||||||
|
### Running the filter tests
|
||||||
|
|
||||||
|
The filter is covered by unit tests in
|
||||||
|
`filter_plugins/tests/test_homarr_layout.py`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install pytest ansible-core
|
||||||
|
pytest filter_plugins/tests/
|
||||||
|
```
|
||||||
|
|
||||||
|
15 tests cover packing, width clamping, height/section-height,
|
||||||
|
input validation and custom grid sizes.
|
||||||
|
|
||||||
## First login
|
## First login
|
||||||
|
|
||||||
After the role completes, log in at `{{ homarr_base_url }}` with:
|
After the role completes, log in at `{{ homarr_base_url }}` with:
|
||||||
|
|
|
||||||
140
roles/homarr/filter_plugins/homarr_layout.py
Normal file
140
roles/homarr/filter_plugins/homarr_layout.py
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: MIT-0
|
||||||
|
|
||||||
|
"""Custom Ansible filter plugin for computing Homarr grid layouts.
|
||||||
|
|
||||||
|
The Homarr SQL seed needs item_layout rows for three breakpoints
|
||||||
|
(desktop / tablet / mobile). Rather than embedding the packing
|
||||||
|
algorithm in Jinja with namespace gymnastics, this filter does the
|
||||||
|
computation in Python and hands the seed template a ready-to-render
|
||||||
|
data structure.
|
||||||
|
|
||||||
|
Usage in tasks:
|
||||||
|
|
||||||
|
- name: Compute Homarr app layouts
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
homarr_layout: "{{ homarr_apps | homarr_compute_layouts }}"
|
||||||
|
|
||||||
|
The result is a dict with two keys:
|
||||||
|
|
||||||
|
apps — original homarr_apps in order, each enriched with
|
||||||
|
'desktop', 'tablet', 'mobile' sub-dicts of
|
||||||
|
{'x', 'y', 'w', 'h'} ready for SQL templating.
|
||||||
|
section_height — dict with 'desktop', 'tablet', 'mobile' keys
|
||||||
|
giving the minimum height (in grid cells) the
|
||||||
|
parent section must have to fit all tiles.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from ansible.errors import AnsibleFilterError
|
||||||
|
|
||||||
|
|
||||||
|
def _pack(apps, cols):
|
||||||
|
"""Greedy left-to-right packing into a fixed-column grid.
|
||||||
|
|
||||||
|
Width values larger than the grid are clamped to the grid width
|
||||||
|
rather than overflowing — so a tile declared with width=8 still
|
||||||
|
renders on the 6-column tablet grid (as a full-width tile) and on
|
||||||
|
the 2-column mobile grid (as a full-width tile) without breaking
|
||||||
|
the layout.
|
||||||
|
|
||||||
|
Returns (placements, total_height) where placements is a list of
|
||||||
|
{'id', 'x', 'y', 'w', 'h'} dicts, one per input app, in the same
|
||||||
|
order. total_height is the y-coordinate of the bottom of the last
|
||||||
|
occupied row (i.e. max(y + h) across placements).
|
||||||
|
"""
|
||||||
|
x = 0
|
||||||
|
y = 0
|
||||||
|
row_h = 0
|
||||||
|
max_y = 0
|
||||||
|
placements = []
|
||||||
|
|
||||||
|
for app in apps:
|
||||||
|
w = min(int(app.get('width', 1)), cols)
|
||||||
|
h = int(app.get('height', 1))
|
||||||
|
|
||||||
|
# Wrap to the next row when the tile would overflow the grid.
|
||||||
|
if x + w > cols:
|
||||||
|
x = 0
|
||||||
|
y += row_h
|
||||||
|
row_h = 0
|
||||||
|
|
||||||
|
placements.append({
|
||||||
|
'id': app['id'],
|
||||||
|
'x': x,
|
||||||
|
'y': y,
|
||||||
|
'w': w,
|
||||||
|
'h': h,
|
||||||
|
})
|
||||||
|
|
||||||
|
x += w
|
||||||
|
if h > row_h:
|
||||||
|
row_h = h
|
||||||
|
if y + h > max_y:
|
||||||
|
max_y = y + h
|
||||||
|
|
||||||
|
return placements, max_y
|
||||||
|
|
||||||
|
|
||||||
|
def homarr_compute_layouts(apps, desktop_cols=10, tablet_cols=6,
|
||||||
|
mobile_cols=2):
|
||||||
|
"""Compute responsive layouts for a list of Homarr apps.
|
||||||
|
|
||||||
|
Input validation is intentionally strict — a malformed apps list
|
||||||
|
should fail the play with a clear message rather than produce a
|
||||||
|
broken SQL seed and a silently misconfigured dashboard.
|
||||||
|
|
||||||
|
Note: uniqueness of app ids is NOT checked here. The role's
|
||||||
|
`Validate homarr_apps have unique ids` assert task runs earlier
|
||||||
|
and is the single source of truth for that check.
|
||||||
|
"""
|
||||||
|
if not isinstance(apps, list):
|
||||||
|
raise AnsibleFilterError(
|
||||||
|
"homarr_compute_layouts: expected a list of apps, "
|
||||||
|
"got {0}".format(type(apps).__name__)
|
||||||
|
)
|
||||||
|
|
||||||
|
for index, app in enumerate(apps):
|
||||||
|
if not isinstance(app, dict):
|
||||||
|
raise AnsibleFilterError(
|
||||||
|
"homarr_compute_layouts: app at index {0} is not a "
|
||||||
|
"dict (got {1})".format(index, type(app).__name__)
|
||||||
|
)
|
||||||
|
for required in ('id', 'width'):
|
||||||
|
if required not in app:
|
||||||
|
raise AnsibleFilterError(
|
||||||
|
"homarr_compute_layouts: app at index {0} is "
|
||||||
|
"missing required key '{1}'".format(index, required)
|
||||||
|
)
|
||||||
|
|
||||||
|
desktop, h_desktop = _pack(apps, desktop_cols)
|
||||||
|
tablet, h_tablet = _pack(apps, tablet_cols)
|
||||||
|
mobile, h_mobile = _pack(apps, mobile_cols)
|
||||||
|
|
||||||
|
enriched = []
|
||||||
|
for src, d, t, m in zip(apps, desktop, tablet, mobile):
|
||||||
|
enriched.append({
|
||||||
|
**src,
|
||||||
|
'desktop': {'x': d['x'], 'y': d['y'], 'w': d['w'], 'h': d['h']},
|
||||||
|
'tablet': {'x': t['x'], 'y': t['y'], 'w': t['w'], 'h': t['h']},
|
||||||
|
'mobile': {'x': m['x'], 'y': m['y'], 'w': m['w'], 'h': m['h']},
|
||||||
|
})
|
||||||
|
|
||||||
|
# Floor section height at 1 so the section stays visible even
|
||||||
|
# when homarr_apps is empty.
|
||||||
|
return {
|
||||||
|
'apps': enriched,
|
||||||
|
'section_height': {
|
||||||
|
'desktop': max(h_desktop, 1),
|
||||||
|
'tablet': max(h_tablet, 1),
|
||||||
|
'mobile': max(h_mobile, 1),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class FilterModule(object):
|
||||||
|
"""Ansible filter plugin entry point."""
|
||||||
|
|
||||||
|
def filters(self):
|
||||||
|
return {
|
||||||
|
'homarr_compute_layouts': homarr_compute_layouts,
|
||||||
|
}
|
||||||
178
roles/homarr/filter_plugins/tests/test_homarr_layout.py
Normal file
178
roles/homarr/filter_plugins/tests/test_homarr_layout.py
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: MIT-0
|
||||||
|
|
||||||
|
"""Unit tests for the homarr_layout filter plugin.
|
||||||
|
|
||||||
|
Run from the role root:
|
||||||
|
|
||||||
|
pytest filter_plugins/tests/
|
||||||
|
|
||||||
|
Requires `pytest` and `ansible-core` in the environment.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Make the filter importable without having Ansible auto-discovery in
|
||||||
|
# the way (it would only run during a real `ansible-playbook` invocation).
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||||
|
|
||||||
|
import pytest # noqa: E402
|
||||||
|
|
||||||
|
from ansible.errors import AnsibleFilterError # noqa: E402
|
||||||
|
from homarr_layout import homarr_compute_layouts # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def _app(app_id, width, height=1):
|
||||||
|
"""Build a minimal app dict for tests."""
|
||||||
|
return {
|
||||||
|
'id': app_id,
|
||||||
|
'name': app_id.title(),
|
||||||
|
'icon': 'https://example.com/{0}.png'.format(app_id),
|
||||||
|
'href': 'https://{0}.example.com'.format(app_id),
|
||||||
|
'width': width,
|
||||||
|
'height': height,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------
|
||||||
|
# Happy-path packing
|
||||||
|
# ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
def test_empty_apps_returns_empty_list_and_min_height():
|
||||||
|
result = homarr_compute_layouts([])
|
||||||
|
assert result['apps'] == []
|
||||||
|
# Even an empty grid keeps section_height >= 1 so the section
|
||||||
|
# renders in the UI.
|
||||||
|
assert result['section_height'] == {
|
||||||
|
'desktop': 1, 'tablet': 1, 'mobile': 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_single_app_positioned_at_origin_in_all_grids():
|
||||||
|
result = homarr_compute_layouts([_app('a', width=2)])
|
||||||
|
a = result['apps'][0]
|
||||||
|
assert a['desktop'] == {'x': 0, 'y': 0, 'w': 2, 'h': 1}
|
||||||
|
assert a['tablet'] == {'x': 0, 'y': 0, 'w': 2, 'h': 1}
|
||||||
|
assert a['mobile'] == {'x': 0, 'y': 0, 'w': 2, 'h': 1}
|
||||||
|
|
||||||
|
|
||||||
|
def test_original_app_keys_are_preserved():
|
||||||
|
apps = [_app('nextcloud', width=2)]
|
||||||
|
apps[0]['description'] = 'Cloud Storage'
|
||||||
|
result = homarr_compute_layouts(apps)
|
||||||
|
a = result['apps'][0]
|
||||||
|
# Original fields survive the layout enrichment.
|
||||||
|
assert a['name'] == 'Nextcloud'
|
||||||
|
assert a['description'] == 'Cloud Storage'
|
||||||
|
assert a['icon'] == 'https://example.com/nextcloud.png'
|
||||||
|
|
||||||
|
|
||||||
|
def test_desktop_wraps_after_filling_row():
|
||||||
|
# 5 apps of width 2 fill the 10-col desktop row exactly; 6th wraps.
|
||||||
|
apps = [_app('a{0}'.format(i), width=2) for i in range(6)]
|
||||||
|
result = homarr_compute_layouts(apps)
|
||||||
|
assert result['apps'][4]['desktop'] == {'x': 8, 'y': 0, 'w': 2, 'h': 1}
|
||||||
|
assert result['apps'][5]['desktop'] == {'x': 0, 'y': 1, 'w': 2, 'h': 1}
|
||||||
|
|
||||||
|
|
||||||
|
def test_mobile_wraps_after_every_app():
|
||||||
|
# Mobile is only 2 cols wide → every app of width 2 starts a new row.
|
||||||
|
apps = [_app('a{0}'.format(i), width=2) for i in range(3)]
|
||||||
|
result = homarr_compute_layouts(apps)
|
||||||
|
assert [a['mobile']['y'] for a in result['apps']] == [0, 1, 2]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------
|
||||||
|
# Width clamping
|
||||||
|
# ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
def test_width_clamped_per_grid():
|
||||||
|
result = homarr_compute_layouts([_app('big', width=8)])
|
||||||
|
# Desktop has room (8 <= 10), tablet clamps to 6, mobile clamps to 2.
|
||||||
|
a = result['apps'][0]
|
||||||
|
assert a['desktop']['w'] == 8
|
||||||
|
assert a['tablet']['w'] == 6
|
||||||
|
assert a['mobile']['w'] == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_width_larger_than_desktop_still_clamps():
|
||||||
|
# A pathological width=20 still works — it just becomes a full-width
|
||||||
|
# tile on every grid.
|
||||||
|
result = homarr_compute_layouts([_app('huge', width=20)])
|
||||||
|
a = result['apps'][0]
|
||||||
|
assert a['desktop']['w'] == 10
|
||||||
|
assert a['tablet']['w'] == 6
|
||||||
|
assert a['mobile']['w'] == 2
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------
|
||||||
|
# Height handling
|
||||||
|
# ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
def test_section_height_grows_with_rows():
|
||||||
|
# 6 apps of width 2 on desktop → 5 in row 1, 1 in row 2.
|
||||||
|
apps = [_app('a{0}'.format(i), width=2) for i in range(6)]
|
||||||
|
result = homarr_compute_layouts(apps)
|
||||||
|
assert result['section_height']['desktop'] == 2
|
||||||
|
# On mobile every app is on its own row.
|
||||||
|
assert result['section_height']['mobile'] == 6
|
||||||
|
|
||||||
|
|
||||||
|
def test_tall_app_extends_row_height():
|
||||||
|
apps = [
|
||||||
|
_app('tall', width=2, height=3),
|
||||||
|
_app('short', width=2, height=1),
|
||||||
|
]
|
||||||
|
result = homarr_compute_layouts(apps)
|
||||||
|
# Both fit in row 0 horizontally, but the section must be 3 tall.
|
||||||
|
assert result['section_height']['desktop'] == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_tall_app_pushes_subsequent_row_down():
|
||||||
|
# tall (h=3) fills full desktop width → next app wraps to y=3.
|
||||||
|
result = homarr_compute_layouts([
|
||||||
|
_app('tall', width=10, height=3),
|
||||||
|
_app('next', width=2, height=1),
|
||||||
|
])
|
||||||
|
assert result['apps'][1]['desktop'] == {'x': 0, 'y': 3, 'w': 2, 'h': 1}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------
|
||||||
|
# Input validation
|
||||||
|
# ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
def test_rejects_non_list_input():
|
||||||
|
with pytest.raises(AnsibleFilterError, match='expected a list'):
|
||||||
|
homarr_compute_layouts('not a list')
|
||||||
|
|
||||||
|
|
||||||
|
def test_rejects_non_dict_entry():
|
||||||
|
with pytest.raises(AnsibleFilterError, match='not a dict'):
|
||||||
|
homarr_compute_layouts(['just a string'])
|
||||||
|
|
||||||
|
|
||||||
|
def test_rejects_app_without_id():
|
||||||
|
with pytest.raises(AnsibleFilterError, match="missing required key 'id'"):
|
||||||
|
homarr_compute_layouts([{'name': 'no id', 'width': 2}])
|
||||||
|
|
||||||
|
|
||||||
|
def test_rejects_app_without_width():
|
||||||
|
with pytest.raises(AnsibleFilterError,
|
||||||
|
match="missing required key 'width'"):
|
||||||
|
homarr_compute_layouts([{'id': 'no-width', 'name': 'x'}])
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------
|
||||||
|
# Configurable grid sizes
|
||||||
|
# ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
def test_custom_grid_sizes():
|
||||||
|
# If Homarr ever switches to 12-col desktop, the filter still works.
|
||||||
|
result = homarr_compute_layouts(
|
||||||
|
[_app('a', width=4), _app('b', width=4), _app('c', width=4)],
|
||||||
|
desktop_cols=12, tablet_cols=8, mobile_cols=4,
|
||||||
|
)
|
||||||
|
# All three fit in desktop row 0 (4+4+4 = 12).
|
||||||
|
assert [a['desktop']['x'] for a in result['apps']] == [0, 4, 8]
|
||||||
|
assert result['section_height']['desktop'] == 1
|
||||||
|
|
@ -128,7 +128,23 @@
|
||||||
no_log: true
|
no_log: true
|
||||||
|
|
||||||
# =====================================================================
|
# =====================================================================
|
||||||
# 5. SEED DATABASE (only if local admin user does not exist yet)
|
# 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 | 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
|
- name: Check if local admin user exists
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,13 @@
|
||||||
{#-
|
{#-
|
||||||
Auto-layout packing macro.
|
Homarr database seed.
|
||||||
|
|
||||||
Greedy left-to-right packing of apps into a grid with `cols` columns.
|
The packing algorithm previously lived in this template as a Jinja
|
||||||
Returns the list of apps with computed x/y/w/h fields.
|
`pack()` macro with from_json/to_json round-trips. It has been
|
||||||
|
extracted to the `homarr_compute_layouts` filter plugin (see
|
||||||
Width is clamped to cols (so an app wider than the grid is downsized
|
filter_plugins/homarr_layout.py) and the result is provided as the
|
||||||
rather than overflowing). Height is taken as-is.
|
`homarr_layout` fact set in tasks/main.yml. This template therefore
|
||||||
|
only renders SQL — no logic.
|
||||||
-#}
|
-#}
|
||||||
{%- 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;
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
-- =====================================================================
|
-- =====================================================================
|
||||||
|
|
@ -148,11 +126,14 @@ VALUES (
|
||||||
'{"json": {}}'
|
'{"json": {}}'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- Section height is sized to fit the computed layout (see
|
||||||
|
-- homarr_compute_layouts filter). It grows automatically when more
|
||||||
|
-- apps or taller tiles are added.
|
||||||
INSERT OR REPLACE INTO section_layout (section_id, layout_id, parent_section_id, x_offset, y_offset, width, height)
|
INSERT OR REPLACE INTO section_layout (section_id, layout_id, parent_section_id, x_offset, y_offset, width, height)
|
||||||
VALUES
|
VALUES
|
||||||
('section-apps', 'layout-desktop', NULL, 0, 0, 10, 3),
|
('section-apps', 'layout-desktop', NULL, 0, 0, 10, {{ homarr_layout.section_height.desktop }}),
|
||||||
('section-apps', 'layout-tablet', NULL, 0, 0, 6, 4),
|
('section-apps', 'layout-tablet', NULL, 0, 0, 6, {{ homarr_layout.section_height.tablet }}),
|
||||||
('section-apps', 'layout-mobile', NULL, 0, 0, 2, 6);
|
('section-apps', 'layout-mobile', NULL, 0, 0, 2, {{ homarr_layout.section_height.mobile }});
|
||||||
|
|
||||||
-- Board permissions
|
-- Board permissions
|
||||||
INSERT OR IGNORE INTO boardGroupPermission (board_id, group_id, permission)
|
INSERT OR IGNORE INTO boardGroupPermission (board_id, group_id, permission)
|
||||||
|
|
@ -161,11 +142,11 @@ VALUES
|
||||||
('board-default', 'group-credentials-admin', 'full-access');
|
('board-default', 'group-credentials-admin', 'full-access');
|
||||||
|
|
||||||
-- =====================================================================
|
-- =====================================================================
|
||||||
-- APPS (auto-generated from homarr_apps variable)
|
-- APPS (positions pre-computed by homarr_compute_layouts filter)
|
||||||
-- =====================================================================
|
-- =====================================================================
|
||||||
|
|
||||||
{% if homarr_apps | length > 0 %}
|
{% for app in homarr_layout.apps %}
|
||||||
{% for app in homarr_apps %}
|
-- {{ app.name }}
|
||||||
INSERT OR IGNORE INTO app (id, name, description, icon_url, href)
|
INSERT OR IGNORE INTO app (id, name, description, icon_url, href)
|
||||||
VALUES (
|
VALUES (
|
||||||
'app-{{ app.id }}',
|
'app-{{ app.id }}',
|
||||||
|
|
@ -184,28 +165,11 @@ VALUES (
|
||||||
'{"json": {}}'
|
'{"json": {}}'
|
||||||
);
|
);
|
||||||
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
INSERT OR REPLACE INTO item_layout (item_id, section_id, layout_id, x_offset, y_offset, width, height)
|
INSERT OR REPLACE INTO item_layout (item_id, section_id, layout_id, x_offset, y_offset, width, height)
|
||||||
VALUES
|
VALUES
|
||||||
{% for entry in desktop_layout %}
|
('item-{{ app.id }}', 'section-apps', 'layout-desktop', {{ app.desktop.x }}, {{ app.desktop.y }}, {{ app.desktop.w }}, {{ app.desktop.h }}),
|
||||||
('item-{{ entry.id }}', 'section-apps', 'layout-desktop', {{ entry.x }}, {{ entry.y }}, {{ entry.w }}, {{ entry.h }}){% if not loop.last %},{% endif %}
|
('item-{{ app.id }}', 'section-apps', 'layout-tablet', {{ app.tablet.x }}, {{ app.tablet.y }}, {{ app.tablet.w }}, {{ app.tablet.h }}),
|
||||||
{% endfor %}
|
('item-{{ app.id }}', 'section-apps', 'layout-mobile', {{ app.mobile.x }}, {{ app.mobile.y }}, {{ app.mobile.w }}, {{ app.mobile.h }});
|
||||||
;
|
|
||||||
|
|
||||||
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 %}
|
{% 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;
|
COMMIT;
|
||||||
|
|
@ -25,18 +25,26 @@ opnform_redis_image: "redis:7"
|
||||||
opnform_db_image: "postgres:16"
|
opnform_db_image: "postgres:16"
|
||||||
opnform_ingress_image: "nginx:1"
|
opnform_ingress_image: "nginx:1"
|
||||||
|
|
||||||
# REQUIRED SECRETS — generate with: openssl rand -base64 32
|
# REQUIRED SECRETS — must be overridden per-inventory.
|
||||||
# Always prefix opnform_app_key with "base64:"
|
|
||||||
# Provide via OpenBao lookup, Ansible Vault or extra-vars.
|
# Provide via OpenBao lookup, Ansible Vault or extra-vars.
|
||||||
# Never commit real keys to version control.
|
# Never commit real keys to version control.
|
||||||
opnform_app_key: "base64:vsQw8EoC64nmhurLUUohXUlAeryaV6Y2Is64Tdvjlko="
|
#
|
||||||
opnform_jwt_secret: "0b2e8ed326334a08ce3846bfcd6588f5a11be33999e96963cd4eaff1a3ae828b"
|
# Generate with:
|
||||||
opnform_front_api_secret: "8f52397785a110b657f2a6beab13362877bfac936ae9002bc236c54ed1011b2d"
|
# opnform_app_key: echo "base64:$(openssl rand -base64 32)"
|
||||||
|
# opnform_jwt_secret: openssl rand -hex 32
|
||||||
|
# opnform_front_api_secret: openssl rand -hex 32
|
||||||
|
#
|
||||||
|
# opnform_app_key MUST start with the prefix "base64:" — the validate
|
||||||
|
# task at the top of tasks/main.yml enforces this.
|
||||||
|
opnform_app_key: ""
|
||||||
|
opnform_jwt_secret: ""
|
||||||
|
opnform_front_api_secret: ""
|
||||||
|
|
||||||
# Database credentials
|
# Database credentials. opnform_db_password must be overridden; the
|
||||||
|
# validate task fails fast on an empty value.
|
||||||
opnform_db_name: "opnform"
|
opnform_db_name: "opnform"
|
||||||
opnform_db_user: "opnform"
|
opnform_db_user: "opnform"
|
||||||
opnform_db_password: "xtNLUVc2ajcWictqWXWkLR"
|
opnform_db_password: ""
|
||||||
|
|
||||||
# Admin bootstrap — when email+password are set, the role creates the
|
# Admin bootstrap — when email+password are set, the role creates the
|
||||||
# first user via OpnForm's /api/register endpoint, skipping the
|
# first user via OpnForm's /api/register endpoint, skipping the
|
||||||
|
|
|
||||||
220
roles/opnform/meta/argument_specs.yml
Normal file
220
roles/opnform/meta/argument_specs.yml
Normal file
|
|
@ -0,0 +1,220 @@
|
||||||
|
---
|
||||||
|
argument_specs:
|
||||||
|
main:
|
||||||
|
short_description: Deploy OpnForm (api + ui + db + redis + ingress) via Docker Compose.
|
||||||
|
description:
|
||||||
|
- Renders a Compose stack for the full OpnForm setup (PHP-FPM api,
|
||||||
|
Nuxt ui, Postgres, Redis, nginx ingress) and exposes it through
|
||||||
|
Traefik.
|
||||||
|
- Optionally bootstraps the first admin user via the OpnForm
|
||||||
|
C(/api/register) endpoint (skipping the self-hosted setup page)
|
||||||
|
and provisions a single OIDC identity connection in the default
|
||||||
|
workspace via the workspace API. Both bootstraps are idempotent.
|
||||||
|
options:
|
||||||
|
docker_compose_base_dir:
|
||||||
|
type: path
|
||||||
|
default: /etc/docker/compose
|
||||||
|
docker_volume_base_dir:
|
||||||
|
type: path
|
||||||
|
default: /srv/data
|
||||||
|
opnform_service_name:
|
||||||
|
type: str
|
||||||
|
default: opnform
|
||||||
|
opnform_docker_compose_dir:
|
||||||
|
type: path
|
||||||
|
description: Defaults to C({{ docker_compose_base_dir }}/{{ opnform_service_name }}).
|
||||||
|
opnform_docker_volume_dir:
|
||||||
|
type: path
|
||||||
|
description: Defaults to C({{ docker_volume_base_dir }}/{{ opnform_service_name }}).
|
||||||
|
opnform_storage_dir:
|
||||||
|
type: path
|
||||||
|
description: OpnForm storage volume mounted into the api container.
|
||||||
|
opnform_db_data_dir:
|
||||||
|
type: path
|
||||||
|
opnform_redis_data_dir:
|
||||||
|
type: path
|
||||||
|
|
||||||
|
opnform_domain:
|
||||||
|
type: str
|
||||||
|
default: forms.local.test
|
||||||
|
description: Hostname used in the traefik Host rule.
|
||||||
|
opnform_base_url:
|
||||||
|
type: str
|
||||||
|
default: https://forms.local.test
|
||||||
|
description: Public URL OpnForm uses for APP_URL and NUXT_PUBLIC_APP_URL.
|
||||||
|
|
||||||
|
opnform_api_image:
|
||||||
|
type: str
|
||||||
|
default: jhumanj/opnform-api:latest
|
||||||
|
opnform_client_image:
|
||||||
|
type: str
|
||||||
|
default: jhumanj/opnform-client:latest
|
||||||
|
opnform_redis_image:
|
||||||
|
type: str
|
||||||
|
default: "redis:7"
|
||||||
|
opnform_db_image:
|
||||||
|
type: str
|
||||||
|
default: "postgres:16"
|
||||||
|
opnform_ingress_image:
|
||||||
|
type: str
|
||||||
|
default: "nginx:1"
|
||||||
|
|
||||||
|
opnform_app_key:
|
||||||
|
type: str
|
||||||
|
required: true
|
||||||
|
description:
|
||||||
|
- Laravel application key. Must be prefixed with C(base64:).
|
||||||
|
Generate with C(echo "base64:$(openssl rand -base64 32)").
|
||||||
|
Provide via OpenBao, Ansible Vault or extra-vars.
|
||||||
|
opnform_jwt_secret:
|
||||||
|
type: str
|
||||||
|
required: true
|
||||||
|
description: JWT signing secret. Generate with C(openssl rand -hex 32).
|
||||||
|
opnform_front_api_secret:
|
||||||
|
type: str
|
||||||
|
required: true
|
||||||
|
description: Shared secret between ui and api. Generate with C(openssl rand -hex 32).
|
||||||
|
|
||||||
|
opnform_db_name:
|
||||||
|
type: str
|
||||||
|
default: opnform
|
||||||
|
opnform_db_user:
|
||||||
|
type: str
|
||||||
|
default: opnform
|
||||||
|
opnform_db_password:
|
||||||
|
type: str
|
||||||
|
required: true
|
||||||
|
|
||||||
|
opnform_admin_name:
|
||||||
|
type: str
|
||||||
|
default: Administrator
|
||||||
|
opnform_admin_email:
|
||||||
|
type: str
|
||||||
|
default: ''
|
||||||
|
description:
|
||||||
|
- When non-empty (together with C(opnform_admin_password)) the role
|
||||||
|
bootstraps the first user via C(/api/register), skipping the
|
||||||
|
self-hosted setup page. Required when C(opnform_oidc_enabled=true).
|
||||||
|
opnform_admin_password:
|
||||||
|
type: str
|
||||||
|
default: ''
|
||||||
|
description:
|
||||||
|
- "Must satisfy OpnForm's policy: min 8 chars, letter + digit +
|
||||||
|
symbol from C(@$!%*#?&-_+=.,:;<>^()[]{}|~)."
|
||||||
|
opnform_admin_hear_about_us:
|
||||||
|
type: str
|
||||||
|
default: ansible
|
||||||
|
|
||||||
|
opnform_php_memory_limit:
|
||||||
|
type: str
|
||||||
|
default: 1G
|
||||||
|
opnform_php_max_execution_time:
|
||||||
|
type: str
|
||||||
|
default: "600"
|
||||||
|
opnform_php_upload_max_filesize:
|
||||||
|
type: str
|
||||||
|
default: 64M
|
||||||
|
opnform_php_post_max_size:
|
||||||
|
type: str
|
||||||
|
default: 64M
|
||||||
|
opnform_nginx_max_body_size:
|
||||||
|
type: str
|
||||||
|
default: 64m
|
||||||
|
|
||||||
|
opnform_mail_mailer:
|
||||||
|
type: str
|
||||||
|
default: log
|
||||||
|
choices: [log, smtp, ses, mailgun, postmark, sendmail]
|
||||||
|
opnform_mail_host:
|
||||||
|
type: str
|
||||||
|
default: ''
|
||||||
|
opnform_mail_port:
|
||||||
|
type: str
|
||||||
|
default: ''
|
||||||
|
opnform_mail_username:
|
||||||
|
type: str
|
||||||
|
default: ''
|
||||||
|
opnform_mail_password:
|
||||||
|
type: str
|
||||||
|
default: ''
|
||||||
|
opnform_mail_encryption:
|
||||||
|
type: str
|
||||||
|
default: ''
|
||||||
|
choices: ['', tls, ssl]
|
||||||
|
opnform_mail_from_address:
|
||||||
|
type: str
|
||||||
|
default: noreply@digitalboard.ch
|
||||||
|
opnform_mail_from_name:
|
||||||
|
type: str
|
||||||
|
default: OpnForm
|
||||||
|
|
||||||
|
opnform_oidc_enabled:
|
||||||
|
type: bool
|
||||||
|
default: false
|
||||||
|
description:
|
||||||
|
- "When true the role calls the workspace API to create a single
|
||||||
|
OIDC C(identity_connection) on the default workspace after the
|
||||||
|
admin bootstrap. Requires C(opnform_admin_email) +
|
||||||
|
C(opnform_admin_password) so the role can authenticate.
|
||||||
|
Idempotent: skipped when any connection already exists."
|
||||||
|
opnform_oidc_issuer:
|
||||||
|
type: str
|
||||||
|
default: https://auth.digitalboard.ch/realms/Digitalboard
|
||||||
|
description: OIDC issuer URL.
|
||||||
|
opnform_oidc_client_id:
|
||||||
|
type: str
|
||||||
|
default: opnform-digitalboard
|
||||||
|
opnform_oidc_client_secret:
|
||||||
|
type: str
|
||||||
|
default: ''
|
||||||
|
description: Required when C(opnform_oidc_enabled=true).
|
||||||
|
opnform_oidc_client_name:
|
||||||
|
type: str
|
||||||
|
default: Digitalboard
|
||||||
|
description: Display name shown in the OpnForm UI.
|
||||||
|
opnform_oidc_slug:
|
||||||
|
type: str
|
||||||
|
default: oidc
|
||||||
|
description:
|
||||||
|
- OpnForm-side identifier used in C(/auth/{slug}/callback). Lowercase
|
||||||
|
alphanumeric + hyphens, unique across all C(identity_connections).
|
||||||
|
opnform_oidc_domain:
|
||||||
|
type: str
|
||||||
|
default: ''
|
||||||
|
description:
|
||||||
|
- Email domain that triggers OIDC for matching users. Required
|
||||||
|
when C(opnform_oidc_enabled=true).
|
||||||
|
opnform_oidc_scopes:
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
default: [openid, profile, email, groups]
|
||||||
|
opnform_oidc_admin_group:
|
||||||
|
type: str
|
||||||
|
default: opnform-admins
|
||||||
|
description:
|
||||||
|
- Convenience setting that maps a single IdP group to the OpnForm
|
||||||
|
C(admin) role. Ignored when C(opnform_oidc_group_role_mappings)
|
||||||
|
is non-empty.
|
||||||
|
opnform_oidc_group_role_mappings:
|
||||||
|
type: list
|
||||||
|
elements: dict
|
||||||
|
default: []
|
||||||
|
description:
|
||||||
|
- Full IdP-group -> OpnForm-role mapping. Takes precedence over
|
||||||
|
C(opnform_oidc_admin_group).
|
||||||
|
options:
|
||||||
|
idp_group:
|
||||||
|
type: str
|
||||||
|
required: true
|
||||||
|
description: Group name as it appears in the IdP groups claim.
|
||||||
|
role:
|
||||||
|
type: str
|
||||||
|
required: true
|
||||||
|
choices: [owner, admin, editor, member]
|
||||||
|
|
||||||
|
opnform_traefik_network:
|
||||||
|
type: str
|
||||||
|
default: proxy
|
||||||
|
opnform_use_ssl:
|
||||||
|
type: bool
|
||||||
|
default: true
|
||||||
|
|
@ -1,35 +1,16 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
#SPDX-License-Identifier: MIT-0
|
||||||
galaxy_info:
|
galaxy_info:
|
||||||
author: your name
|
author: Tobias Wüst
|
||||||
description: your role description
|
description: Deploy OpnForm self-hosted form builder via Docker Compose behind Traefik
|
||||||
company: your company (optional)
|
company: Digitalboard
|
||||||
|
license: MIT-0
|
||||||
|
min_ansible_version: "2.15"
|
||||||
|
|
||||||
# If the issue tracker for your role is not on github, uncomment the
|
galaxy_tags:
|
||||||
# next line and provide a value
|
- opnform
|
||||||
# issue_tracker_url: http://example.com/issue/tracker
|
- forms
|
||||||
|
- docker
|
||||||
# Choose a valid license ID from https://spdx.org - some suggested licenses:
|
- traefik
|
||||||
# - BSD-3-Clause (default)
|
- oidc
|
||||||
# - MIT
|
|
||||||
# - GPL-2.0-or-later
|
|
||||||
# - GPL-3.0-only
|
|
||||||
# - Apache-2.0
|
|
||||||
# - CC-BY-4.0
|
|
||||||
license: license (GPL-2.0-or-later, MIT, etc)
|
|
||||||
|
|
||||||
min_ansible_version: 2.2
|
|
||||||
|
|
||||||
# If this a Container Enabled role, provide the minimum Ansible Container version.
|
|
||||||
# min_ansible_container_version:
|
|
||||||
|
|
||||||
galaxy_tags: []
|
|
||||||
# List tags for your role here, one per line. A tag is a keyword that describes
|
|
||||||
# and categorizes the role. Users find roles by searching for tags. Be sure to
|
|
||||||
# remove the '[]' above, if you add tags to this list.
|
|
||||||
#
|
|
||||||
# NOTE: A tag is limited to a single word comprised of alphanumeric characters.
|
|
||||||
# Maximum 20 tags per role.
|
|
||||||
|
|
||||||
dependencies: []
|
dependencies: []
|
||||||
# List your role dependencies here, one per line. Be sure to remove the '[]' above,
|
|
||||||
# if you add dependencies to this list.
|
|
||||||
|
|
@ -1,133 +0,0 @@
|
||||||
---
|
|
||||||
services:
|
|
||||||
api: &api-environment
|
|
||||||
image: jhumanj/opnform-api:latest
|
|
||||||
container_name: opnform-api
|
|
||||||
volumes: &api-environment-volumes
|
|
||||||
- opnform_storage:/usr/share/nginx/html/storage:rw
|
|
||||||
environment: &api-env
|
|
||||||
APP_ENV: production
|
|
||||||
# Database settings
|
|
||||||
DB_HOST: db
|
|
||||||
REDIS_HOST: redis
|
|
||||||
DB_DATABASE: ${DB_DATABASE:-forge}
|
|
||||||
DB_USERNAME: ${DB_USERNAME:-forge}
|
|
||||||
DB_PASSWORD: ${DB_PASSWORD:-forge}
|
|
||||||
DB_CONNECTION: ${DB_CONNECTION:-pgsql}
|
|
||||||
# PHP Configuration
|
|
||||||
PHP_MEMORY_LIMIT: "1G"
|
|
||||||
PHP_MAX_EXECUTION_TIME: "600"
|
|
||||||
PHP_UPLOAD_MAX_FILESIZE: "64M"
|
|
||||||
PHP_POST_MAX_SIZE: "64M"
|
|
||||||
env_file:
|
|
||||||
- ./api/.env
|
|
||||||
depends_on:
|
|
||||||
db:
|
|
||||||
condition: service_healthy
|
|
||||||
redis:
|
|
||||||
condition: service_healthy # Depend on redis being healthy too
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "php /usr/share/nginx/html/artisan about || exit 1"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 15s
|
|
||||||
retries: 3
|
|
||||||
start_period: 60s
|
|
||||||
|
|
||||||
api-worker:
|
|
||||||
<<: *api-environment
|
|
||||||
container_name: opnform-api-worker
|
|
||||||
command: ["php", "artisan", "queue:work"]
|
|
||||||
environment:
|
|
||||||
<<: *api-env
|
|
||||||
APP_ENV: production
|
|
||||||
healthcheck:
|
|
||||||
test:
|
|
||||||
["CMD-SHELL", "pgrep -f 'php artisan queue:work' > /dev/null || exit 1"]
|
|
||||||
interval: 60s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 30s
|
|
||||||
|
|
||||||
api-scheduler:
|
|
||||||
<<: *api-environment
|
|
||||||
container_name: opnform-api-scheduler
|
|
||||||
command: ["php", "artisan", "schedule:work"]
|
|
||||||
environment:
|
|
||||||
<<: *api-env
|
|
||||||
APP_ENV: production
|
|
||||||
healthcheck:
|
|
||||||
test:
|
|
||||||
[
|
|
||||||
"CMD-SHELL",
|
|
||||||
"php /usr/share/nginx/html/artisan app:scheduler-status --mode=check --max-minutes=3 || exit 1",
|
|
||||||
]
|
|
||||||
interval: 60s
|
|
||||||
timeout: 30s
|
|
||||||
retries: 3
|
|
||||||
start_period: 70s # Allow time for first scheduled run and cache write
|
|
||||||
|
|
||||||
ui:
|
|
||||||
image: jhumanj/opnform-client:latest
|
|
||||||
container_name: opnform-client
|
|
||||||
env_file:
|
|
||||||
- ./client/.env
|
|
||||||
depends_on:
|
|
||||||
api:
|
|
||||||
condition: service_healthy
|
|
||||||
healthcheck:
|
|
||||||
test:
|
|
||||||
["CMD-SHELL", "wget --spider -q http://localhost:3000/login || exit 1"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 45s
|
|
||||||
|
|
||||||
redis:
|
|
||||||
image: redis:7
|
|
||||||
container_name: opnform-redis
|
|
||||||
volumes:
|
|
||||||
- redis-data:/data
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 5s
|
|
||||||
|
|
||||||
db:
|
|
||||||
image: postgres:16
|
|
||||||
container_name: opnform-db
|
|
||||||
environment:
|
|
||||||
POSTGRES_DB: ${DB_DATABASE:-forge}
|
|
||||||
POSTGRES_USER: ${DB_USERNAME:-forge}
|
|
||||||
POSTGRES_PASSWORD: ${DB_PASSWORD:-forge}
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME:-forge}"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 5s
|
|
||||||
volumes:
|
|
||||||
- postgres-data:/var/lib/postgresql/data
|
|
||||||
|
|
||||||
ingress:
|
|
||||||
image: nginx:1
|
|
||||||
container_name: opnform-ingress
|
|
||||||
volumes:
|
|
||||||
- ./docker/nginx.conf:/etc/nginx/templates/default.conf.template
|
|
||||||
ports:
|
|
||||||
- 80:80
|
|
||||||
environment:
|
|
||||||
- NGINX_MAX_BODY_SIZE=64m
|
|
||||||
depends_on:
|
|
||||||
api:
|
|
||||||
condition: service_started
|
|
||||||
ui:
|
|
||||||
condition: service_started
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "nginx -t && curl -f http://localhost/ || exit 1"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 3
|
|
||||||
start_period: 10s
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
postgres-data:
|
|
||||||
opnform_storage:
|
|
||||||
redis-data:
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
#SPDX-License-Identifier: MIT-0
|
||||||
---
|
---
|
||||||
# vars file for homarr
|
# vars file for opnform
|
||||||
Loading…
Add table
Add a link
Reference in a new issue