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

8.8 KiB

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

{
  "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

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":

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.

{
  "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:

{
  "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:

{"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:

{
  "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:

{
  "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:

<%- 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:

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:

python -m pytest tests/test_apps.py -q -k mijn-app