All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 1m27s
Aanleiding: Homepage weigerde met "Host validation failed" omdat de standaardwaarde van allowed_hosts poort 3001 noemde terwijl het poortveld op 3002 stond. Alle 96 sjablonen zijn daarop nagelopen. Systemisch: - De dobbelsteenknop, waar zeventien apps in hun uitleg naar verwijzen, verscheen nooit: hij hing af van een veld 'secret' dat fields() niet meestuurde. Wachtwoorden stonden daardoor leesbaar in het formulier. - 27 velden die je toch nooit zelf typt (databasewachtwoorden, JWT- en versleutelingssleutels) worden nu voorgevuld met een willekeurige waarde. Inlogwachtwoorden krijgen alleen een knop, want die moet je noteren. - De knop hangt nu aan 'generate' in plaats van aan 'secret': voor een WireGuard-privésleutel of een token uit een andere app is een verzonnen waarde juist fout. - required werd nergens gecontroleerd. Je kon installeren met een leeg databasewachtwoord of een lege sleutel. Nu geweigerd bij installeren en bij herconfigureren, met vermelding van de lege velden. Velden achter een uitgeschakelde groepsschakelaar tellen niet mee. - Wachtwoorden werden uitgeschreven in het installatielog; nu gemaskeerd. Losse fouten: - homepage: allowed_hosts stond op localhost:3001, nu * met uitleg. - baserow, hedgedoc, ntfy: URL in de standaardwaarde wees naar een poort waar niets luistert. - Vast wachtwoord in acht sjablonen weggehaald: ghost, immich, miniflux, paperless-ngx, unifi-network en vikunja kregen allemaal dezelfde database-wachtwoorden; grafana en gotify stonden op 'admin'. - data_dir bij keycloak, metube, miniflux en teslamate stond in het formulier maar kwam nergens terecht (die apps gebruiken een Docker-volume). - puid/pgid bij freshrss: het image kent ze niet. - prometheus gaf de gevraagde tijdzone niet door. Nieuw tests/test_app_templates.py: rendert elke app met zijn standaardwaarden en controleert ongedefinieerde variabelen, velden zonder werking, niet-gedeclareerde volumes, depends_on, dubbele containernamen, poortconflicten, poortnummers in standaardwaarden en vaste wachtwoorden. 2236 tests groen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C7oLCRYzY5ixJ5Sv8Y8EFb
333 lines
10 KiB
Markdown
333 lines
10 KiB
Markdown
# Eigen apps toevoegen
|
|
|
|
Een app in Server Up is een map met twee dingen: `template.json` beschrijft wat
|
|
de app is en welke vragen je bij het installeren krijgt, en `files/compose.yaml`
|
|
is het compose-bestand met plaatshouders erin.
|
|
|
|
```
|
|
apps/mijn-app/
|
|
├── template.json
|
|
└── files/
|
|
└── compose.yaml
|
|
```
|
|
|
|
Zet die map in `apps/`, of in je eigen git-repo die je toevoegt bij
|
|
**Instellingen → App Store → Repository**.
|
|
|
|
---
|
|
|
|
## Het kortste voorbeeld dat werkt
|
|
|
|
**`template.json`**
|
|
|
|
```json
|
|
{
|
|
"kind": "compose",
|
|
"metadata": {
|
|
"name": "Mijn App",
|
|
"description": "Wat de app doet, in één zin.",
|
|
"tags": ["voorbeeld"],
|
|
"categories": ["productiviteit"],
|
|
"icon": {"provider": "selfhst", "id": "mijn-app"},
|
|
"version": {"name": "latest"}
|
|
},
|
|
"variables": [
|
|
{
|
|
"title": "Algemeen",
|
|
"items": [
|
|
{"name": "service_name", "type": "str", "title": "Servicenaam",
|
|
"default": "mijn-app", "required": true},
|
|
{"name": "port", "type": "int", "title": "Web poort",
|
|
"default": 8200, "required": true},
|
|
{"name": "data_dir", "type": "str", "title": "Data directory",
|
|
"default": "/opt/serverup/appdata", "required": true},
|
|
{"name": "timezone", "type": "str", "title": "Tijdzone",
|
|
"default": "Europe/Amsterdam", "required": true}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
**`files/compose.yaml`**
|
|
|
|
```yaml
|
|
services:
|
|
<< service_name >>:
|
|
image: voorbeeld/mijn-app:latest
|
|
container_name: << service_name >>
|
|
ports:
|
|
- "<< port >>:8080"
|
|
environment:
|
|
- TZ=<< timezone >>
|
|
volumes:
|
|
- << data_dir >>/<< service_name >>/config:/config
|
|
restart: unless-stopped
|
|
```
|
|
|
|
Dat is genoeg. De rest van dit document beschrijft wat je er nog meer mee kunt.
|
|
|
|
---
|
|
|
|
## Compose-syntaxis
|
|
|
|
De sjablonen gebruiken Jinja met afwijkende haakjes, zodat ze niet botsen met
|
|
de `${VAR}` van docker compose zelf:
|
|
|
|
| | |
|
|
|---|---|
|
|
| `<< variabele >>` | waarde invullen |
|
|
| `<%- if variabele %>` … `<%- endif %>` | blok alleen opnemen als de waarde waar is |
|
|
| `<< waarde \| lower >>` | filters van Jinja werken gewoon |
|
|
|
|
Het renderen gebeurt in een sandbox: je kunt niet bij Python-internals.
|
|
|
|
### Named volumes moeten gedeclareerd worden
|
|
|
|
Gebruik je een named volume, zet het dan óók in het top-level `volumes:`-blok.
|
|
Vergeet je dat, dan weigert docker compose met *"refers to undefined volume"*:
|
|
|
|
```yaml
|
|
services:
|
|
<< service_name >>-db:
|
|
image: postgres:16-alpine
|
|
volumes:
|
|
- << service_name >>_pgdata:/var/lib/postgresql/data
|
|
|
|
volumes:
|
|
<< service_name >>_pgdata:
|
|
```
|
|
|
|
De testsuite controleert dit voor elke app.
|
|
|
|
---
|
|
|
|
## Metadata
|
|
|
|
| Veld | Betekenis |
|
|
|---|---|
|
|
| `name` | Naam zoals getoond in de catalogus |
|
|
| `description` | Eén zin: wat is het, en waarvoor gebruik je het |
|
|
| `tags` | Vrije trefwoorden, gebruikt voor zoeken en filteren |
|
|
| `categories` | Eén of meer categorie-ids (zie onder). Ontbreken ze, dan worden ze afgeleid uit de tags |
|
|
| `icon` | `{"provider": "selfhst", "id": "…"}` of `dashboard-icons`. Bestaat het icoon niet, dan valt de interface terug op een emoji |
|
|
| `version` | Alleen ter informatie in de catalogus |
|
|
|
|
### Categorieën
|
|
|
|
Een app mag in meerdere categorieën zitten. De ids:
|
|
|
|
`media` · `fotos` · `downloaden` · `domotica` · `3d-printen` · `netwerk` ·
|
|
`beveiliging` · `opslag` · `documenten` · `productiviteit` · `communicatie` ·
|
|
`financien` · `gezin` · `monitoring` · `ontwikkeling` · `ai` · `beheer` ·
|
|
`overig`
|
|
|
|
Elke categorie heeft een vaste kleur die overal in de interface terugkomt. De
|
|
definities staan in `server-up/core/categories.py`.
|
|
|
|
---
|
|
|
|
## Velden
|
|
|
|
Elk item onder `items` wordt één vraag in het installatieformulier.
|
|
|
|
```json
|
|
{
|
|
"name": "port",
|
|
"type": "int",
|
|
"title": "Web poort",
|
|
"default": 8200,
|
|
"required": true,
|
|
"description": "Korte tekst onder het veld",
|
|
"help": "Langere uitleg, zichtbaar achter het info-icoon",
|
|
"advanced": false,
|
|
"config": {"placeholder": "bv. 8200", "options": ["a", "b"]}
|
|
}
|
|
```
|
|
|
|
| Type | Wordt |
|
|
|---|---|
|
|
| `str` | tekstveld |
|
|
| `int` | getalveld |
|
|
| `bool` | aanvinkvakje |
|
|
| `enum` | keuzelijst, met de waarden uit `config.options` |
|
|
|
|
### Vaste namen
|
|
|
|
Deze namen worden speciaal behandeld:
|
|
|
|
- **`service_name`** — verplicht. Wordt de containernaam en het voorvoegsel van
|
|
alles in de stack. Wordt automatisch gevuld met de instantienaam.
|
|
- **Velden met `port` in de naam en type `int`** — krijgen bij het installeren
|
|
automatisch een vrij poortnummer voorgesteld als de standaard al bezet is.
|
|
- **Velden met `token`, `password`, `secret`, `wachtwoord`, `jwt` of `key` in de
|
|
naam** — worden als geheim behandeld: de waarde wordt gemaskeerd in het
|
|
installatielog en het veld krijgt een eigen weergave. Je kunt het ook
|
|
afdwingen met `"secret": true`.
|
|
|
|
### Waarden laten genereren
|
|
|
|
Voor een databasewachtwoord of een JWT-sleutel heeft de gebruiker geen zinnige
|
|
keuze; die moet gewoon willekeurig zijn. Zet daarvoor `generate`:
|
|
|
|
```json
|
|
{"name": "db_password", "type": "str", "title": "Databasewachtwoord",
|
|
"default": "", "required": true, "generate": "auto"}
|
|
```
|
|
|
|
| Waarde | Wat er gebeurt |
|
|
|---|---|
|
|
| `"auto"` | Het veld wordt bij het openen van het formulier gevuld met 32 willekeurige tekens. Voor waarden die de gebruiker nooit hoeft te kennen. |
|
|
| `true` | Alleen een knop naast het veld. Voor wachtwoorden waarmee de gebruiker zélf inlogt: die moet hij noteren, dus vul ze niet ongevraagd in. |
|
|
| weglaten | Geen knop. Voor waarden met een eigen formaat of herkomst — een bcrypt-hash, een token uit een andere app, een sleutel die bij iets anders moet passen. Iets verzonnens is daar juist fout. |
|
|
|
|
> **Zet nooit een wachtwoord als `default`.** Dan krijgt iedereen die de app
|
|
> installeert hetzelfde, en de meesten klikken erdoorheen. Een test weigert
|
|
> sjablonen met een vaste waarde op een geheim veld.
|
|
|
|
### Verplichte velden
|
|
|
|
`"required": true` wordt afgedwongen: een lege waarde levert een foutmelding op
|
|
in plaats van een container die niet start of met een leeg geheim draait. Zit
|
|
het veld achter een groepsschakelaar die uitstaat, dan telt het niet mee.
|
|
|
|
### Geavanceerde velden
|
|
|
|
`"advanced": true` zet een veld achter het uitklapblok *Geavanceerde opties*.
|
|
Bedoeld voor wat je meestal met rust laat: database-wachtwoorden, PUID/PGID,
|
|
USB-paden, bewaartermijnen. Zet niet álles op geavanceerd — een test controleert
|
|
dat er per app zichtbare velden overblijven.
|
|
|
|
Je kunt het ook op een hele groep zetten: `{"title": "…", "advanced": true, …}`.
|
|
|
|
### Uitleg
|
|
|
|
`description` staat als kleine tekst onder het veld. `help` is de langere uitleg
|
|
achter het info-icoon; ontbreekt die, dan wordt `description` getoond. Gebruik
|
|
`help` voor dingen die een gebruiker echt moet weten:
|
|
|
|
```json
|
|
{
|
|
"name": "downloads_dir",
|
|
"type": "str",
|
|
"title": "Downloadmap",
|
|
"default": "/mnt/downloads",
|
|
"help": "Zet deze map op hetzelfde bestandssysteem als je mediamap. Staan ze op verschillende volumes, dan kopieert elke voltooide download in plaats van te verplaatsen."
|
|
}
|
|
```
|
|
|
|
### Velden afhankelijk maken
|
|
|
|
`needs` verbergt een veld tot een ander veld een bepaalde waarde heeft:
|
|
|
|
```json
|
|
{"name": "smtp_host", "type": "str", "title": "SMTP-server",
|
|
"needs": ["mail_enabled == true"]}
|
|
```
|
|
|
|
---
|
|
|
|
## Apps aan elkaar koppelen
|
|
|
|
Verwijst een veld naar een andere app, zet er dan `connect` op. De interface
|
|
toont dan een keuzelijst met wat er al geïnstalleerd is, in plaats van een leeg
|
|
tekstveld:
|
|
|
|
```json
|
|
{
|
|
"name": "mqtt_server",
|
|
"type": "str",
|
|
"title": "MQTT-server",
|
|
"default": "mqtt://mosquitto:1883",
|
|
"connect": {"app": "mosquitto", "scheme": "mqtt", "port": 1883},
|
|
"help": "Heb je de Mosquitto-app geïnstalleerd, kies die dan hier."
|
|
}
|
|
```
|
|
|
|
| Sleutel | Betekenis |
|
|
|---|---|
|
|
| `app` | De mapnaam van de app waarnaar verwezen wordt (`apps/mosquitto`) |
|
|
| `scheme` | Wat voor het adres komt: `http`, `mqtt`, … |
|
|
| `port` | Poort binnen het gedeelde netwerk (de interne poort, niet die op de host) |
|
|
| `path` | Optioneel pad achter het adres |
|
|
|
|
Dat levert bijvoorbeeld `mqtt://mosquitto:1883` op. Staat de app er nog niet, dan
|
|
meldt het veld dat en kun je alsnog handmatig invullen.
|
|
|
|
**Let op de poort:** dit gaat over de poort *binnen* de container, niet de poort
|
|
die op de host gepubliceerd is. De stacks zitten samen op een gedeeld netwerk en
|
|
bereiken elkaar op containernaam.
|
|
|
|
---
|
|
|
|
## Onderdelen optioneel maken
|
|
|
|
Voor stacks met meerdere componenten kun je per groep een schakelaar zetten:
|
|
|
|
```json
|
|
{
|
|
"title": "Bazarr — ondertitels",
|
|
"toggle": "enable_bazarr",
|
|
"toggle_default": true,
|
|
"description": "Haalt automatisch ondertitels op",
|
|
"items": [
|
|
{"name": "port_bazarr", "type": "int", "title": "Poort", "default": 6767}
|
|
]
|
|
}
|
|
```
|
|
|
|
In het compose-bestand gebruik je die schakelaar als voorwaarde:
|
|
|
|
```yaml
|
|
<%- if enable_bazarr %>
|
|
<< service_name >>-bazarr:
|
|
image: lscr.io/linuxserver/bazarr:latest
|
|
ports:
|
|
- "<< port_bazarr >>:6767"
|
|
<%- endif %>
|
|
```
|
|
|
|
`toggle_default` bepaalt of het onderdeel standaard aan staat. Zonder die sleutel
|
|
staat hij aan.
|
|
|
|
De ARR-stack (`apps/arr-stack`) gebruikt dit voor twintig onderdelen en is het
|
|
beste voorbeeld om van af te kijken.
|
|
|
|
---
|
|
|
|
## Waar je op moet letten
|
|
|
|
**Poorten.** Kies een standaardpoort die nog niet door een andere app gebruikt
|
|
wordt. De testsuite controleert dat, en meldt welke botsen. Alternatieven voor
|
|
dezelfde taak (twee reverse proxies, twee DNS-blokkers) mogen wel dezelfde poort
|
|
delen; die staan in een allowlist in `tests/test_apps.py`.
|
|
|
|
**`network_mode`.** Heeft een service `network_mode: host` of
|
|
`network_mode: service:…`, dan kan hij niet aan het gedeelde netwerk hangen.
|
|
Server Up slaat die services over, dus het werkt — maar zo'n app kan andere apps
|
|
niet op naam bereiken.
|
|
|
|
**Images.** Gebruik een image waarvan je zeker weet dat hij bestaat en welke
|
|
architecturen hij ondersteunt. De testsuite controleert wel de vorm van je
|
|
compose, maar haalt geen images op.
|
|
|
|
---
|
|
|
|
## Testen
|
|
|
|
Elke app in `apps/` wordt automatisch meegenomen in de testsuite:
|
|
|
|
```bash
|
|
python -m pytest tests/test_apps.py -q
|
|
```
|
|
|
|
Gecontroleerd wordt: geldige metadata, renderen naar geldige YAML, elke service
|
|
heeft een `image` of `build`, named volumes zijn gedeclareerd, geen onvervangen
|
|
`<< variabelen >>`, geen onbedoeld dubbele poorten, geldige categorieën, en dat
|
|
`network_mode` niet botst met het gedeelde netwerk.
|
|
|
|
Wil je alleen jouw app zien:
|
|
|
|
```bash
|
|
python -m pytest tests/test_apps.py -q -k mijn-app
|
|
```
|