server-up/docs/apps-maken.md
Ramon 2ba15418ab
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 1m13s
v0.7.20-beta - instellingen wijzigen, koppelen achteraf, defect uit v0.7.00
Defect (v0.7.00): add_shared_network() hing elke service aan het gedeelde
netwerk, ook services met network_mode. Docker compose weigert die combinatie
("declares mutually exclusive network_mode and networks"), waardoor Homebridge,
Scrypted, Beszel en de ARR-stack met Gluetun niet te installeren waren met de
standaardkeuze "Verbinden met andere apps". Die services worden nu overgeslagen;
blijft er niets over, dan ook geen netwerkblok. Test over alle 96 apps.

Instellingen wijzigen na installatie:
- De gemaakte keuzes worden bij installatie opgeslagen in .serverup.json
  (values + format); zonder die waarden viel het formulier niet te heropenen.
- GET /api/stacks/<naam>/config geeft de opgeslagen waarden plus het actuele
  veldschema; POST /reconfigure maakt een backup, rendert opnieuw, past netwerk
  en eigen IP opnieuw toe, valideert en herstart. Mislukt het valideren, dan
  wordt de backup teruggezet.
- Knop Instellingen op de stackkaart opent dezelfde modal in wijzigen-stand,
  met een waarschuwing dat handmatige compose-wijzigingen verloren gaan.
- Stacks van voor deze versie: knop uit met uitleg.

Bestaande stacks koppelen:
- POST /api/stacks/<naam>/connect zet een bestaande stack op het gedeelde
  netwerk, valideert en herstart; wordt de compose ongeldig, dan wordt de
  wijziging teruggedraaid.

Audit-log:
- audit.query()/count() accepteren filters op bron, actie, status, periode en
  vrije tekst; facets() levert de keuzelijsten. /api/audit ondersteunt die als
  queryparameters plus doorbladeren. Filterbalk in de UI.

Documentatie:
- README.md (bestond niet), docs/apps-maken.md met het volledige
  templateformaat, docs/README.md als index.
- Changelog-secties voor v0.5.44/45/46 aangevuld; release.yml haalt de notes
  daaruit, dus een tag daarop gaf een lege release. Test die afdwingt dat het
  huidige VERSION een sectie heeft.

941 tests groen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C7oLCRYzY5ixJ5Sv8Y8EFb
2026-07-27 15:43:49 +02:00

305 lines
8.8 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` of `key` in de naam** — krijgen een
knop die een willekeurige waarde genereert.
### 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
```