Invulmenu:
- Vaste stappen (Basis, Verbinden, Instellingen, Toegang, Controleren) in
plaats van één lange lijst. Eén stap per groep werkt niet: de mediane app
heeft één groep, arr-stack eenentwintig. Lege stappen worden overgeslagen.
- Volgende controleert de verplichte velden van die stap; die melding kwam
eerder pas bij het installeren.
- Geavanceerde opties achter één schakelaar die op elke stap zichtbaar is,
plus 'Alles op één pagina' voor het oude gedrag. Beide onthouden.
Geheimen naar .env:
- compose_transform.geheimen_naar_env() vervangt geheime waarden door
${NAAM} en schrijft ze naar .env met rechten 0600. Werkt ook midden in
een database-URL. Poorten en paden blijven leesbaar in compose.
- .serverup.json bevat geen geheimen meer en krijgt ook 0600; daar stonden
ze wereldleesbaar in. Het herconfiguratieformulier leest ze terug uit
.env via een bewaarde veld-naar-sleutel-koppeling.
- docs/beveiliging.md legt uit wat dit niet oplost: docker inspect toont de
waarde nog steeds.
Bewerken:
- Laatste stap toont het gerenderde resultaat met jouw waarden en is
bewerkbaar; compose_override/env_override gaan mee bij het installeren.
Ongeldige YAML wordt geweigerd voordat er een map bestaat.
- De bestaande editor heeft tabbladen (compose/.env) en biedt herstarten na
opslaan. De .env-PUT weigert een regel zonder '=' en logt in audit.
Inloggegevens:
- /api/stacks/<n>/credentials plus paneel op de stackkaart: adres,
gebruikersnaam, wachtwoord achter een oog-knop, kopieerknop. Alleen voor
beheerders, elk bekijken komt in het auditlog.
- Nieuw 'credential'-kenmerk in template.json met naamherkenning als
terugval, zodat de bestaande 96 sjablonen meteen werken.
Herstel van een regressie uit v0.7.60: acht inlogwachtwoorden hadden hun
genereerknop verloren toen die aan 'generate' werd gehangen. Terug, behalve
waar een verzonnen waarde fout is (WireGuard-sleutel, externe tokens).
De menulogica wordt getest door de echte component uit index.html in Node uit
te voeren; slaat zichzelf over waar node ontbreekt. 2295 tests groen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C7oLCRYzY5ixJ5Sv8Y8EFb
12 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
portin de naam en typeint— krijgen bij het installeren automatisch een vrij poortnummer voorgesteld als de standaard al bezet is. - Velden met
token,password,secret,wachtwoord,jwtofkeyin 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:
{"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.
In welke stap komt een veld?
Het installatieformulier is een invulmenu met vaste stappen. Server Up leidt af waar een veld hoort, en meestal klopt dat:
| Stap | Wat er terechtkomt |
|---|---|
| Basis | service_name, poortvelden (type int met port in de naam), data_dir, timezone, puid/pgid |
| Verbinden | velden met connect |
| Toegang | alles wat als geheim geldt, plus gebruikersnamen en e-mailadressen |
| Instellingen | de rest |
Klopt dat voor jouw veld niet, zet het dan zelf:
{"name": "retention", "type": "str", "title": "Bewaartermijn", "step": "basis"}
Geldige waarden: basis, verbinden, instellingen, geheimen. Een onbekende
waarde valt terug op instellingen.
Inloggegevens
Velden met credential komen terug in het paneel Inloggegevens op de
stackkaart, zodat iemand na de installatie kan opzoeken waarmee hij moet
inloggen — handig bij een gegenereerd wachtwoord.
{"name": "admin_user", "type": "str", "title": "Gebruikersnaam",
"default": "admin", "credential": "username"}
Meestal hoef je dit niet te zetten: een veld met user, login of email in de
naam geldt als username, en een geheim veld met password erin als password.
Tokens en sleutels tellen bewust níét mee — daar log je niet mee in.
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:
{
"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