server-up/CHANGELOG.md
Ramon f86d7d35a6
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 6m26s
chore: dev terug op een betanummer (0.8.31-beta)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H9Vcwu7owJZ3nj5dCf4VFT
2026-08-07 15:43:50 +02:00

135 KiB
Raw Blame History

v0.8.31-beta — In ontwikkeling

Nog niets uitgebracht sinds v0.8.30. Wat hier onder komt te staan gaat mee in de volgende uitgave; bij het uitbrengen wordt deze kop de releaseversie.

v0.8.30 — Je ziet nu waarom een app niet start

Een app die niet opkwam meldde alleen dependency failed to start: container X is unhealthy. Dat is een gevolg, geen oorzaak: de echte reden stond in de log van díé container, en daar kwam je alleen met docker logs — als je wist dat dat kon. Op een Raspberry Pi bleef een MongoDB die stopt met Illegal instruction daardoor dagenlang een raadsel.

  • De reden staat in hetzelfde joblog. Mislukt het starten, dan haalt Server Up van elke container die niet draait of ongezond is de uitvoer van de healthcheck en de laatste logregels op en zet die eronder. Bij starten, bijwerken, installeren en instellingen wijzigen.
  • De lege map die Docker achterlaat, wordt opgeruimd. Wijst een bind-mount naar een bestand dat nog niet in de stackmap staat, dan maakt Docker er een map van. De container stopt daarna met exit 126 (Is a directory), en het opnieuw renderen struikelde over diezelfde map — waarmee het probleem zichzelf in stand hield. Een lege map wordt nu opgeruimd; staat er iets in, dan krijg je te horen wat er aan de hand is.
  • UniFi's reparatiecontainer zegt wat er mis is als zijn script ontbreekt, in plaats van een kaal exit-nummer.
  • Een bijgewerkt sjabloon valt op. Opnieuw starten rendert een app niet opnieuw, dus een verbeterd sjabloon bereikte een bestaande installatie nooit. Het wijzigscherm laat nu zien welke images het sjabloon inmiddels gebruikt en wat jouw app draait — een eigen versiekeuze telt daarbij niet als verschil.

Vier nieuwe apps voor iets wat de catalogus nog niet had: een machine ergens anders bij je eigen netwerk laten komen, en Proton Mail bruikbaar maken voor apps die geen Proton spreken.

  • WireGuard. Verbind deze machine met een ander netwerk door je eigen wg0.conf in de map wg_confs te zetten, of laat hem zelf een server met clientprofielen aanmaken. Krijgt de rechten die een tunnel nodig heeft: NET_ADMIN, de kernelmodules van de host en src_valid_mark.
  • Tailscale. Zet de machine in je eigen tailnet, met network_mode: host zodat de tunnel voor de hele machine geldt. Optioneel als subnetrouter, zodat je bij je hele thuisnetwerk kunt.
  • Cloudflare Tunnel. Een app van buitenaf bereikbaar zonder een poort open te zetten; publiceert zelf niets, want dat is het hele punt.
  • Proton Mail Bridge. Vertaalt je Proton-account naar gewone IMAP en SMTP. Staat op poort 1026 in plaats van 1025, zodat hij naast Mailpit past. Het sjabloon vertelt hoe je de eenmalige aanmelding doet — die kan niet via een formulier, want Proton vraagt om een tweede factor.

En de updater is niet meer kieskeurig over het adres dat je invult.

Wie in Forgejo naar zijn releases keek en dat adres in de instellingen plakte, kreeg een webpagina in plaats van JSON — en als enige melding JSONDecodeError: Expecting value: line 1 column 1 (char 0). Daar leidt niemand uit af dat er /api/v1/repos/ in het adres had moeten staan.

  • Het adres wordt omgerekend. https://git.example.nl/bes-r/server-up en .../releases worden allebei het juiste API-adres; GitHub krijgt zijn eigen api.github.com. Een adres dat al klopt of dat we niet herkennen blijft staan.
  • Een begrijpelijke melding als er tóch HTML terugkomt, met wat er dan wél in het veld hoort.
  • https:// voor het imageveld wordt weggehaald. Een image-adres heeft nooit een schema, dus docker pull https://… faalde op iets wat er in het veld goed uitzag.

v0.8.29 — UniFi start weer op een Raspberry Pi 4

Het sjabloon pinde MongoDB op 4.4.29 "voor de Pi", maar juist vanaf 4.4.19 eist MongoDB ARMv8.2-A. Een Pi 4 is ARMv8.0-A, dus mongod stopte daar meteen met Illegal instruction, de container werd nooit healthy en UniFi startte niet: dependency failed to start: container unifi-mongo is unhealthy.

  • Standaard 4.4.18, de laatste versie die op een Pi 4 draait. Dit is een patch-downgrade binnen 4.4; het datafomaat verandert daar niet mee.
  • De versie is instelbaar (geavanceerd veld). Op een Pi 5 of een gewone server kun je hoger — maar alleen bij een nieuwe installatie: een bestaande 4.4-database gaat niet zomaar naar 7.0.
  • De healthcheck heeft meer lucht. Op een Pi kost het starten van de mongo-shell zelf al seconden en duurt de eerste initialisatie op een SD-kaart langer dan een halve minuut. Op een snelle host verandert er niets: die is na de eerste controle gezond.
  • De reparatiecontainer volgt dezelfde versie, zodat die niet alsnog een image start dat de processor niet aankan.

v0.8.28 — Installeren vanaf een ander adres

De ingebouwde hostnaam in install.sh lost niet overal op. Staat je Forgejo achter een naam die alleen op je eigen netwerk werkt, dan strandt bijwerken vanaf een andere locatie op could not resolve host — ook door een tunnel, want die repareert de naam niet.

  • --host ADRES geeft aan waar de broncode staat: een naam, een IP of een tailnet-adres. Een schema mag mee (--host http://10.0.20.22:3000), want een IP over https levert een certificaatfout op.
  • Een bestaande installatie gaat mee. Die haalt op via de remote die in de kloon staat — precies het adres dat het niet doet. Met --host wordt die remote eerst omgezet, zodat bijwerken daarna vanzelf via het nieuwe adres gaat.
  • --branch accepteert ook een tag, en dat staat nu ook in de hulptekst.

v0.8.27 — Wachtwoorden, wachtrijen, snelheid en versies

Eerste stabiele uitgave sinds v0.8.12. Wie vanaf die versie bijwerkt, krijgt de hele beta-reeks v0.8.13 tot en met v0.8.26 er in één keer bij; de losse secties daarvan staan hieronder. De hoofdpunten:

  • UniFi komt weer online. Een geslaagde instellingswijziging werd door een fout in het audit-logboek meteen teruggedraaid, en UniFi's eigen kopie van de databasegegevens werd nooit bijgewerkt. Beide zijn verholpen; de MongoDB- gebruiker wordt ook op een oud volume gerepareerd.
  • Koppelen laat je database met rust. Alleen de app zelf gaat op het gedeelde netwerk, niet zijn opslag.
  • Eigen mappen en apparaten per app. Een mediamap of USB-stick koppelen zonder het compose-bestand met de hand te bewerken, en zonder dat een volgende wijziging het weer weghaalt.
  • Zichtbaar waar een sjabloon vandaan komt, met een waarschuwing als de app-repo achterloopt op de draaiende versie — en de branch van een repo is nu te zien en te wijzigen.

En de vier verbeteringen van deze uitgave zelf, die alle vier dezelfde vorm hebben: iets dat er goed uitzag totdat je het nodig had.

  • Een databasewachtwoord wijzigen breekt je app niet meer. POSTGRES_PASSWORD en verwanten worden door de officiële images alleen bij een lege datamap gebruikt. Wie dat veld wijzigde kreeg een app met een nieuw wachtwoord en een database met het oude — 64 van de sjablonen hebben zo'n veld. Server Up zet het wachtwoord nu eerst om in de nog draaiende database (ALTER USER, updateUser) en breekt de wijziging af als dat niet lukt. Het oude en nieuwe wachtwoord gaan daarbij via de omgeving, niet via de argumenten: die zijn op de host zichtbaar in de procestabel.
  • Eén taak tegelijk per app. De nachtelijke backup kon starten midden in een instellingswijziging en zo een half herschreven map inpakken. Taken op dezelfde app wachten nu netjes op elkaar; de geplande backup slaat een bezette app over en meldt dat. Een taak die klapt laat het slot los en komt niet meer voor eeuwig op 'bezig' te staan.
  • Het dashboard doet één docker-aanroep in plaats van één per app. Bij vijftien apps waren dat vijftien processen per keer verversen, en nog eens zoveel bij elk openen van de installatiemodal.
  • Versie per app te kiezen. De meeste sjablonen staan op latest, waardoor Bijwerken je ongemerkt over een majorversie kan tillen. In de stap 'Extra's' zet je per onderdeel een versie vast; die overleeft elke volgende wijziging. In het containerpaneel staat welke versie er nu draait.

v0.8.26-beta — Eigen mappen en apparaten per app

Een sjabloon kan niet elke situatie voorzien: een mediamap die ergens anders staat, een USB-stick voor Zigbee, een videokaart om mee te transcoderen. Dat kon alleen door het compose-bestand met de hand te bewerken — en dat werd bij de eerstvolgende 'Instellingen wijzigen' weer overschreven.

  • Nieuwe stap 'Extra's' in het invulmenu. Per onderdeel een map van de host koppelen (met een vinkje 'alleen lezen') of een apparaat doorgeven.
  • Ze overleven een wijziging. De keuzes staan in .serverup.json en worden ná het renderen opnieuw toegepast, net als de netwerkinstellingen.
  • Apparaten worden gevonden. Server Up leest /dev/serial/by-id, ttyUSB*, ttyACM* en /dev/dri uit en biedt ze aan met één klik. Een by-id-pad blijft gelijk als je de stick in een andere poort steekt, en krijgt daarom het label 'vast'.
  • Padcontrole vooraf. Een map die Server Up niet kan bereiken meldt zich meteen, in plaats van een lege mount in de container op te leveren.
  • Gevaarlijke mounts worden geweigerd. De Docker-socket, /etc, /proc, /sys en /boot gaan niet via dit scherm: die geven een container de macht over de hele host. Je krijgt te horen waaróm, in plaats van een stille correctie.

v0.8.25-beta — Zichtbaar uit welk sjabloon een app komt

Sjablonen komen uit de git-cache van een app-repo, niet uit het image van Server Up. Die twee kunnen ver uit elkaar lopen: staat de repo op een branch die achterloopt, dan rendert 'Instellingen wijzigen' een oud sjabloon over een werkende app heen — en daar was aan niets te zien.

  • Het wijzigscherm noemt zijn bron. Repo, branch en hoe lang geleden er gesynchroniseerd is, boven de velden waar je op opslaan drukt.
  • Een achterlopende repo waarschuwt. Levert de repo zelf een VERSION mee (zoals deze) en is die ouder dan de draaiende versie, dan zegt het scherm dat met de versie erbij.
  • De repolijst toont de stand. Commit en synchronisatiemoment per repo, zodat je niet hoeft te gokken of een sync nog nodig is.

v0.8.24-beta — Koppelen laat je database met rust

Koppelen aan het gedeelde netwerk hing élke service van een stack eraan, ook de database. Eén druk op de knop maakte roosterwijs-db of graylog-mongo op containernaam bereikbaar voor elke andere gekoppelde app — en lang niet elk sjabloon zet een wachtwoord op zijn database.

  • Alleen de app gaat mee, niet zijn opslag. Databases, caches en zoekindexen blijven standaard binnen hun eigen stack. Een MQTT-broker of RabbitMQ hoort daar bewust niet bij: die dééllen is juist waarvoor het gedeelde netwerk bestaat.
  • Per onderdeel te kiezen. Wil je één Postgres voor twee stacks, dan zet je hem in de stap 'Verbinden' aan. De keuze wordt bewaard en blijft gelden bij een volgende wijziging.
  • Het joblog vertelt wat er gekoppeld is, in plaats van alleen het netwerk.
  • Een backup van een database zonder wachtwoord werkt weer. Het dumpcommando stuurde altijd inloggegevens mee, ook bij een MongoDB zonder authenticatie — dat mislukte gegarandeerd, terwijl de melding erna 'Backup klaar' was.
  • Het joblog overleeft een herstart. Taken stonden alleen in het geheugen; wie tijdens het bijwerken meekeek zag daarna alleen nog "unknown". De laatste twintig taken staan nu in /data/jobs, en een taak die door een herstart is afgekapt zegt dat ook.

v0.8.23-beta — De branch van een repo is te zien en te wijzigen

Sjablonen komen uit de git-cache van een app-repo, niet uit het image van Server Up. Welke branch die repo volgt was nergens te zien, en alleen te veranderen door de repo te verwijderen en opnieuw toe te voegen. De id wordt dan uit de URL afgeleid en krijgt er een volgnummer bij zodra hij al bestaat — en juist die id staat in de metadata van elke app uit die repo. Overstappen maakte zo stilzwijgend het bronsjabloon van je apps onvindbaar.

  • De branch staat in de lijst. Zo is te zien waarom een app een oudere versie van zijn sjabloon blijft renderen.
  • Bewerken zonder de id te raken. Naam, branch en submap zijn aan te passen; na opslaan wordt de repo meteen opnieuw opgehaald.
  • Een leeg tokenveld wist het token niet. De interface krijgt het token nooit te zien, dus zou opslaan hem anders weggooien.

v0.8.22-beta — UniFi leest zijn databasegegevens opnieuw in

Het linuxserver-image schrijft /config/data/system.properties alleen bij de allereerste start ("Only evaluated on first run"). Alles wat daarna in Compose verandert — de toegevoegde authSource=admin, een ander wachtwoord — kwam daar dus nooit in terecht. UniFi bleef verbinden met de gegevens van de allereerste installatie, en MongoDB antwoordde met UserNotFound: unifi@unifi.

  • De databasegegevens worden bij elke start gelijkgetrokken. De reparatiecontainer zet db.mongo.uri, statdb.mongo.uri, db.mongo.local en unifi.db.name opnieuw uit de actuele Compose-omgeving. Andere instellingen in het bestand blijven staan.
  • Bijzondere tekens gaan ge-url-codeerd de URI in. Een @ of / in het wachtwoord brak de verbindingsreeks anders doormidden.
  • Het bestand houdt zijn eigenaar en rechten. Het wordt op zijn plaats overschreven, zodat UniFi er zelf in kan blijven schrijven.
  • Het oude wachtwoord telt mee bij de reparatie. Wijkt .env af van waarmee de database ooit is aangemaakt, dan meldt de reparatie zich aan met het wachtwoord uit system.properties en trekt daarna alles gelijk — in plaats van je met een dode stack achter te laten.

v0.8.21-beta — Wijzigingen blijven staan, UniFi komt online

Een geslaagde instellingenwijziging werd meteen weer teruggedraaid: het audit-logboek las het IP-adres van de aanvrager pas uit in de achtergrondthread, waar geen request meer bestaat. De RuntimeError die daarop volgde kwam in de foutafhandeling terecht, die netjes de backup terugzette — inclusief het oude compose-bestand. De MongoDB-reparatie van v0.8.20 kon zo nooit landen.

  • Het adres wordt vóór de job vastgelegd. Herconfigureren, koppelen, een backup maken en terugrollen loggen weer wie de actie startte, zonder crash.
  • UniFi repareert ook een oud database-volume. Het init-script probeert achtereenvolgens de beheerders unifi en root en herkent een server zonder authenticatie, in plaats van te veronderstellen dat root bestaat.
  • De healthcheck van Mongo gebruikt geen inloggegevens meer. Op een volume zonder de gebruiker root bleef de container ongezond, waardoor de reparatiecontainer nooit startte.
  • Bestaande rollen blijven staan. De reparatie trekt alleen het wachtwoord gelijk en vult ontbrekende rollen aan — inclusief backup en restore, die mongodump en mongorestore nodig hebben.
  • Backups van MongoDB werken. Het dumpcommando gebruikt de beheerder uit het compose-bestand in plaats van de verzonnen gebruiker app.
  • Elke app met een webinterface krijgt een openknop op de kaart. Het adres stond alleen in de modal met inloggegevens, waar je niet kijkt als je de app gewoon wilt openen.
  • Geen dubbele regels meer in het joblog. Bij een trage stap vertrokken meerdere polls met dezelfde offset, waardoor hetzelfde blok tot vijf keer in beeld kwam.

v0.8.20-beta — UniFi herstelt zijn MongoDB-gebruiker

Een eerder geïnitialiseerd Mongo-volume voerde het init-script niet opnieuw uit. Daardoor bleef UniFi herstarten met UserNotFound: unifi@unifi en kwam de webinterface nooit online.

  • Idempotente databasereparatie. Een korte init-container maakt de UniFi-gebruiker aan of werkt hem bij, ook bij een bestaand Mongo-volume.
  • Oude installaties worden ondersteund. Zowel de officiële authsource admin als de eerder opgeslagen authsource unifi wordt gerepareerd.
  • UniFi wacht op de reparatie. De applicatie start pas wanneer Mongo gezond is en de gebruikerscontrole succesvol is afgerond.
  • Geen misleidende poort 8843. De appkaart toont alleen werkelijk op de host gepubliceerde poorten; UniFi's ongepubliceerde gastportaalpoort verdwijnt.

v0.8.19-beta — Mobiele appacties reageren weer

In het mobiele actiemenu kregen normale acties zonder expliciete uitschakelstatus toch het HTML-attribuut disabled. Daardoor deed onder meer de knop Verwijderen zichtbaar helemaal niets.

  • Verwijderen is weer klikbaar. Alleen acties die expliciet als uitgeschakeld zijn gemarkeerd krijgen nog het disabled-attribuut.
  • De actie loopt vóór het menu sluit. Zo blijft de Alpine-scope beschikbaar totdat de verwijdermodal daadwerkelijk is geopend.
  • Echte DOM-regressietest. De test rendert de appkaart met Alpine en klikt zowel de desktop- als mobiele verwijderroute volledig door.

v0.8.18-beta — UniFi opent via HTTPS

De UniFi-container was wel correct op zijn HTTPS-poort gepubliceerd, maar Server Up maakte er altijd een http://-link van. Daardoor leek de app onbereikbaar terwijl de browser met het verkeerde protocol verbinding maakte.

  • UniFi declareert HTTPS expliciet. Het template vermeldt voortaan zowel het protocol als de interne webpoort 8443.
  • De juiste hostpoort wordt gekozen. Server Up koppelt de interne webpoort aan de ingestelde hostpoort, ook wanneer een databasepoort eerder in het Compose-bestand staat.
  • Bestaande installaties werken mee. Ontbrekende installatiemetadata wordt uit het actuele bronsjabloon aangevuld; opnieuw installeren is niet nodig.
  • Eigen IP-adressen blijven correct. Een UniFi-stack met een eigen IP opent als https://<app-ip>:8443.

v0.8.17-beta — Elf nieuwe apps voor beheer, automatisering en monitoring

De app-store bevatte nog weinig lichte infrastructuurapps die zowel op een reguliere x86-server als op een 64-bits Raspberry Pi werken. Daarom zijn elf nieuwe templates toegevoegd met expliciete AMD64- en ARM64-ondersteuning.

  • Remote beheer: RustDesk Server, MeshCentral en Apache Guacamole. Guacamole genereert bij de eerste start automatisch het officiële PostgreSQL-schema.
  • Identiteit en discovery: LLDAP en Consul bieden respectievelijk een lichte LDAP-directory en service discovery met DNS en healthchecks.
  • Monitoring: VictoriaMetrics en OpenObserve voegen efficiënte opslag toe voor metrics en voor logs, metrics en traces.
  • Automatisering: Activepieces en Windmill bieden visuele workflows, scripts en interne apps. De Windmill-worker krijgt standaard geen toegang tot de Docker-socket.
  • Bestandsoverdracht: PairDrop maakt lokale bestandsoverdracht vanuit de browser mogelijk.
  • Nomad als veilig control-plane: de Nomad-container draait alleen als server met webinterface. Workload-clients horen rechtstreeks op de hosts te worden geïnstalleerd, omdat HashiCorp Nomad-clients in Docker niet officieel ondersteunt.
  • Cataloguscontrole: gerichte tests bewaken dat alle elf apps beschikbaar blijven voor AMD64 en ARM64, dat Nomad geen containerized client wordt en dat Guacamole zijn database vóór de webapp initialiseert.

v0.8.16-beta — De startwizard blijft gesloten

De knop Sluiten verborg de optionele startwizard alleen tijdelijk. De server kreeg daardoor nooit door dat de gebruiker de wizard had overgeslagen en opende hem bij iedere nieuwe paginalading opnieuw.

  • Sluiten wordt blijvend onthouden. De knop markeert de wizard nu via de API als afgerond en sluit pas nadat de server dit heeft opgeslagen.
  • Een ontbrekende appmap reset de wizard niet meer. Een nog niet gemounte of tijdelijk onbereikbare library wordt als padprobleem behandeld en overschrijft niet langer een expliciete keuze van de gebruiker.
  • Bewust opnieuw starten blijft mogelijk. De knop “Opnieuw uitvoeren” in Instellingen wist de voltooiingsstatus en opent de wizard weer vanaf het begin.

v0.8.15-beta — Apps verwijderen en mobiele schermen werken weer

De verwijderknop stopte containers en verwijderde volumes, maar liet de appconfiguratie staan. Daardoor bleef de app in Server Up zichtbaar en leek de knop niets te doen. Daarnaast konden modals en instellingen op een klein scherm buiten de viewport vallen zonder dat de inhoud bereikbaar was.

  • Verwijderen doet nu wat de keuze zegt. De veilige keuze verwijdert de appconfiguratie, maar bewaart appdata en named volumes. De expliciete keuze “app en alle data verwijderen” wist ook appdata en niet-externe volumes.
  • Overgenomen apps zijn verwijderbaar. Bij een app die als symlink is gekoppeld wordt alleen de Server Up-verwijzing weggehaald; de externe Compose-map van de gebruiker blijft bestaan.
  • Duidelijke voortgang en fouten. De dialoog onthoudt de juiste app, voorkomt dubbele opdrachten en toont zowel succes als backendfouten.
  • Modals passen op ieder mobiel scherm. Dialogen en de setupwizard gebruiken de dynamische viewporthoogte, houden kop en knoppen in beeld en maken de inhoud zelfstandig scrollbaar.
  • Instellingen blijven binnen de schermbreedte. Brede keuzerijen kunnen horizontaal scrollen, kaartkoppen lopen om en invoervelden mogen binnen hun grid krimpen. Ook het loginvenster blijft op een laag scherm bereikbaar.

v0.8.14-beta — Raspberry Pi krijgt werkende en eerlijke apps

De UniFi-template startte een MongoDB-container, maar maakte de database- gebruiker en rollen die UniFi nodig heeft niet aan. Daardoor bleef de controller zonder werkende database hangen. Tegelijk kon de app-store op een Raspberry Pi ook images aanbieden die uitsluitend voor AMD64 bestaan; dat werd pas zichtbaar nadat Docker de pull weigerde.

  • UniFi initialiseert MongoDB volledig. Een init-script maakt de UniFi- gebruiker met de voorgeschreven rollen aan, beide containers gebruiken authSource=admin en UniFi wacht op de database-healthcheck.
  • Geschikt voor een 64-bits Raspberry Pi 4. MongoDB staat vast op 4.4.29: deze tag heeft een ARM64-image en vermijdt de ARMv8.2-eis van MongoDB 5 en hoger. De hoofdimage ondersteunt AMD64 en ARM64.
  • Platformcontrole vóór installatie. Templates kunnen hun ondersteunde Docker-architecturen vastleggen. De catalogus legt een mismatch uit, schakelt de knop uit en de API weigert dezelfde installatie ook rechtstreeks.
  • Elf AMD64-only apps zijn gemarkeerd. Alle 318 gebruikte image-manifests zijn gecontroleerd. Cal.com, ComfyUI, Dawarich, Focalboard, Formbricks, Mattermost, Pinchflat, Pixelfed, TubeArchivist, Unbound en Wazuh bevatten momenteel minstens één image zonder ARM64-variant en worden daarom niet meer als installeerbaar op een Pi getoond.
  • Init-scripts houden hun uitvoerrecht. Tekstbestanden die tijdens het renderen worden geschreven nemen voortaan de bestandsmodus van het sjabloon over.

Een Mongo-init-script draait alleen bij een lege database. Server Up verwijdert bestaande data daarom niet automatisch. Verwijder bij een eerdere mislukte UniFi-installatie eerst bewust het oude Mongo-volume, of installeer onder een nieuwe instantienaam.

v0.8.13-beta — De update haalt nu de juiste release op

Bijwerken vanuit de interface vroeg Docker om een image dat niet bestond. Forgejo noemt een release v0.8.13, terwijl de workflows bewust image :0.8.13 publiceren. De interface stuurde de Git-tag mét v door en dus mislukte iedere beta- en stable/main-update bij docker pull.

  • Eén expliciete registry-tag. De updatecheck geeft naast de Git-tag nu image_tag terug. Frontend, endpoint en pull-laag normaliseren allemaal de ene voorloop-v. De workflows publiceren ook :v<versie> als tijdelijke compatibiliteitstag, zodat de al geïnstalleerde defecte updater zichzelf naar deze reparatie kan brengen.
  • De herstart gebruikt exact het bestaande Compose-project. Projectnaam, projectmap en alle configuratiebestanden komen uit de containerlabels. --no-build --pull never voorkomt dat Compose tijdens de update oude broncode bouwt of onverwacht een andere tag ophaalt.
  • Tag en versie blijven bij elkaar. De helper en beide deploy-workflows schrijven SU_TAG en SU_VERSION samen. Ook na een handmatige Compose- herstart toont en draait Server Up daardoor dezelfde beta- of releaseversie.
  • Terugrollen blijft naar een echte versie wijzen. Als de container via de beweeglijke alias beta of latest draaide, wordt voortaan het ingebakken OCI-versielabel als terugrolpunt onthouden.

v0.8.12-beta — De image-controle van twaalf minuten naar seconden

De controle of elk image nog bestaat kostte elf à twaalf minuten, bij élke push, ook als er geen enkel sjabloon was gewijzigd. Gemeten: 709 seconden, terwijl de netwerktijd voor 318 images maar zo'n 48 seconden is.

Twee dingen zaten fout.

Het aantal tegelijk gold in totaal in plaats van per registry. Registries knijpen per host af, en tweederde van de catalogus staat op Docker Hub. Met vier tegelijk in totaal stonden ghcr, lscr en quay dus achter Docker Hub in de rij te wachten zonder dat dat iets oploste. Nu geldt de limiet van vier per registry en lopen registries naast elkaar.

Elke ronde controleerde alles opnieuw. Geslaagde controles blijven nu een week staan, met een vaste spreiding per image zodat niet de hele catalogus op dezelfde dag verloopt. De sleutel is de volledige naam mét tag: een sjabloon dat naar een andere tag gaat wijzen levert een nieuwe sleutel op en wordt dus altijd gecontroleerd. Mislukkingen komen er nooit in — een ontbrekend image blijft elke ronde gemeld tot het is opgelost, want daar is deze controle voor.

Gemeten na de wijziging:

koude cache   318 gecontroleerd     43 s
warme cache     0 gecontroleerd      0 s

Met --alles negeer je de cache en wordt alles opnieuw nagelopen.

v0.8.11-beta — Overnemen en verwijderen

Handmatig gestarte apps stonden in de lijst, maar je kon er alleen mee starten, stoppen en logs bekijken. Nu ook:

Overnemen. Draait er een compose-app die je buiten Server Up om hebt gestart, dan trek je hem er met één knop bij. Er wordt niets verplaatst of gekopieerd: de stackmap wordt een verwijzing naar de map die je al hebt. Kopiëren zou de relatieve paden in je compose-bestand breken — die worden opgezocht vanaf de map waar dat bestand staat, en dat zou dan een andere zijn.

Kan Server Up niet bij die map — hij ziet alleen wat via BASE_DIR gekoppeld is — dan zegt hij dat, met waar je hem neer moet zetten. Overnemen zonder erbij te kunnen heeft geen zin.

Verwijderen. Voor een container die niet bij een app hoort. De vraag vermeldt dat alleen de container weggaat, niet zijn gegevens of zijn image.

Hoort hij bij een compose-project, dan staat er bovendien bij dat hij terugkomt zodra dat project ergens anders start, en dat overnemen waarschijnlijk is wat je zoekt. Dat is de reden dat de overnemen-knop links van de verwijderknop staat: verwijderen lost dat geval niet op.

Een overgenomen app verwijderen haalt alleen de verwijzing weg. Jouw map blijft staan — shutil.rmtree zou daar trouwens sowieso op weigeren ("Cannot call rmtree on a symbolic link") en de verwijdering halverwege laten stranden.

v0.8.10-beta — Een repo verwijderen mag niet struikelen

DELETE /api/repos/<id> las r["id"] zonder controle. Staat er één regel zonder id in je configuratie, dan gaf verwijderen een 500 — dezelfde fout die GET /api/repos had en die de hele app store leeg liet. Nu wordt zo'n regel overgeslagen.

Verwijderen haalt alle regels met die id weg, dus een dubbele vermelding is in één klik opgeruimd.

v0.8.09-beta — Twee repo's met dezelfde naam braken de app store

In een configuratie met twee repo's die dezelfde id dragen, tekende de app store geen kaarten terwijl de knop "Nog N tonen" een dalend getal liet zien. Klikken verlaagde het getal met zestig en er verscheen niets.

De interface tekent de repolijst met :key="repo.id". Dubbele keys zijn voor Alpine ongedefinieerd gedrag — je krijgt ontbrekende of verkeerde blokken. De tellers rekenen ondertussen gewoon door over de gegevens, dus je ziet een getal onder een leeg scherm en nergens een fout.

GET /api/repos geeft een id nu nog maar één keer terug. Dat is geen schoonheidsreparatie: zolang er twee in zaten, was de hele storepagina stuk.

v0.8.08-beta — Paden rechtzetten en er verandert niets

Zet je in Instellingen → Paden je mappen goed, dan bleef de appslijst leeg. Niet omdat er iets mis was — de API leverde de apps gewoon — maar omdat de pagina ze nooit opnieuw ophaalde. goto() wisselt alleen van pagina, en saveSettings() verversde niets. Je keek dus naar de lijst zoals die bij het laden van de pagina was: leeg. Een herlaadactie loste het op, als je dat wist.

Twee dingen daaraan:

  • Verandert een pad, dan worden de appslijst en de containers opnieuw opgehaald. Alleen bij een padwijziging, zodat een gewone instelling niet telkens twee verzoeken kost.
  • Een geweigerde opslag meldde zich als "opgeslagen". saveSettings() gooide het antwoord van de server weg en zette onvoorwaardelijk het vinkje. Sinds v0.8.05 een pad buiten de gekoppelde map weigert, is dat het verschil tussen "aangepast" en "er is niets gebeurd" — en je zag alleen het vinkje. De foutmelding van de server komt nu in beeld.

v0.8.07-beta — "Rondgang geslaagd" en toch een rode job

De backup-rondgang doorliep alle vijf de stappen goed — dump, archief, alles weggooien, terugzetten, eigenaar bewaard, database schrijft weer — en de CI-stap viel er daarna alsnog op om. Zonder melding: tussen "Rondgang geslaagd" en het einde van de stap zaten tweeënhalve seconde en verder niets.

Dat is het opruimblok. Dat hoort de uitslag niet te bepalen: of een testcontainer zich laat verwijderen zegt niets over of backups werken. Een fout daar liet nu de hele stap vallen, en omdat er niets werd afgedrukt was er ook niets aan te zien.

  • opruimen kan de uitslag niet meer bepalen; mislukt het, dan staat dát er
  • het script eindigt expliciet met exitcode 0, zodat er geen twijfel over de afloop kan bestaan
  • de CI drukt de exitcode van de stap af. Zonder dat getal was niet te achterhalen waar een rode job vandaan kwam terwijl het scherm groen was

De postgres-race uit v0.8.06 is hiermee bevestigd opgelost: de rondgang liep in één keer door.

v0.8.06-beta — De backuptest in de CI struikelde over postgres

Vier CI-runs op rij faalden, en niet op iets in Server Up: de backup-rondgang die sinds v0.7.96 in de CI draait, viel om op

psql: connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed

Het officiële postgres-image start bij een eerste run twee servers: eerst een tijdelijke op de socket om de init-scripts te draaien, die daarna stopt, en pas dan de echte. pg_isready meldt tijdens die tijdelijke server al "klaar". Kwam de volgende opdracht net in het gat daartussen, dan was de socket weg. Een race, dus soms goed en soms niet.

De wachtlus kijkt nu niet meer naar pg_isready maar voert echte query's uit, en gaat pas door na twee geslaagde achter elkaar — één toevallige treffer op de tijdelijke server overleeft dat niet. Bij een time-out staat de laatste fout erbij in plaats van alleen "kwam niet op".

Verder wordt een achtergebleven testcontainer nu vóór de start opgeruimd. Een geannuleerde run draait zijn opruimblok niet, en een half geïnitialiseerde datamap levert daarna raadselachtige fouten op.

De deploy zelf werkte al die tijd gewoon; deze stap draait er ná en liet alleen de job rood kleuren.

v0.8.05-beta — Een onbereikbaar pad liet zich gewoon opslaan

Dit is de oorzaak van de lege appslijst waar de vorige vier uitgaven omheen hebben gezocht.

LIBRARY_DIR stond op /srv/serverup/stacks, terwijl alleen /opt/serverup in de container gekoppeld is. Dat pad bestaat binnen de container niet eens — dus geen enkele app in de lijst, en alle containers onder "handmatig gestart", want beheerd leest dezelfde map. Nergens stond waarom.

De controle daarop bestond al sinds v0.7.92: paden.bereikbaar() weet dat de container alleen bij paden onder BASE_DIR kan, en geeft de exacte regel die je in .env moet zetten. Hij hing alleen aan het veld Hoofdmap en aan het verhuizen. De drie padvelden eronder schrijven rechtstreeks naar PUT /api/settings — en daar stond geen wacht. Een pad buiten de koppeling liet zich dus gewoon opslaan, waarna alles stilviel.

  • PUT /api/settings weigert nu een LIBRARY_DIR, DATA_DIR of BACKUP_DIR waar Server Up niet bij kan, met dezelfde uitleg: welke regel in .env, en welk commando erna. Een ongewijzigd pad blokkeert niets, anders kun je met een al kapotte instelling niets meer wijzigen.
  • Is de lijst leeg, dan zegt het scherm nu waarom: niet "Geen apps gevonden", maar dat Server Up er niet bij kan en wat eraan te doen is.

v0.8.04-beta — --doctor vraagt het de app in plaats van het af te leiden

Drie keer op rij heb ik de verkeerde oorzaak aangewezen voor een lege appslijst — een verouderde koppeling, submounts, ontbrekende compose-bestanden — omdat --doctor niet genoeg vertelde om ertussen te kiezen. Hij telde regels met ls, en dat zegt niets over wat de app zelf ziet.

Nu vraagt hij het de app, en toont:

  • het pad letterlijk (repr), dus met een spatie of regeleinde er zichtbaar in. Zo'n onzichtbaar teken maakt een pad onvindbaar terwijl het er in een log volkomen normaal uitziet
  • of de app die map ook echt vindt — zo niet, dan is dat een fout met de aanwijzing waar je moet kijken
  • per map of hij als app meetelt, en zo niet: waarom. Een map zonder compose-bestand telt niet mee, verdwijnt uit de lijst, en laat zijn containers onder "handmatig gestart" belanden

Dat is het verschil tussen "Server Up ziet geen enkele stack" en weten wélke map om welke reden afvalt.

v0.8.03-beta — Mappen zonder compose-bestand verdwenen geruisloos

GET /api/stacks sloeg elke map zonder compose-bestand over met een kale continue. Staat er zo'n map in je stacks-map, dan duikt hij nergens meer op: niet in de lijst, en de containers die erbij horen belanden onder "handmatig gestart" omdat ook beheerd op datzelfde compose-bestand afgaat. Je ziet een leeg scherm, en nergens staat waarom.

De interface hééft er een weergave voor — rode stip, een streepje in plaats van een status, geen knoppen — maar die kon door dat continue nooit te zien zijn. Dode weergavecode voor een toestand die de API niet kon opleveren.

Zulke mappen staan er nu gewoon tussen, mét die waarschuwing. Beter een app die zegt dat er iets mis is dan een app die verdwijnt.

v0.8.02-beta — Koppelingen ónder BASE_DIR

--doctor zei dat /opt/serverup netjes gekoppeld was, en dat klopte ook. Alleen zag Server Up er geen enkele app, terwijl er op de host drie stonden.

De verklaring zit een niveau dieper. Docker bindt met propagation rprivate: een koppeling die op de host het aanmaken van de container ontstaat, komt daar niet doorheen. Is stacks (of appdata, of backups) een eigen mount — een aparte dataset, een tweede schijf, een netwerkmap — dan ziet de container de lege map eronder. De koppeling van BASE_DIR zelf is intact, dus daar valt niets aan op te merken; het scherm blijft alleen leeg.

--doctor loopt nu met findmnt alle koppelingen onder BASE_DIR langs en vergelijkt ze met wat de container in zijn eigen mountlijst heeft. Ontbreekt er een, dan zegt hij welke, en hoe je hem meekoppelt:

✖ /opt/serverup/stacks is op de host een eigen koppeling, maar niet in de container
  De container ziet daar de lege map eronder. Koppel hem apart mee in
  docker-compose.yml:
      - /opt/serverup/stacks:/opt/serverup/stacks

v0.8.01-beta — "Nog 106 tonen" onder een leeg scherm

De app store toonde geen enkele kaart, met daaronder een knop die beloofde dat er nog 106 apps wachtten. Twee bronnen die uit elkaar konden lopen: het raster liep over de repolijst, de teller over de opgehaalde apps. Waren die twee het oneens, dan stond er een getal onder een leeg scherm — zonder dat er ergens een foutmelding te zien was.

  • Teller en raster komen nu uit dezelfde lijst. De knop kan niets meer beloven wat er niet staat.
  • Een lege store zegt nu wát er aan de hand is: geen repository toegevoegd, of geen apps die aan het filter voldoen. Eerder bleef het scherm gewoon leeg.
  • GET /api/repos gaf een 500 op een repo zonder id. Eén rare regel in de configuratie maakte daarmee de hele store leeg; zulke regels worden nu overgeslagen.

v0.8.00-beta — Een kapotte cache herstelt zichzelf

fatal: shallow file has changed since we read it liet een repo achter die zich niet meer liet bijwerken — en daarmee een app store die leeg bleef.

Die melding komt van twee git fetch in dezelfde map. Bij het opstarten synchroniseert Server Up alles, en ondertussen kan je op Synchroniseren drukken of een app installeren die de repo ook aanraakt. Er zat geen slot op: elke synchronisatie kon over een andere heen lopen en de cache halverwege achterlaten, met een .git/shallow waar git niets meer mee kan.

Twee dingen erbij:

  • Eén git tegelijk per repo. Botsen kan nu niet meer.
  • Zelfherstel. Lukt het bijwerken niet — om welke reden dan ook — dan wordt de cache weggegooid en opnieuw opgehaald. Er staat niets in dat bewaard hoeft te blijven, dus dat is het juiste antwoord op élke fout hier, en niet alleen op deze. Een repo die zich niet meer laat bijwerken kan daarmee niet meer bestaan.

Dat repareert ook een cache die al stuk is: de eerstvolgende synchronisatie haalt hem gewoon opnieuw op.

v0.7.99-beta — "Gekoppeld" zei niets

--doctor meldde vrolijk dat /opt/serverup in de container gekoppeld was, terwijl Server Up daar geen enkele app zag en er buiten de container drie stonden. De controle keek of het pad in /proc/self/mountinfo voorkwam, en dat is precies de verkeerde vraag.

Een bind mount wijst naar een inode, niet naar een pad. Wordt de map op de host vervangen nadat de container is aangemaakt — verwijderd en opnieuw aangemaakt — dan blijft de container naar de oude, losgekoppelde map kijken. Meestal is die leeg. Het pad staat dan nog gewoon in de mountlijst, dus "gekoppeld" blijft er stralend staan terwijl je hele appslijst leeg is.

--doctor telt nu aan beide kanten en vergelijkt:

  • bestaat de map binnen de container überhaupt?
  • staan er buiten wél apps en binnen niet? Dan is de koppeling verouderd, en staat erbij hoe je hem herstelt (docker compose up -d --force-recreate)
  • is hij aan beide kanten leeg, dan is er simpelweg nog niets geïnstalleerd — een waarschuwing, geen fout

v0.7.98-beta — Het heet nu gewoon een app

Je installeerde een app uit de App Store en kreeg een stack. Dat moest je maar net weten. In de interface heet alles nu een app: 24 teksten per taal plus de terugvalteksten in de pagina zelf.

De code, de API-paden (/api/stacks/…), de mapnamen en .serverup.json houden stack. Dat zijn geen woorden die iemand leest, en ze hernoemen zou opgeslagen gegevens en URL's breken zonder dat er iets duidelijker van wordt.

De tweede lijst is verdwenen. "Overige containers" stond als aparte sectie onderaan, met een eigen kop, een eigen uitleg en een eigen vormgeving. Wie zijn apps kwijt was moest weten dat die lijst bestond. Handmatig gestarte containers staan nu gewoon tussen de rest, als kaart, met het label Handmatig gestart. Er is minder mee te doen — starten, stoppen, logs — maar ze zijn wel te vinden. De zoekterm werkt op allebei; een categoriefilter laat ze vallen, want ze hébben geen categorie.

Onderweg gevonden: de waarschuwing bij backups zei nog dat een backup "niet de Docker-volumes met de eigenlijke appdata" bevat. Sinds v0.7.88 zit de appdata er juist wél in, met een dump van elke database erbij. Die tekst klopt nu.

v0.7.97-beta — Repo's bijwerken werkte niet meer

fatal: Cannot rebase onto multiple branches. Het bijwerken van een app-repo gebruikte git pull --rebase --autostash, en dat commando leunt op de upstream-instellingen van de lokale kloon. Staan er meerdere merge-doelen in branch.<tak>.merge, dan stopt git ermee en werkt er niets meer bij — ook de app store niet, want die leest uit diezelfde cache.

Bij het naspelen bleek er een tweede fout in dezelfde regel te zitten: git pull zonder argumenten haalt de tak op waarop ooit gekloond is. De branch uit de repo-instellingen werd dus genegeerd. Zette je die van main naar dev, dan bleef je stilzwijgend main krijgen.

De cache is van Server Up zelf; er staat niets in dat bewaard hoeft te blijven. Het bijwerken haalt nu de ingestelde tak op en zet de cache daar hard op. Dat is ongevoelig voor ontspoorde upstream-instellingen, volgt een gewijzigde branch wél, en laat zich niet blokkeren door lokale wijzigingen in de cache.

--doctor vraagt Server Up nu zelf naar zijn paden. Wat er op de host staat is één ding; waar de app kijkt is een ander, en die twee kunnen uit elkaar lopen — een LIBRARY_DIR uit een oude configuratie wint van de standaard. Dan zie je een lege stacklijst zonder dat ergens staat waarom. Hij toont nu welke LIBRARY_DIR en DATA_DIR de app gebruikt, hoeveel stacks hij daar ziet, en hoeveel apps er in de store-cache zitten.

v0.7.96-beta — Backups eindelijk écht getest, en terugrollen

Backups waren nooit tegen een echte Docker-daemon gedraaid. Alle backuptests vervangen Docker door een nepversie — vijftien keer in test_backups.py alleen al. Dat toetst de logica maar niet de keten waar het om gaat, en juist dáár betekent fout zijn gegevensverlies.

tools/backup_rondgang.py draait die keten nu wel echt, in de CI, op het image dat ook in productie draait: een stack met een postgres-sidecar starten en vullen, backuppen, de tabel en de appdata weggooien, terugzetten, en controleren dat de rij er weer staat én dat de database nog kan schríjven. Dat laatste is de vraag waar het om draait — een datamap van de verkeerde eigenaar geeft pas bij de eerste schrijfactie een fout.

Dat bracht meteen een fout aan het licht die de unittests niet konden zien: het terugzetten gaf de appdata de verkeerde eigenaar. tarfile met filter="data" laat uid en gid vallen, en de terugval was de PUID uit de metadata — of anders onze eigen uid. Maar een postgres-container draait als uid 70 en zegt dat nergens in de metadata. Het archief wéét het wel: die uid staat er gewoon in. Die gaat nu voor, met de PUID als tweede en onze eigen uid als laatste antwoord.

Een mislukte --update rolt nu terug. De commit van vóór het bijwerken wordt onthouden; komt de nieuwe versie niet omhoog, dan gaat de broncode terug en start die opnieuw. Je houdt geen stilstaande server over en geen rijtje commando's dat je zelf moet uitvoeren.

Installeren gaat sneller als er een registry-image is. Staat er een SU_IMAGE met een registrypad in .env, dan haalt het script dat op in plaats van zelf te bouwen — dat scheelt de paar minuten die een build kost. Zonder blijft het bouwen, want dan bestaat het image nergens anders.

Nieuw: --reconfigure. Bind-adres, poort, account of hoofdmap wijzigen zonder de broncode aan te raken. Daarvoor moest je --update draaien, en dan haalde je ongewild ook nieuwe code binnen.

Nieuw: --uninstall --purge. Verwijdert ook het su-data-volume, je gegevensmap en het aangemaakte account — per onderdeel apart gevraagd, met de omvang van de gegevensmap erbij. Zonder --purge somt --uninstall nu op wat er blijft staan, inclusief het account, in plaats van dat te verzwijgen.

Verder:

  • --dir wordt net als --base-dir op een volledig pad gecontroleerd. Met curl … | sh is de werkmap je thuismap, en dit pad gaat letterlijk naar compose.
  • Het tijdelijke bestand voor het aanmaken van de beheerder viel terug op /tmp/su-setup.$$ als mktemp faalde. Die naam is voorspelbaar en /tmp is voor iedereen schrijfbaar.
  • controleer_docker deed twee rondgangen naar de daemon (docker info én docker version) waar er één volstaat.

v0.7.95-beta — Bijwerken vanuit de interface, en een script dat nakijkt

Het tweede deel van de controle op het installatiepad.

Bijwerken vanuit de interface kon nooit werken. selfupdate schreef SU_TAG naar <werkmap>/.env — maar die map is nergens in de container gemount. Er zijn maar drie mounts: de docker-socket, het su-data-volume en BASE_DIR. De installatiemap zit daar niet bij, dus schrijven mislukte. Dat viel niet op omdat een zelfgebouwd image de knop toch al uitschakelt; zette je SU_IMAGE naar een registry — de manier die docs/updates.md noemt om hem aan te zetten — dan faalde hij altijd. De helper-container mount die map wél en draait als root, dus die zet de tag nu.

Een vastgepinde versie kon nooit bijgewerkt worden. --branch v0.7.92-beta installeerde prima, maar bijwerken deed git reset --hard origin/v0.7.92-beta en tags krijgen geen origin/-ref: "fatal: ambiguous argument". Het resetten gaat nu naar FETCH_HEAD, wat voor takken en tags allebei klopt. De test liet zien dat het na een tag-installatie óók voor takken stuk was.

De token stond in .git/config en in ps. In de URL schrijft git hem verbatim in de config, waar hij blijft staan; als argument staat hij in /proc/<pid>/cmdline en leest elke gebruiker op de server hem met ps. Dat is precies waarom dit script --admin-password weigert. Hij gaat nu via een bestand met modus 0600 dat na afloop weer weg is — git leest het via een credential-helper, curl via --config; alleen het pad staat op de opdrachtregel.

Twee nieuwe opties:

  • --base-dir PAD zet de hoofdmap voor stacks, appdata en backups bij de installatie, in plaats van achteraf in .env — waarna je de data alsnog moet verhuizen. Relatieve paden en systeemmappen worden geweigerd.
  • --doctor licht een bestaande installatie door zonder iets te wijzigen: container, bereikbaarheid op het ingestelde adres, onder welk account hij draait versus wat er in .env staat, of BASE_DIR echt gekoppeld is (bestaan is niet genoeg), de rechten van .env, en hoeveel ruimte er over is. Exitcode 1 bij fouten. De CI draait hem na elke deploy tegen de echte installatie — ruim honderd regels shell die je nergens anders getest krijgt.

v0.7.94-beta — Het installatiescript loog op drie plekken

Een controle van het hele installatiepad leverde drie fouten op die alledrie op curl … | sh liggen — de regel die de README noemt.

Vragen werden niet gesteld en beantwoordden zichzelf met "ja". vraag() keek naar stdin, en bij curl … | sh is dat de pipe. Alle andere prompts in het script gebruiken /dev/tty, dat gewoon bereikbaar is. Gevolg: "Nu installeren via het officiële script van docker.com?" werd overgeslagen en met ja beantwoord, --uninstall brak je installatie af zonder bevestiging, en de vraag of je een beheerdersaccount wilde bleef ongesteld terwijl de vervolgprompt ("Gebruikersnaam:") wél verscheen. Er is nu één terminal_beschikbaar() die alle vijf de plekken gebruiken.

Met een eigen bind-adres mislukte de healthcheck én het beheerdersaccount. Compose publiceert op $BIND, maar het script vroeg altijd 127.0.0.1. Met --bind 10.0.20.5 luistert daar niets, dus meldde het "reageerde niet binnen anderhalve minuut" en daarna "Server Up was niet bereikbaar; account niet aangemaakt" — terwijl alles draaide. Je hield een installatie over zonder account, precies het venster waar het script voor waarschuwt. Het controleadres volgt nu $BIND, met blokhaken om IPv6.

Een mislukte update meldde zich als geslaagd. wacht_op_gereed || true gevolgd door goed "Bijgewerkt." maakte een kapotte update niet te onderscheiden van een goede, ook niet aan de exitcode. Nu stopt hij met een foutmelding en de weg terug. Bij een verse installatie gaat hij wél door — die aanwijzingen zijn juist dán nuttig — maar het slot zegt eerlijk dat er niet geantwoord is en de exitcode is 1.

Verder:

  • curl kreeg tijdslimieten bij het pollen. Zonder die limieten bleef hij hangen op een adres waar niets naartoe routeert en betekende het aantal pogingen niets. Het aantal is nu ook instelbaar met SU_WACHT_POGINGEN, voor trage machines.
  • Het slot zei nog "Server Up beheert Docker als root" en noemt nu onder welk account het draait.

Onderweg gevonden en meteen verholpen: de eerste versie van terminal_beschikbaar() gebruikte { : </dev/tty; }. : is een special builtin, en een mislukte redirect daarop beëindigt volgens POSIX de hele shell — exit 2, zonder melding, nog vóór de eerste stap. Dat brak elke installatie zonder terminal. De openpoging staat nu in een subshell.

v0.7.93-beta — Server Up draait niet meer als root

Er stond user: "0:0" in docker-compose.yml en nergens waarom. Dat is nu een keuze die je bij de installatie maakt.

Bij het installeren wordt gevraagd onder welk account het moet draaien: een nieuw systeemaccount serverup (aanbevolen, wordt aangemaakt zonder shell en zonder wachtwoord), het account waarmee je werkt, een bestaand account uit een lijst, of root zoals voorheen. Met --user NAAM of --yes sla je de vraag over. Een bestaande installatie krijgt de vraag één keer bij --update; daarna blijft je keuze staan.

De container start nog steeds als root en zakt daarna af. Dat moet: Docker maakt het su-data-volume als root aan, en een container die meteen als een gewone gebruiker start komt niet eens tot zijn configuratie. Het nieuwe docker-entrypoint.sh zet /data, stacks en backups klaar en gebruikt dan setpriv — niet gosu, want een proces dat met setuid afzakt verliest zijn capabilities, en die zijn hier nodig. appdata blijft ongemoeid: die mappen zijn van de apps zelf, en een chown daaroverheen breekt precies de containers die als hun eigen uid draaien.

Het afgezakte proces houdt CHOWN, DAC_OVERRIDE en FOWNER. Alle drie zitten al in de standaardset van Docker, dus de container krijgt geen enkel recht bij — er gaan er alleen af. Wat dat wél en níét oplevert staat in docs/beveiliging.md; kort gezegd beschermt het tegen bugs en ongelukken, niet tegen iemand die de interface overneemt. Die heeft de docker-socket, en dat blijft root. Een socket-proxy helpt daar niet tegen, omdat het dóél van deze app is om containers met willekeurige bind mounts aan te maken.

Twee rechten-fouten die hierdoor aan het licht kwamen, zijn ook verholpen:

  • Het installatieformulier zette PUID/PGID op wat het sjabloon toevallig noemde — 44 sjablonen staan blind op 1000. Nu krijgen ze de ids waaronder Server Up draait, zodat appdata van hem is en hij hem kan inpakken.
  • Eén onleesbaar bestand liet de héle backup falen: de tar.add van de appdata-map zat in dezelfde try als de rest. Nu gaat de rest gewoon mee en komt wat ontbreekt in het log én in de metadata (skipped) te staan. Een archief dat compleet lijkt maar het niet is, is erger dan een archief dat zegt wat het mist.
  • Terugzetten gaf alles aan wie uitpakte: tarfile met filter="data" (PEP 706) laat uid en gid uit het archief vallen. De appdata krijgt nu de PUID/PGID uit de metadata terug, anders die van Server Up zelf.

De CI controleert vanaf nu in een echte container dat setpriv bestaat, dat er wordt afgezakt, dat de gid van de docker-socket meekomt, dat /data schrijfbaar is, en dat SU_UID=0 root laat blijven.

Terug naar de oude situatie: zet SU_UID=0 en SU_GID=0 in je .env en draai docker compose up -d.

v0.7.92-beta — Eén hoofdmap, en je data mag mee verhuizen

Server Up stelde /opt/serverup voor, wat je ook instelde. Dat was geen weergavefout maar drie fouten tegelijk:

  • LIBRARY_DIR, DATA_DIR en BACKUP_DIR stonden hard op /opt/serverup/* en trokken zich niets aan van de BASE_DIR die docker-compose.yml wél kent. Ze worden nu afgeleid van BASE_DIR, en compose geeft die variabele voortaan ook aan de container door — die stond alleen in de mount, dus je kon /srv koppelen terwijl de app /opt/serverup bleef voorstellen.
  • 266 van de 286 sjablonen hebben /opt/serverup/appdata als standaard voor appdata_dir. Het installatieformulier overschreef wel de tijdzone met die van de server, maar niet de appdata-map. Wat je ook had ingesteld, je data belandde in /opt. Dat is nu één plek in _annotate_fields(), en daarmee meteen goed voor alle sjablonen.
  • .env.example en de documentatie noemden /opt/serverup als vast gegeven.

Instellingen → Paden werkt nu met één hoofdmap. Typ je er een, dan verschijnen de drie submappen eronder als voorstel, elk met een knop om hem over te nemen, en één knop Alles overnemen. De velden blijven los aanpasbaar. /api/paths/check controleert onderweg of Server Up er überhaupt bij kán: de container ziet alleen wat via BASE_DIR gekoppeld is, en daarbuiten schrijft hij naar zijn eigen overlay — dat lijkt te lukken en levert daarna lege apps op. Ligt je map erbuiten, dan noemt de melding de regel voor .env en het commando, in plaats van "kon niet opslaan".

Een gewijzigd pad biedt aan de data mee te verhuizen. Alleen de instelling aanpassen liet je data staan waar hij stond, en elke stack wees nog naar zijn oude appdata-map. De verhuizing stopt de draaiende stacks, kopieert met een voortgangsbalk, controleert aantal en omvang, schrijft de paden om in docker-compose.yml, .env en de metadata van elke stack, start de stacks weer op de nieuwe plek — en ruimt pas daarna het oude op. Klopt de controle niet, dan blijft het origineel staan en is de melding de fout. Het omschrijven kijkt naar hele pad-segmenten, zodat /opt/serverup vervangen /opt/serverup-oud met rust laat.

Rechten én eigenaar gaan mee. shutil.copy2 neemt de rechten over maar niet de eigenaar, en Server Up draait als root — verhuisde appdata werd dus root:root, waarna een container die als PUID 1000 draait niet meer bij zijn eigen gegevens kon. De linuxserver-images zetten /config bij het starten terug, maar postgres, Nextcloud en Immich lopen er gewoon op vast. De verhuizing chownt nu elk bestand, elke map, elke symlink én de hoofdmap zelf naar de eigenaar van het origineel. Lukt dat niet, dan gaat de verhuizing door met een waarschuwing in het log.

Jobs kregen daarvoor een voortgang(); de terminal toont de balk boven het log.

v0.7.91-beta — "Poort {port} is vrij" stond er letterlijk

De poortcontrole werd aangesloten op vertaalsleutels die er al waren, en twee daarvan verwachten een plaatshouder die niet werd meegegeven: port_free wil {port}, port_in_use wil {port} én {by}. In beeld stond dus letterlijk "Poort {port} is vrij".

Er draait nu een test die elke t('sleutel', { … }) in de interface naloopt en vergelijkt met de plaatshouders in de Nederlandse tekst — in beide richtingen, en ook voor aanroepen zónder variabelen. Dat laatste was nodig: de eerste versie van die test keek alleen naar aanroepen die wél iets meegaven, en liet de fout waar hij voor gemaakt was gewoon passeren.

v0.7.90-beta — Poorten kiezen zonder gokken, en installeren op een telefoon

De poortcontrole zat er wel, maar werd nooit gevraagd. /api/ports/check bestaat en kijkt naar wat Docker publiceert én naar wat er rechtstreeks op de host luistert — dat laatste is precies de botsing die je anders pas ziet als de stack niet start. Het installatieformulier riep hem alleen nergens aan. Er stond hooguit een regeltje als de stándaardwaarde bezet was; typte je zelf iets, dan hoorde je niets meer.

Een poortveld heeft nu knoppen om een stapje omhoog of omlaag te doen, een vergrootglas om te controleren, en een melding eronder: vrij, of bezet en door welke container, met een knop om meteen naar de eerstvolgende vrije poort te springen. De controle loopt ook zodra je een stap opent, dus je ziet het vóórdat je iets typt. Kan de host niet uitgelezen worden, dan zegt de melding dat "vrij" een aanname is in plaats van het te verzwijgen.

De knoppen zitten alleen op velden die ook echt een poort zijn — puid en pgid zijn ook getallen. En het scherm Instellingen wijzigen van een bestaande stack deed de controle bij het openen niet mee; dat is hetzelfde formulier, dus dat hoort er ook bij.

Installeren op een telefoon. De installatiemodal is daar nu schermvullend in plaats van een venstertje met marges, met de kop en de knoppenbalk vast en alleen de inhoud die scrollt. Vorige en Volgende vullen samen de breedte in plaats van op een rare plek af te breken. En omdat er dan geen achtergrond meer is om op te tikken, zit er een sluitknop in de kop — die was er niet, en zonder deze wijziging was je er niet uit gekomen.

v0.7.89-beta — De store blijft soepel, en achtentwintig apps erbij

Typen in het zoekveld werd traag. filteredStore() is een methode, geen gecachte getter, en werd per repo drie keer per reactieve tick aangeroepen: in de x-show, in de x-for en via de teller in het filtermenu. Elke aanroep liep alle apps langs met een toLowerCase() op naam, map, omschrijving en elke tag — bij iedere toetsaanslag opnieuw. De catalogus ging deze reeks van 142 naar 286 apps, en dat begon te merken.

Het filteren gebeurt nu één keer per filterstand en wordt vastgehouden tot je iets verandert. Daarnaast staan er nog maar zestig kaarten tegelijk in beeld, met een knop Nog N tonen eronder; die limiet valt terug zodra je het filter of de zoekterm wijzigt, anders sta je na één zoekactie voor de rest van de sessie naar alles te kijken. En het zoekveld wacht 200 ms voordat het filtert.

Achtentwintig apps in de hoeken die het dunst waren:

  • Beveiliging: 2FAuth (tweestapscodes naast je kluis, niet erin), Passbolt (wachtwoorden delen in een team), OpenBao, Trivy
  • Ontwikkeling: Verdaccio, Docker Registry, MkDocs Material
  • AI: LiteLLM, Flowise, Speaches
  • Spellen: Pelican Panel, Gameyfin, EmulatorJS
  • 3D-printen: Mainsail en Fluidd — de twee interfaces voor Klipper, waar OctoPrint niets mee doet
  • Productiviteit: Kanboard, Wekan, Filestash
  • Documenten: Linkwarden (bladwijzers mét een gearchiveerde kopie), Slash, RSSHub
  • Communicatie: Apprise (één API naar honderd meldingsdiensten), Mailpit (vangt de mail van je eigen apps op)
  • Verder: Dashy, Plausible, NetBird, LibreSpeed, PinePods

Alle 318 images in de catalogus zijn opnieuw tegen hun registry gecontroleerd.

v0.7.88-beta — Backups bevatten nu je gegevens, en Pangolin hoort erbij

Een backup gaf je niets terug. backups.create() maakte een tar van de stackmap: de compose, de .env en de metadata. LIBRARY_DIR is /opt/serverup/stacks en de appdata staat in /opt/serverup/appdata — buren, geen genestelde mappen. Je maakte dus een backup, kreeg een groen vinkje, en hield bij terugzetten een lege app over. De documentatie noemde als uitweg dat je data wél meegaat "als die map binnen je stackmap zit", maar met de standaardinstellingen zit hij er juist buiten.

Een archief heeft nu drie takken: de stackmap, de appdata-mappen van die stack, en een dump per database. Die dump is geen dubbelop — een tar van een draaiende PostgreSQL is een momentopname van bestanden die tijdens het inpakken veranderden. Bij terugzetten komt de dump erin nádat de container draait, met een wachtlus omdat een database na het starten niet meteen verbindingen aanneemt. Stond de container tijdens de backup uit, dan staat dat als waarschuwing in het log; stil overslaan zou het ergste geval opleveren, een archief dat compleet lijkt.

Nieuwe instelling Appdata meenemen (BACKUP_APPDATA, standaard aan). Uit zetten is nodig bij een mediabibliotheek of fotoarchief: daar loopt dit in de honderden gigabytes en is restic of borg het betere middel.

Pangolin en Newt staan nu in de store. Server Up had al een complete Pangolin-integratie — een instellingenpagina, een publiceerknop per stack — maar je moest Pangolin zelf ergens anders vandaan halen. Nu staan de tunnelclient (Newt) en de server zelf (pangolin + gerbil + traefik, met de configuratie erbij) in de catalogus.

En er is een stap voor in de setup-wizard, na de git-stap: koppelen aan een Pangolin die je al hebt, er hier een neerzetten, of overslaan. Bij de eerste keuze kun je meteen Newt erbij installeren. Terzijde: de wizard hing zijn afhandeling aan stapnummers, dus met een stap ertussen ging de git-stap mis. Die vergelijkingen gaan nu op de naam van de stap.

Waarschuwing op de stackpagina. depends_on meldde alleen bij het installeren wat er ontbrak. Haal je Mosquitto later weg, dan viel Zigbee2MQTT stil zonder dat iets zei waarom. Dat staat nu als badge op de stackkaart, met de reden in de tooltip en een doorklik naar de store.

v0.7.87-beta — Leesbare namen, zichtbaar wat al draait, en bruikbaar op een telefoon

Het auditlog toonde ruwe id's. In de kolommen stond store, reconfigure, credentials_view — de sleutels waarmee de code logt, niet iets wat je leest. Die hebben nu Nederlandse namen ("App Store", "Opnieuw ingesteld", "Inloggegevens bekeken"), ook in de filterlijsten erboven, en de kolomkoppen zijn niet langer hardgecodeerd Nederlands maar vertaald. Een categorie die we niet kennen — van een module bijvoorbeeld — valt terug op de ruwe waarde in plaats van leeg te blijven. Er draait een test die de broncode afloopt en elke audit.log(...) zonder vertaling aanwijst. Een modulepagina heette in de titelbalk letterlijk mod:mijnmodule; die toont nu de naam van de module.

De store zag niet dat een app al draaide. Er zat een klein groen vinkje naast de installeerknop, en dat werkte alleen als je de stack precies zo had genoemd als het sjabloon: sonarr of sonarr-iets. Noemde je hem media-tv, dan bleef de store volhouden dat Sonarr nog niet geïnstalleerd was. Nu leest hij .serverup.json uit — dezelfde source die het herconfigureerscherm al gebruikte — en toont een duidelijk Geïnstalleerd-label met de naam van elke draaiende installatie, waar je op kunt klikken om er meteen naartoe te gaan. De knop eronder zegt dan "Nog een keer" in plaats van "Installeren".

Op een telefoon was de stackpagina niet te doen. Elke stack had twaalf icoonknoppen zonder tekst naast elkaar, wat op een smal scherm drie rijen plaatjes werd. Daar staan nu starten, herstarten en logs, plus een menu met de rest — mét namen erbij. Op een breed scherm verandert er niets. De rij van twintig categorieknoppen boven de stacks is hetzelfde menu geworden als in de app store, en "Updates controleren" verliest op smalle schermen zijn label.

v0.7.86-beta — De filter in de app store is een menu geworden

Twee rijen knoppen vulden een half scherm. Boven de app store stonden twintig categorieknoppen en veertien labelknoppen, allemaal tegelijk uitgeklapt. Bij 256 apps is dat geen filter meer maar een muur waar je langs moet scrollen voordat je de eerste app ziet.

Het is nu één knop Filter met een menu eronder. Categorieën staan in een raster van twee kolommen met hun eigen kleur en het aantal apps erachter, labels hebben een eigen zoekveld, en onderaan staat hoeveel apps er overblijven met een knop om alles in één keer te wissen. Het menu gaat dicht als je ernaast klikt of op Escape drukt.

En de labelfilter werkte maar half. De rij was afgekapt op veertien labels, terwijl er 297 in de catalogus zitten. De overige 283 waren gewoon niet te kiezen — je zag ze wel op de app-kaarten staan, maar erop filteren kon niet. Het menu zoekt nu in de volledige lijst; zonder zoekterm toont het de veertig meest gebruikte, en een label dat je gekozen hebt blijft zichtbaar ook als het zeldzaam is.

Wat je aan hebt staan zie je zonder het menu te openen: actieve filters staan als chips naast de knop, met een kruisje om ze los weg te halen. En het zoekveld heeft een wisknop gekregen.

Nieuwe test op de HTML zelf. De interface is één sjabloon van ruim drieduizend regels met Alpine-templates die in elkaar zitten. Eén </div> verkeerd en de rest van de pagina schuift in de verkeerde tak — wat je pas merkt als je die kant op klikt. Er draait nu een test die het hele sjabloon op gebalanceerde tags controleert, naast de bestaande die de JavaScript parseert.

v0.7.85-beta — Sjablonen kunnen zeggen wat ze nodig hebben, en vijftien domotica-apps

Een app kon niet zeggen dat hij een andere nodig had. Het connect-veld wijst naar een andere app en vult een adres in, maar zegt niets over noodzaak. Daardoor kon je Zigbee2MQTT installeren zonder MQTT-broker, Alertmanager zonder Prometheus en Grafana Alloy zonder Loki. Alle drie starten ze, doen niets, en de reden staat nergens.

Sjablonen kunnen dat nu wél zeggen, met depends_on in de metadata. Het installatiescherm toont vóór het invullen wat er ontbreekt, met de reden erbij en een knop die je meteen naar dat sjabloon brengt. Advies, geen blokkade — je kunt de broker of Home Assistant net zo goed op een andere machine hebben staan. De reden is verplicht: een waarschuwing zonder uitleg klik je weg.

Meteen toegepast op acht sjablonen waar het gemis tot nu toe onzichtbaar was: Zigbee2MQTT, Alloy, Alertmanager, node-exporter, cAdvisor, Collabora, Double Take en Soularr. Bewust níét op Element, open-webui, Exportarr, Unpackerr en ONLYOFFICE: die werken met meerdere alternatieven, en depends_on kent geen "een van deze".

Vijftien domotica-apps. Van 12 naar 27 in die categorie — vooral het spul dat om Home Assistant heen hoort:

  • Stemassistent, tot nu toe volledig afwezig: Piper (tekst naar spraak), Whisper (spraak naar tekst) en openWakeWord (het activeerwoord). Samen maken ze de spraakbediening van HA compleet zonder dat er iets naar een clouddienst gaat.
  • Matter Server en Matter Hub, plus AppDaemon voor automatiseringen in Python
  • go2rtc (camerastreams) en Double Take (gezichtsherkenning op Frigate)
  • Domoticz en openHAB als alternatief voor Home Assistant zelf
  • Voor de Nederlandse opstelling: DSMR-reader leest de P1-poort van je slimme meter uit, rtl_433 vangt de 433 MHz-sensoren van weerstations en regenmeters op en zet ze op MQTT
  • Traccar (GPS-tracking), Snapcast (audio door het hele huis) en EMQX (MQTT-broker met webinterface)

Drie daarvan hebben een fysiek apparaat nodig — een P1-kabel, een SDR-stick, een Zigbee-dongle. Die krijgen een veld met uitleg over hoe je het juiste pad vindt, in plaats van een gok die stilzwijgend faalt.

v0.7.84-beta — Prometheus startte niet, en 23 apps erbij

Prometheus stond kapot in de catalogus. Het sjabloon koppelde een lege map op /etc/prometheus en startte met --config.file=/etc/prometheus/prometheus.yml — een bestand dat er nooit kwam. Prometheus stopt daar meteen op. En zelfs mét configuratie viel er niets te meten: er was in de hele catalogus geen node-exporter en geen cAdvisor. Er zit nu een startconfiguratie bij, met koppelvelden naar allebei.

Datzelfde patroon zat op meer plekken, en dat is waar de meeste apps van deze ronde vandaan komen — niet nieuwe categorieën, maar de ontbrekende helft van wat er al stond:

  • node-exporter, cAdvisor en Alertmanager maken Prometheus en Grafana pas bruikbaar
  • Loki en Grafana Alloy bewaren logs; Dozzle liet alleen zien wat er nú gebeurt, en na een herstart was de oorzaak weg
  • Unbound zoekt DNS zelf op. Pi-hole en AdGuard blokkeerden wel reclame, maar stuurden de rest gewoon door naar Google of Cloudflare

Domeinen die ontbraken: evcc (zonnepanelen, thuisbatterij en laadpaal op elkaar afstemmen), Healthchecks (merkt dat een cronjob níét liep — Uptime Kuma merkt alleen dat iets uit ligt), Infisical (geheimen voor je applicaties, waar Vaultwarden voor mensen is), Music Assistant en pgAdmin.

Verder: Crafty Controller, Manyfold, ErsatzTV, FitTrackee, ComfyUI, Omada Controller, en de zwaardere Zabbix, Graylog, Wazuh, NetBox, Seafile en OpenCloud. Van 218 naar 241 apps. Monitoring gaat van 18 naar 27.

Een controle die zichzelf tegensprak. De image-controle draaide met zestien verzoeken tegelijk en meldde negen ontbrekende lscr.io-images. Alle negen bleken te bestaan: registries knijpen af, en een afgeknepen verzoek ziet er precies zo uit als een image dat er niet is. Bijna had ik negen werkende sjablonen "gerepareerd". Die controle zit nu in tools/controleer_images.py, met vier tegelijk en opnieuw proberen bij twijfel, en draait als stap in CI.

v0.7.83-beta — Vervangers voor big tech, en sleutels in het juiste formaat

Een sleutel is niet zomaar een willekeurige string. De generator maakte altijd url-veilige base64, en dat is precies wat Laravel, Homarr en LibreChat níét accepteren. Laravel wil base64: ervoor met standaard-base64; Homarr wil 64 hexadecimale tekens; LibreChat wil er twee, van 64 en 32. Een waarde in het verkeerde formaat levert een container op die niet start, met een foutmelding die nergens naar de sleutel wijst. Sjablonen kunnen nu secret_format (hex of laravel) en secret_bytes opgeven.

Daarmee vielen twee fouten uit de vorige versie op: Homarr en LibreChat kregen sleutels die ze hadden geweigerd. Rechtgezet. En Snipe-IT en Invoice Ninja, die vorige ronde afvielen omdat hun APP_KEY niet te genereren was, staan er nu wel in.

Negentien vervangers voor big tech. De catalogus gaat van 197 naar 218 apps:

In plaats van Nu
Google Agenda en Contacten Baïkal, Radicale
Google Translate LibreTranslate
Google Docs CryptPad, Collabora Online, ONLYOFFICE
Google Forms Formbricks
Google Meet en Zoom Jitsi Meet
YouTube PeerTube, Invidious, Tube Archivist
Twitch Owncast
X (Twitter) Mastodon
Instagram Pixelfed
Reddit Lemmy
Calendly Cal.com
Doodle Rallly
DocuSign DocuSeal
Figma Penpot

Communicatie gaat daarmee van 12 naar 16 apps en productiviteit van 21 naar 31 — de twee hoeken waar zelfhosten het meest oplevert en de catalogus het dunst was.

Wat er niet in ging. Bij een paar apps staat een waarschuwing in de omschrijving in plaats van dat ik doe alsof het vanzelf gaat: Mastodon en PeerTube leggen hun domein vast zodra andere servers je kennen, CryptPad heeft echt twee verschillende domeinen nodig om zijn sandbox te laten werken, en LibreTranslate haalt bij de eerste start zijn taalmodellen op.

v0.7.82-beta — 55 apps erbij, en drie kapotte images gevonden

De catalogus stond scheef. Na het ARR-werk telde media 42 apps en downloaden 19, terwijl communicatie er 4 had, AI 4, financiën 3 en foto's 2. Wie de store opende voor iets anders dan media, vond het vaak niet. Er zijn nu 55 apps bij, van 142 naar 197, gekozen op wat er in 2026 daadwerkelijk gedraaid wordt.

Drie nieuwe categorieën: Statistiek (Umami, Matomo), Zakelijk (Odoo, FreeScout, EspoCRM) en Spellen (RomM, Minecraft) — hoeken die er helemaal niet waren.

Homelab-gereedschap: Dockge, Komodo, Glance, Gatus, Scrutiny, Backrest, Headscale, Zoraxy, CrowdSec, NetAlertX, Pocket ID en Authentik. Communicatie gaat van 4 naar 12 met Mattermost, Synapse, Element, Listmonk, Mumble, Roundcube en Stalwart. Notities en documenten: Outline, Trilium, SilverBullet, Readeck, Docmost en Kiwix. Verder LibreChat, AnythingLLM, LocalAI, Ghostfolio, Wallos, Maybe, Lychee, Piwigo, Homebox, wger, Donetick, Dawarich, Gitea, Adminer, code-server, Woodpecker, Semaphore, Opengist, MinIO, Pingvin Share, Leantime, PrivateBin, Emby en ConvertX.

Drie apps in de catalogus waren niet te installeren. De controle die bij het ARR-werk Huntarr ontmaskerde, is nu over alle 213 images in de catalogus gehaald. Daaruit kwamen drie stille fouten: Forgejo wees naar :latest terwijl dat project helemaal geen latest-tag publiceert, Planka naar een tag 1 die niet bestaat, en Baby Buddy naar ghcr.io/babybuddy/babybuddy — een pad dat er niet is, terwijl het sjabloon met zijn PUID/PGID al voor de linuxserver-variant geschreven was. Alle drie rechtgezet.

Twee testregels waren te krap. Een config.yaml in files/ die geen compose-bestand is, viel tussen wal en schip: hij werd niet als compose gescand en overgeslagen omdat hij op .yaml eindigt — velden die alleen dáár gebruikt werden, gingen door voor ongebruikt. En een verplicht geheim dat de gebruiker ergens anders vandaan haalt, stond in een handmatige lijst met veldnamen. Dat is nu een vlag in het sjabloon zelf: "eigen_invoer": true.

Nieuwe controles. Zesendertig apps brengen inmiddels hun eigen database mee. Daarvoor geldt nu: elke extra container draagt de servicenaam als voorvoegsel (anders botsen twee installaties van dezelfde app), geen enkel databasewachtwoord staat vast in een sjabloon, en er wacht altijd iemand met depends_on op de database.

v0.7.81-beta — De ARR-stack uit elkaar, en netwerkopties per container

*Elk arr-onderdeel is nu een losse app. Wie alleen Sonarr wil, hoefde eerst de hele ARR-stack te nemen. Er staan nu zesenveertig losse sjablonen in de store — Sonarr, Radarr, Prowlarr, Bazarr, Gluetun, en alles daaromheen — en de gecombineerde stack blijft bestaan als "alles in één keer". Het zijn dezelfde containers: beide verpakkingen komen uit één bron (tools/arr_sjablonen.py) en een test draait die opnieuw en vergelijkt het resultaat, zodat ze niet stilletjes uit elkaar groeien. Die test vond meteen zijn eerste geval: los koppelde qBittorrent zijn interne poort aan de hostpoort die jij koos, wat niet meer klopt zodra hij achter een VPN zit.

Hardlinks werkten niet, en dat was niet zichtbaar. Elke app kreeg losse mounts — /tv, /movies, /downloads. Voor Sonarr zijn dat twee verschillende bestandssystemen, ook al staat alles op dezelfde schijf, dus werd élke voltooide download naar je bibliotheek gekopieerd in plaats van gelinkt: traag, tijdelijk alles dubbel, en je torrent stopte met seeden zodra je opruimde. De beschrijving waarschuwde wel voor "zet ze op hetzelfde bestandssysteem", maar de mounts maakten het alsnog onmogelijk. Alles deelt nu één map die als /data binnenkomt, met UMASK=002 zodat de apps bij elkaars bestanden kunnen.

Negentien lege kaarten. De schakelaar van een onderdeel stond hard in de stap Instellingen, terwijl je hem ook in de kop van zijn eigen groep in Basis zag staan: elk onderdeel had twee schakelaars in twee schermen, en Instellingen bestond voor het grootste deel uit kaarten zonder inhoud. Er is nu een eerste stap Onderdelen waar alle schakelaars bij elkaar staan, gegroepeerd per soort — bij negenenveertig onderdelen scheelt dat nogal. Wat je uitzet verdwijnt uit de volgende stappen in plaats van als lege kaart te blijven staan.

Een eigen IP-adres ging per stack, niet per container. Bij een app met één container klopt dat. Bij een stack met twintig webinterfaces kreeg de eerste container het adres en raakten alle andere hun poortmappings kwijt zonder er iets voor terug te krijgen — daarna waren ze nergens meer te bereiken. Je wijst nu per container een adres toe; wie er geen krijgt, houdt gewoon zijn poort. Hetzelfde gold voor Pangolin, dat één URL per stack maakte op de eerste poort die het tegenkwam: dat gaat nu ook per container.

Huntarr is eruit. Het project werd in februari 2026 offline gehaald nadat er onder meer authenticatie-bypasses en API-sleutels in platte tekst waren gevonden; de auteur heeft de repo verwijderd. Het image dat in het sjabloon stond bestaat niet meer — een controle langs alle registries bevestigde dat. Vervangen door NeutArr, de fork van de laatste schone versie. In diezelfde ronde bleek Maintainerr verhuisd naar een andere organisatie.

Nieuw in de catalogus: Overseerr, Cleanuparr, Decluttarr, Janitorr, cross-seed, Configarr, Profilarr, Byparr, Kometa, Tautulli, Jellystat, Wizarr, SuggestArr, Trailarr, Lingarr, NZBHydra2, Deluge, Transmission, slskd, Soularr, Mylar3, Kapowarr, Pulsarr, Prefetcharr, Posterizarr, Notifiarr, Exportarr, Autopulse, Requestrr, Homarr en NeutArr. Alle images zijn tegen hun registry gecontroleerd.

Twee mappen die allebei "data" heetten. data_dir was de map waar een app zijn eigen spullen bewaart; daar kwam nu een gedeelde /data naast. Die heten nu appdata_dir en data_root, in alle sjablonen. En qBittorrent stond standaard op poort 8080, samen met zes andere apps; die staat nu op 8097.

v0.7.80-beta — De knop Volgende, tijdzones, en netwerken per groepje

De knop Volgende deed niets. Alpine haalt een boolean-attribuut alleen weg bij null, undefined of false — het getal 0 zét het juist. Mijn :disabled="stepMissing().length" gaf bij een compleet ingevuld formulier 0, en daarmee stond de knop permanent uit. De logica klopte, de binding niet.

Dat was met losse logica niet te vinden, dus er draait nu een test die de echte modal met Alpine in een DOM rendert en er daadwerkelijk op klikt. Die vangt precies dit soort bindingsfouten. Hij slaat zichzelf over waar jsdom ontbreekt.

Tijdzones komen van de server. In alle 77 sjablonen die er een hebben stond Europe/Amsterdam hard ingevuld — dat klopt alleen toevallig. Er is nu een instelling Tijdzone die standaard de tijdzone van je server overneemt (uit TZ, /etc/timezone of /etc/localtime), en die wordt als voorstel gebruikt. De negentien apps zonder tijdzoneveld krijgen TZ er automatisch bij, want die draaiden tot nu toe in UTC en zetten dus overal het verkeerde tijdstip neer.

Poortcontrole kijkt nu ook naar de host. docker ps toont alleen wat Docker publiceert; een nginx of DNS-server die rechtstreeks op de host draait bleef onzichtbaar terwijl je er net zo goed mee botst. Die worden nu meegeteld, via /proc/net/tcp in een hulpcontainer met --network host. Lukt dat niet, dan zegt het antwoord host_checked: false — "vrij" is dan een aanname.

Niet alles hoeft meer aan alles vast. Er was één gedeeld netwerk waar élke app aan hing. Je kiest nu per app aan welke netwerken hij meedoet, en kunt er bij het installeren zelf een maken — met een eigen naam of een voorgestelde (su-<appnaam>). Dat netwerk verschijnt daarna bij elke andere app in de lijst. Een app kan aan meerdere netwerken tegelijk hangen.

Advies bij IP-adressen. Vul je een adres in, dan wordt gecontroleerd of het binnen het subnet valt, binnen je ingestelde bereik, of het al aan een container is toegekend, en of de host het recent op het LAN heeft gezien — dat laatste uit de ARP-tabel, en dat vangt een fysiek apparaat met een DHCP-adres. Klopt er iets niet, dan staat erbij wát en krijg je een vrij adres voorgesteld.

Nieuw: Pangolin. Onder Inloggegevens kun je met één klik een publiek adres voor een app aanmaken via een Pangolin-tunnel, zonder poorten open te zetten. Zie docs/pangolin.md.

Deze integratie is getest tegen een nagebouwde API, niet tegen een echte Pangolin-server — die heb ik hier niet. De API veranderde bovendien in versie 1.9 (een resource hing eerst onder een site en staat nu los), dus beide routes worden geprobeerd en bij een fout krijg je beide meldingen te zien.

2369 tests groen.


v0.7.70-beta — Installeren via een invulmenu, geheimen naar .env

Het installatieformulier is een invulmenu geworden. Eén lange lijst werkte slecht aan beide uiteinden: de mediane app heeft vijf velden in één groep, en de ARR-stack tweeënvijftig in eenentwintig groepen. Nu zijn er vaste stappen — Basis, Verbinden, Instellingen, Toegang, Controleren — waarbij een lege stap wordt overgeslagen. Een eenvoudige app krijgt er dus drie, de ARR-stack vijf.

Volgende controleert de verplichte velden van díé stap en noemt wat er nog leeg is. Tot nu toe kwam die melding pas als je op Installeren drukte. De stapindicator is klikbaar om terug te springen.

Geavanceerde opties zitten achter één schakelaar die op elke stap zichtbaar is, in plaats van een uitklapblok per groep dat je steeds opnieuw moest zoeken. Een badge toont hoeveel velden verborgen zijn. Er is ook een schakelaar Alles op één pagina voor wie het oude gedrag wil; beide keuzes worden onthouden.

Geheimen gaan naar .env. Wachtwoorden en sleutels staan niet meer in het compose-bestand maar als ${NAAM} met de waarde in .env, geschreven met rechten 0600. Poorten, paden en tijdzone blijven leesbaar in compose, zodat je nog steeds in één bestand kunt zien wat er draait. Ook .serverup.json bevat geen geheimen meer en krijgt 0600 — daar stonden ze tot nu toe wereldleesbaar in.

Eerlijk over de grens hiervan: compose vult ${...} in bij het inlezen, dus docker inspect toont de waarde nog steeds. Wat je wint is dat het compose-bestand deelbaar wordt en dat alles op één afgeschermde plek staat. docs/beveiliging.md legt uit wanneer je verder wilt gaan met secrets: van Compose, en waarom een externe kluis hier een slecht idee is.

Compose en .env zijn nu ook vóór het installeren te bewerken. De laatste stap toont het gerenderde resultaat met de waarden die je hebt ingevuld; met Bewerken pas je het aan en gaat jouw versie mee in plaats van het sjabloon. Ongeldige YAML wordt geweigerd voordat er een map is aangemaakt. De editor voor bestaande stacks heeft tabbladen gekregen (je moest hem sluiten en opnieuw openen om van compose naar .env te gaan) en biedt aan te herstarten na opslaan. De .env-editor weigert nu een regel zonder = — compose negeert die stilzwijgend, waarna er ineens een wachtwoord mist — en schrijft een auditregel.

Nieuw: Inloggegevens per app. Een knop op de stackkaart toont het adres, de gebruikersnaam en het wachtwoord, met een oog-knop en een kopieerknop. Bij een gegenereerd wachtwoord kon je dat nergens meer terugvinden. Apps die je bij het eerste bezoek zelf een account laten aanmaken (Vaultwarden, Immich) melden dat. Alleen beheerders kunnen erbij en elk bekijken komt in het auditlog.

Herstel van een regressie uit v0.7.60. Ik had de genereerknop toen aan generate gehangen in plaats van aan secret, waardoor vijftien velden hun knop kwijtraakten. Acht daarvan zijn inlogwachtwoorden (InfluxDB, Kimai, Linkding, Miniflux, PhotoPrism, Pi-hole, Technitium, ChangeDetection) en die hebben hem terug. De zeven waar een verzonnen waarde juist fout is — een WireGuard- privésleutel, een Beszel-agentsleutel, tokens uit een andere app — houden geen knop.

Bij het uitzoeken bleek ook dat ik in v0.7.60 twee dingen verkeerd heb gemeld. De genereerknop wérkte destijds wel (de vlag werd gezet in app.py, niet in boilerplates.py waar ik keek), en er wás al een compose- en .env-editor.

De logica van het invulmenu wordt nu getest door de echte component uit index.html in Node uit te voeren; die test slaat zichzelf over waar Node ontbreekt, zoals in het bouwimage. 2295 tests groen.


v0.7.60-beta — App-instellingen die niet meer stilzwijgend misgaan

Aanleiding: Homepage weigerde na installatie met "Host validation failed". De standaardwaarde van allowed_hosts noemde poort 3001 terwijl het poortveld op 3002 stond — en localhost werkt sowieso niet als je je server op zijn IP-adres benadert. Dat bleek geen los geval, dus alle 96 sjablonen zijn nagelopen.

De dobbelsteenknop bestond niet. Zeventien apps zeggen in hun uitleg "gebruik de dobbelsteenknop voor een willekeurige waarde". Die knop hing af van een veld secret dat de backend nooit meestuurde, dus hij verscheen nooit — en wachtwoorden stonden als gewone leesbare tekst in het formulier. Nu worden geheimen herkend, en 27 velden die je toch nooit zelf typt (databasewachtwoorden, JWT- en versleutelingssleutels) worden meteen ingevuld met een willekeurige waarde. Inlogwachtwoorden krijgen alleen de knop: die moet je zelf noteren.

De knop hangt nu aan "hier past een willekeurige waarde" en niet aan "dit is geheim" — voor een WireGuard-privésleutel of een token uit een andere app is iets verzonnens juist fout.

Verplichte velden werden niet gecontroleerd. Het formulier zette een sterretje achter een verplicht veld en hield verder niets tegen: je kon een app installeren met een leeg databasewachtwoord of een lege sleutel. De container start dan niet, of draait met een leeg geheim. Dat wordt nu geweigerd, bij installeren én bij het achteraf wijzigen van instellingen, met de melding welke velden nog leeg zijn. Velden achter een uitgeschakelde groepsschakelaar tellen niet mee.

Acht apps hadden een vast wachtwoord in het sjabloon: immich_db_pass, ghost_db_pass en soortgelijke voor iedereen die het installeert, en admin als beheerderswachtwoord van Grafana en Gotify. De databasewachtwoorden worden nu gegenereerd; bij Grafana en Gotify moet je zelf een wachtwoord kiezen.

Wachtwoorden stonden in het installatielog, dat in de interface zichtbaar is en vaak in een bugmelding geplakt wordt. Die worden nu gemaskeerd.

Verder rechtgezet:

  • baserow, hedgedoc en ntfy hadden dezelfde poortfout als Homepage: de URL in de standaardwaarde wees naar een poort waar niets luistert, waardoor links en terugkeer-na-inloggen niet werkten.
  • Zes velden stonden in het formulier maar kwamen nergens terecht: data_dir bij Keycloak, MeTube, Miniflux en TeslaMate (die gebruiken een Docker-volume, dus je gegevens stonden niet waar je dacht), en puid/pgid bij FreshRSS, die het image helemaal niet kent.
  • De tijdzone van Prometheus werd wel gevraagd maar niet doorgegeven.

Nieuw is tests/test_app_templates.py: elke app wordt met zijn standaardwaarden gerenderd en gecontroleerd op ongedefinieerde variabelen, velden zonder werking, niet-gedeclareerde volumes, depends_on naar een onbekende service, dubbele containernamen, poortconflicten binnen een stack, poortnummers in standaardwaarden die niet kloppen met het poortveld, en vaste wachtwoorden. 2236 tests groen.


v0.7.50-beta — Je hoort het als er iets misgaat

Het onbewaakte deel was de zwakke plek. Geplande backups, updatechecks en de zelf-update draaien 's nachts zonder toezicht; ging er iets mis, dan kwam dat alleen in het auditlog terecht. Een backup die drie weken stilletjes faalt, ontdek je op het slechtst denkbare moment.

Meldingen via ntfy, een webhook of e-mail — alle drie met wat er al in Server Up zat, dus geen extra software. Je krijgt bericht bij een mislukte backup, een archief dat niet leesbaar blijkt, een vastgelopen geplande taak, weinig schijfruimte, een beschikbare update en een container die zou moeten draaien maar dat niet doet. Standaard staan alleen de storingen aan.

Eén bericht per ronde, niet één per stack: bij een volle schijf faalt alles tegelijk en dan wil je geen twintig meldingen. Bij weinig ruimte en gestopte containers wordt alleen de overgang gemeld, anders krijg je elk uur hetzelfde. Er is een testknop die eerst je instellingen opslaat, zodat je test wat je net hebt ingevuld. Een storing in het meldingskanaal kan de taak die de melding veroorzaakte nooit alsnog laten omvallen.

Schijfruimte wordt nu bewaakt. Vóór elke backup wordt gekeken of het er redelijkerwijs in past; zo niet, dan wordt hij geweigerd in plaats van halverwege afgebroken — een half archief ziet er in een lijst compleet uit. Daarbovenop blijft DISK_MIN_FREE_GB gereserveerd, want een volle schijf op een Docker-host breekt alles en niet alleen Server Up. In de backuplijst staat per schijf een balkje, dat rood kleurt onder DISK_WARN_PCT.

Backups worden gecontroleerd. Elk nieuw archief wordt meteen helemaal uitgelezen: dat controleert de tar-structuur én de gzip-checksum. Die checksum staat aan het eind, dus een archief dat halverwege is afgebroken valt door de mand terwijl het in een directorylisting compleet lijkt. Er is ook een diepe variant die echt uitpakt naar een tijdelijke map, met dezelfde beperkingen als een echt herstel — de enige manier om te weten dat terugzetten werkt vóórdat je het nodig hebt. Achter elk archief staat een schildje: groen als het gelezen is, rood met de foutmelding als het beschadigd is.

Backups kunnen de deur uit. Er is een downloadknop, en met BACKUP_OFFSITE_DIR wordt elke nieuwe backup gekopieerd naar een tweede bestemming: een gemounte schijf, NFS- of SMB-share. Tot nu toe stonden backups uitsluitend op dezelfde schijf als de data die ze moeten beschermen, en kon je ze er niet eens afhalen. Het kopiëren gaat via een .part-bestand dat daarna hernoemd wordt, en valt de mount weg — dan bestaat het pad vaak nog als lege map op je systeemschijf — dan wordt eerst gekeken of er ruimte is, zodat je niet ongemerkt de verkeerde schijf vult.

Het token en het SMTP-wachtwoord zijn write-only, net als de git- en registry-tokens: instellen kan, teruglezen niet.

Nieuw: docs/meldingen.md, en docs/backups.md is uitgebreid met controleren, schijfruimte, de tweede bestemming en downloaden.

Getest tegen een draaiende server met een echte ontvanger voor de meldingen: een gehalveerd archief wordt herkend, een volle schijf weigert de backup zonder iets achter te laten, het gedownloade bestand is een uitpakbare tarball, en een padtruc in de bestandsnaam levert een 400 op. 1064 tests groen.


v0.7.40-beta — Account en bereikbaarheid meteen goed

Na het installeren was je nog niet klaar: je moest zelf naar de webinterface om een beheerdersaccount te claimen, en stond BIND op 127.0.0.1, dan kón je daar niet eens bij zonder eerst .env aan te passen. Het script regelt nu allebei.

Beheerdersaccount. Op een terminal vraagt het script of je er meteen één wil, en zo ja om een naam en wachtwoord (twee keer, met de echo uit). Automatisch gaat het met --admin NAAM plus --admin-password-file PAD of SU_ADMIN_PASSWORD. Is er geen wachtwoordbron én geen terminal, dan maakt het script er zelf een van 24 tekens en zet die op het scherm.

Het wachtwoord kan bewust niet als optie mee: --admin-password weigert met uitleg. Opdrachtregelargumenten zijn voor elke gebruiker op de server zichtbaar met ps en blijven in je shell-geschiedenis staan. Onderweg naar de server gaat het via stdin naar curl, niet als argument.

Nieuw is ook --create-admin: alleen het account aanmaken, bij een installatie die al draait. Voor als het de eerste keer is misgegaan of je het hebt overgeslagen. De poort komt daarbij uit .env.

Bereikbaarheid. Het script vraagt nu waarop de interface moet luisteren: alleen deze server, het hele netwerk (met het gedetecteerde adres erbij), of een adres dat je zelf opgeeft. Installeer je opnieuw over een bestaande map, dan is je huidige instelling het uitgangspunt en houdt enter die vast.

Daarbij zat een fout die je zelf tegenkwam: een bestaande .env bleef altijd ongemoeid, ook als je --bind meegaf. De installatie leek dan te lukken terwijl de server op het oude adres bleef luisteren. Nu wordt doorgevoerd wat je expliciet meegeeft, en blijft de rest staan. Andersom rekent het script bij --update voortaan met wat er écht in .env staat: draaide je op poort 8080, dan wachtte het daarvoor op poort 5000 en meldde het het verkeerde adres.

Verder: --dry-run riep sudo docker aan en vroeg dus om een wachtwoord, terwijl het net beloofd had niets te doen — zonder terminal liep het daar vast. Een proefdraai spreekt de Docker-daemon nu niet meer aan. En een ongeldig bind-adres of een poort buiten 165535 wordt meteen geweigerd in plaats van als een onbegrijpelijke fout uit docker compose terug te komen.

De vragen worden allemaal vooraf gesteld, zodat je kunt weglopen terwijl het image gebouwd wordt.

Getest met een echte pseudo-terminal voor de vragen, en tegen een echt draaiende server voor het aanmaken van het account — inclusief een wachtwoord vol aanhalingstekens en backslashes, een tweede account dat geweigerd hoort te worden, en een server die niet reageert.


v0.7.30 — Installeren met één regel

curl -fsSL https://git.ramonbesselink.nl/bes-r/server-up/raw/branch/main/install.sh | sh

Het script controleert je systeem, biedt aan Docker te installeren als dat ontbreekt, haalt de code op, maakt .env aan, bouwt het image, start de container en wacht tot /healthz antwoordt. Daarna toont het waar je terecht kunt — met de waarschuwing dat je meteen een beheerdersaccount moet aanmaken.

Opties: --dir, --port, --bind, --branch, --token, --update, --uninstall, --yes en --dry-run.

--dry-run toont de hele gang van zaken zonder iets te wijzigen. Handig om te zien wat er gaat gebeuren voordat je een script met root-rechten loslaat op je server; docs/installeren.md zet die stap dan ook vooraan.

Bijwerken gaat met sh /opt/server-up/install.sh --update: nieuwe code ophalen, opnieuw bouwen, herstarten. Je .env, stacks en gegevens blijven staan. Verwijderen stopt de container maar laat je gegevens met rust, en vertelt daarna hoe je die alsnog opruimt.

Het script gebruikt alleen sudo waar dat nodig is: kun je zelf al in de doelmap schrijven én docker aanroepen, dan blijft alles onder je eigen account.

Nieuw: README.md verwijst er nu naar, plus docs/installeren.md met alle opties en een probleemoplostabel.

Getest onder sh, dash en bash, met een testsuite die controleert dat er geen bashismen in sluipen, dat een typefout in de opties netjes stopt, en dat een proefdraai echt niets aanmaakt. Twee dingen die daarbij naar boven kwamen: de wachtlus gaf anderhalve minuut lang geen enkel teken van leven (nu puntjes), en docker compose --project-directory zoekt zonder -f het compose-bestand alsnog in de huidige map — wat bij curl | sh je thuismap is.

Een derde kwam pas op de bouwserver boven: ontbrak curl, dan stopte ook een proefdraai meteen. Juist dan wil je zien wat er gaat gebeuren, dus dat is nu een waarschuwing in plaats van een harde fout — net als bij een ontbrekende Docker.

Dit is de eerste stabiele release sinds v0.4.60. Alles uit de v0.5-, v0.6- en v0.7-beta's zit erin: inloggen met rollen, backups, netwerken met een eigen IP-adres, 96 apps met categorieën, apps onderling koppelen, instellingen achteraf wijzigen, en zelf bijwerken. Draaide je nog v0.4.60, lees dan eerst docs/beveiliging.md: daar stond de interface nog open zonder login, en na het bijwerken moet je meteen een beheerdersaccount aanmaken.


v0.7.20-beta — Instellingen wijzigen, koppelen achteraf, en een defect uit v0.7.00

🔴 Gedeeld netwerk botste met network_mode

De koppeling uit v0.7.00 hing élke service aan het gedeelde netwerk. Maar een service met network_mode deelt al andermans namespace, en docker compose weigert die combinatie. Met de standaardkeuze "Verbinden met andere apps" waren Homebridge, Scrypted, Beszel en de ARR-stack met Gluetun daardoor niet te installeren.

Services met network_mode worden nu overgeslagen. Een test past het gedeelde netwerk toe over alle 96 apps en controleert dat die combinatie nooit ontstaat.

⚙️ Instellingen wijzigen na installatie

Je koos bij het installeren netjes een poort, map en wachtwoord — maar daarna was er geen weg terug naar dat formulier. Voor een andere poort moest je het compose-bestand met de hand bewerken.

De knop Instellingen op elke stackkaart opent hetzelfde formulier, voorgevuld met wat je destijds hebt gekozen. Opslaan maakt eerst een backup, rendert de stack opnieuw, valideert en herstart. Loopt het valideren mis, dan wordt de backup automatisch teruggezet.

Stacks die zijn geïnstalleerd voordat Server Up die keuzes bewaarde, hebben de knop uit staan met uitleg waarom.

🔗 Bestaande stacks alsnog koppelen

Het gedeelde netwerk gold alleen voor nieuwe installaties. Een knop op de stackkaart zet een bestaande stack er alsnog op, valideert en herstart. Wordt de compose ongeldig, dan wordt de wijziging teruggedraaid.

🔍 Audit-log doorzoekbaar

Het log toonde de laatste tweehonderd regels zonder filter, terwijl er inmiddels logins, rolwijzigingen, backups en updates in landen. Nu filteren op bron, actie, status en periode, met vrij zoeken door referentie, details en IP-adres, en doorbladeren voorbij de eerste honderd.

📖 Documentatie

  • README.md — die was er niet. Wat Server Up is, installeren, het eerste account, en waar je verder moet kijken.
  • docs/apps-maken.md — het volledige templateformaat: velden, geavanceerde opties, uitleg per veld, apps aan elkaar koppelen, onderdelen optioneel maken met groepsschakelaars, en de valkuilen (named volumes, poorten, network_mode).
  • docs/README.md — index over de zeven documenten.

De changelog-secties voor v0.5.44, v0.5.45 en v0.5.46 ontbraken; omdat release.yml de notes daaruit haalt, zou een tag op die versies een lege release opleveren. Aangevuld, met een test die afdwingt dat het huidige VERSION altijd een sectie heeft.


v0.7.10-beta — Categorieën met kleur, in de app store en bij je apps

Alle 96 apps zijn ingedeeld in zeventien categorieën, elk met een eigen kleur en pictogram. Een app mag in meerdere categorieën zitten: Frigate staat onder Domotica én Media, Immich onder Foto's én Opslag, Kavita onder Media én Documenten. Negenendertig apps vallen onder meer dan één.

Categorie Categorie
🟣 Media 🔵 Netwerk
🩷 Foto's 🔴 Beveiliging
🟢 Downloaden 🟠 Opslag & backup
🟩 Domotica 🩵 Documenten
🟦 3D-printen 🟦 Productiviteit
🔷 Communicatie 🟩 Financiën
🌸 Gezin & huishouden 🟧 Monitoring
🩵 Ontwikkeling 🟪 AI
Systeembeheer

In de app store staat een gekleurde categoriebalk met per categorie het aantal apps. Klikken filtert; het bestaande zoekveld en de tagfilters werken ernaast gewoon door. Elke app-kaart toont zijn categorieën als gekleurde badge, en klikken op zo'n badge filtert er meteen op.

Bij je geïnstalleerde apps staat dezelfde balk, met alleen de categorieën die je daadwerkelijk draait. Elke stackkaart heeft een gekleurde rand in de kleur van zijn eerste categorie, plus dezelfde klikbare badges.

Apps uit externe repo's (zoals de ChristianLempa-boilerplates) geven geen categorieën op. Die worden afgeleid uit hun tags en omschrijving, zodat ook zij een kleur en een plek in het filter krijgen in plaats van als naamloze groep buiten de indeling te vallen.

Tests bewaken dat elke app minstens één geldige categorie heeft, dat de kleuren onderling verschillen, en dat er geen categorie in het filter staat waar geen enkele app onder valt.


v0.7.00-beta — Apps koppelen, eenvoudiger instellen, geavanceerde opties verborgen

🔗 Apps kunnen elkaar nu bereiken

Elke stack draaide als eigen compose-project en kreeg daarmee zijn eigen netwerk. Twee stacks konden elkaar dus niet op naam vinden: Zigbee2MQTT zag de Mosquitto-broker uit een andere stack simpelweg niet.

Bij het installeren staat nu "Verbinden met andere apps" aan. De stack komt dan op een gedeeld bridge-netwerk (serverup), waar containers elkaar op naam oplossen. Het netwerk wordt aangemaakt zodra de eerste stack het nodig heeft.

🎯 Kiezen in plaats van typen

Een veld dat naar een andere app verwijst, toont voortaan een keuzelijst met wat je al hebt draaien in plaats van een leeg tekstveld. Installeer je Zigbee2MQTT en heb je Mosquitto al staan, dan staat mqtt://mosquitto:1883 er meteen ingevuld. Hetzelfde voor Open WebUI dat je Ollama vindt.

Staat de app er nog niet, dan zegt het veld dat — met de mogelijkheid om alsnog handmatig een adres in te vullen.

💬 Uitleg per instelling

Achter elk veld met toelichting staat een info-icoon. Klikken opent een blok met de volledige uitleg en de standaardwaarde. Voor de instellingen die de meeste vragen oproepen is die uitleg uitgeschreven — waarom je downloadmap op hetzelfde bestandssysteem moet staan als je mediamap, wat shm_size bij Frigate doet, waarom Caddy poort 80 nodig heeft ook als je alles via HTTPS aanbiedt.

🎛️ Geavanceerde opties uit het zicht

Velden die je meestal met rust laat — database-wachtwoorden, PUID/PGID, USB-paden, bewaartermijnen — zitten nu achter een uitklapblok "Geavanceerde opties" met een teller erbij. Het installatieformulier begint daardoor met alleen de vragen die er echt toe doen.

Templates kunnen advanced: true op een veld of op een hele groep zetten. Een test bewaakt dat er per app altijd zichtbare velden overblijven, zodat niemand per ongeluk een volledig leeg formulier maakt.


v0.6.20-beta — 31 apps erbij na een rondgang langs de verzamelsites

Van 65 naar 96 apps. Selectie op basis van awesome-selfhosted, selfh.st, Perfect Media Server en een paar overzichtslijsten van 2026, aangevuld met wat er in de bestaande collectie nog ontbrak.

Monitoring — Prometheus (de tegenhanger van Grafana, die er al stond), InfluxDB, Netdata, Beszel

Reverse proxy — Caddy, dat zijn certificaten volledig zelf regelt

Notities en kennis — Memos, HedgeDoc, Wiki.js, Excalidraw, Karakeep

Data en dashboards — NocoDB, Baserow, Metabase

Identiteit — Authelia en Keycloak, allebei bruikbaar met de reverse-proxy-SSO van Server Up zelf

Media — Komga, PhotoPrism, MeTube, Pinchflat

Meldingen — ntfy

Productiviteit — Planka, Focalboard, Kimai

Locatie en voertuig — OwnTracks, TeslaMate

Hulpmiddelen — Shlink, CyberChef, LanguageTool, Kopia

AI — Ollama en Open WebUI, allebei volledig lokaal

De images van de nieuwere apps (Beszel, Karakeep, Pinchflat) zijn nagetrokken bij de bron in plaats van uit het hoofd opgeschreven; van Karakeep is de officiële compose overgenomen, inclusief de meilisearch- en chrome-containers die hij nodig heeft.

Vijftien poortbotsingen met bestaande apps zijn automatisch rechtgezet. Caddy deelt 80 en 443 met Traefik en Nginx Proxy Manager — alternatieven voor dezelfde taak, dus dat staat in de allowlist.


v0.6.10-beta — ARR-stack met een schakelaar per onderdeel

De ARR-stack installeerde altijd dezelfde zeven containers. Nu kies je per onderdeel wat je wilt, uit twintig:

Media beheren — Sonarr (series), Radarr (films), Lidarr (muziek), Readarr (boeken), Whisparr

Indexers en ondersteuning — Prowlarr, Jackett, FlareSolverr, Bazarr (ondertitels)

Downloaden — qBittorrent, SABnzbd, NZBGet, en Gluetun om al het downloadverkeer door een VPN te leiden

Verzoeken en aanvullingen — Jellyseerr, Recyclarr (TRaSH-profielen), Unpackerr, Tdarr (hercoderen), Autobrr, Maintainerr, Huntarr

Standaard staan Sonarr, Radarr, Prowlarr, Bazarr, qBittorrent en Jellyseerr aan; de rest zet je zelf bij.

Gluetun schakelt de downloadclients om

Zet je Gluetun aan, dan verhuizen qBittorrent, SABnzbd en NZBGet naar diens netwerknamespace en verliezen ze hun eigen poortmapping — precies zoals het hoort, want anders lekt hun verkeer om de VPN heen. Hun webinterfaces bereik je dan via de poorten van de Gluetun-container.

Daarbij zat een val: qBittorrent en SABnzbd luisteren allebei standaard op 8080, en achter één VPN-container botsen ze. qBittorrent draait nu intern op 8090.

Groepsschakelaars werkten niet

Een groepsschakelaar in template.json was zelf geen variabele en kreeg dus nooit een startwaarde. Elk schakelbaar onderdeel stond daardoor standaard uit — een stack met schakelaars zou leeg binnenkomen. Ze worden nu als veld meegegeven met een instelbare standaardstand, en de interface toont ze alleen als schakelaar in de kop van de groep.


v0.6.00-beta — 29 apps erbij, en een bug die zes bestaande apps onstartbaar maakte

🐛 Named volumes verdwenen uit het compose-bestand

Bij het bouwen van de nieuwe apps liep ik tegen iets aan dat er al langer zat. _drop_empty_mappings ruimt sleutels op die een <% if %>-blok leeg achterlaat. Maar een named volume wordt gedeclareerd als een kale sleutel zonder waarde:

volumes:
  immich_pgdata:

Die werd als "lege mapping" opgeruimd, waarna ook het bovenliggende volumes: wegviel. De services verwezen vervolgens naar een volume dat nergens meer gedeclareerd stond, en docker compose up weigert dat met "refers to undefined volume".

Immich, Ghost, Miniflux, Paperless-ngx, Unifi Network en Vikunja waren daardoor niet installeerbaar via Server Up. Alleen bekende compose-sleutels worden nu opgeruimd; een volumedeclaratie blijft staan.

📦 29 nieuwe apps

Domotica — Zigbee2MQTT, Mosquitto MQTT, ESPHome, Node-RED, Z-Wave JS UI, Homebridge, Scrypted, Frigate NVR

3D-printen — Bambuddy (Bambu Lab, zonder cloud), OctoPrint, Spoolman

Gezin en huishouden — Baby Buddy, Grocy, Tandoor Recipes, Firefly III

Media — Audiobookshelf, Navidrome, Kavita, Calibre-Web, Jellyseerr

Kennis — BookStack, FreshRSS, Wallabag

Netwerk en beheer — WireGuard Easy, Homepage, SearXNG, Duplicati, Watchtower, Technitium DNS

Daarmee staat de teller op 65 apps.

Testsuite over de echte templates

Nieuwe tests/test_apps.py rendert elke app met de echte engine en controleert de uitkomst: geldige YAML, elke service heeft een image of build, named volumes zijn gedeclareerd, geen onvervangen << variabelen >>, en geen onbedoeld dubbele standaardpoorten. Die laatste vond zeven botsingen tussen de nieuwe apps en bestaande; die zijn rechtgezet. Botsingen die logisch zijn (Traefik en Nginx Proxy Manager op 80/443, AdGuard en Pi-hole op 53) staan met uitleg in een allowlist.


v0.5.50-beta — Containers die nergens te vinden waren

Het dashboard meldde 13 actieve containers en 17 images, terwijl je die nergens terugzag. Dat kwam doordat de cijfers en de lijst uit twee verschillende bronnen komen:

  • de tellers komen van docker info en gaan over de hele Docker-daemon
  • de stacklijst toont alleen mappen in LIBRARY_DIR met een compose-bestand

Alles wat je buiten Server Up om had gestart — je Forgejo, de act_runner, containers van vóór je Server Up ging gebruiken — telde dus wel mee maar was onzichtbaar en onbedienbaar. Een beheertool die dingen telt die hij niet toont, is verwarrend.

Wat er nu is

  • Overzicht van alle containers op de host. Onder de stacklijst staat een blok "Overige containers" met alles wat niet bij een beheerde stack hoort, inclusief image, poorten en compose-project. Starten, stoppen, herstarten en logs bekijken kan direct.
  • De dashboardteller vertelt nu het hele verhaal: onder "actief" staat hoeveel daarvan via Server Up loopt.
  • De eigen container is gemarkeerd als "deze app" en kan niet via dit blok gestopt worden — daarvoor is de herstartknop in de instellingen, die dat netjes afhandelt in plaats van zichzelf halverwege een verzoek te stoppen.
  • Een viewer ziet de lijst wel maar kan niets bedienen.

Verholpen tijdens het bouwen

Containers zonder poorten én zonder compose-labels vielen weg uit de lijst als ze toevallig als laatste stonden: strip() haalde de lege velden aan het eind van de uitvoer weg, waarna die regel te weinig kolommen leek te hebben. Gevonden door de test die precies zo'n container als laatste zet.


v0.5.46-beta — Deploy strandde op een .env van een andere gebruiker

De stap die instellingen klaarzet faalde op touch: cannot touch '.env': Permission denied. Het bestand was met sudo aangemaakt en dus van root, terwijl de runner als gewone gebruiker draait — waarmee de hele deploy afbrak op iets dat geen blokkade hoort te zijn.

  • SU_TAG gaat nu altijd via de omgeving naar docker compose. Wegschrijven in .env blijft de voorkeur omdat het een handmatige docker compose up overleeft, maar is niet meer nodig om te kunnen deployen.
  • Is .env niet schrijfbaar, dan meldt de log wie de eigenaar is en welk chown-commando dat rechtzet, en loopt de deploy door.

v0.5.45-beta — Wizard-knop toegevoegd

De wizard was alleen zichtbaar bij een verse installatie: zodra hij een keer was afgerond, was er geen enkele manier meer om hem te openen. De vertaalsleutels wizard_open en wizard_reset bestonden al en /api/wizard/reset werkte ook, maar de knop is nooit gebouwd — ook niet in v0.4.60.

  • Blok Setup Wizard onder Instellingen met "Wizard openen" en "Opnieuw uitvoeren".
  • Sluitknop in de wizard zelf; die kon je eerder alleen verlaten door hem helemaal af te ronden.
  • De staptitels lopen nu via t() in plaats van harde Nederlandse teksten.

v0.5.44-beta — Gedetecteerd netwerk verdween uit het formulier

Zodra je een netwerk had toegevoegd, was het blok "gevonden op deze server" leeg bij een volgende poging: subnetten die al geconfigureerd waren werden uit de suggesties gefilterd. Dat pakt verkeerd uit — je kunt prima een tweede netwerk op hetzelfde subnet willen met een andere range, en zonder dat blok lijkt het formulier kapot.

  • Gedetecteerde netwerken blijven staan en tonen "al toegevoegd" in plaats van te verdwijnen.
  • Levert de detectie niets op zonder dat er een fout is, dan staat er nu uitleg in plaats van een leeg blok.

v0.5.43-beta — Van-tot omrekenen naar een CIDR-blok

"Ik wil van 10.0.20.200 tot .254" is een volstrekt redelijke wens, maar bestaat niet als CIDR-notatie: dat zijn 55 adressen, en Docker accepteert alleen uitgelijnde blokken van 2, 4, 8, 16 … adressen.

Onder het IP-range-veld zit nu een van-tot-hulp. Vul de eerste en laatste gewenste adressen in en Server Up rekent uit welke blokken in de buurt komen:

  • dekt alles — het kleinste blok dat je hele wens omvat (pakt er onderaan wat bij)
  • past binnen — het grootste blok dat volledig binnen je wens valt

Bij .200 t/m .254 levert dat 10.0.20.192/26 op: 62 adressen, .193 t/m .254. Klikken vult het veld in.


v0.5.42-beta — Netwerk toevoegen: twee blokkades weg

Een netwerk aanmaken lukte niet, met wisselend gedrag: soms een foutmelding dat de IP-range buiten het subnet lag terwijl dat aantoonbaar niet zo was, soms geen melding maar toch geen resultaat. Het bleken twee losse oorzaken.

De foutmelding die niet klopte

De beoordeling gaat af bij elke toetsaanslag. Tijdens het intypen van /28 ga je langs /2, en 10.0.20.240/2 normaliseert naar 0.0.0.0/2 — inderdaad buiten je subnet. Kwam dat antwoord ná het antwoord voor de volledige waarde binnen, dan bleef die foutmelding staan en bleef de knop uitgeschakeld.

Er zit nu een volgnummer op: alleen het antwoord bij de laatste vraag telt. En de knop wordt niet meer uitgeschakeld op de clientstatus — bij het opslaan beslist de server, zodat een verouderde beoordeling je nooit kan blokkeren.

De range die Docker weigerde

10.0.20.200/28 heeft hostbits gezet. Python leest dat soepel als 10.0.20.192/28, maar Docker is streng en antwoordt "has host bits set". Wij valideerden soepel en stuurden vervolgens de ruwe tekst door, dus de interface keurde iets goed dat bij het aanmaken alsnog strandde.

Subnet en IP-range worden nu omgezet naar hun canonieke vorm voordat ze naar Docker gaan én voordat ze worden opgeslagen, met een waarschuwing die laat zien wat er van je invoer gemaakt is. .200/28 is trouwens een logische invoer — je bedoelt "vanaf .200" — maar een /28 begint nu eenmaal op een veelvoud van 16.


v0.5.41-beta — Netwerk toevoegen liep vast tijdens het typen

Een netwerk toevoegen gaf een JSON-parsefout in de browser.

Oorzaak: de live beoordeling die bij elke toetsaanslag afgaat, bouwde een lijst van alle bruikbare adressen in het bereik. Tijdens het intypen van 192.168.1.0/24 is de tussenstand 192.168.1.0/2 een volkomen geldig netwerk — met 1.073.741.824 adressen. Het verzoek liep daarop vast en de browser kreeg geen JSON meer terug.

  • review() rekent eerste adres, laatste adres en aantal nu uit in plaats van ze op te sommen. Antwoord binnen 0,05 seconde bij elk prefix.
  • suggest_range() had hetzelfde probleem: die somde alle /28-blokken op, wat bij een /8 ruim een miljoen blokken zijn. Rekent nu van achteren naar voren.
  • Waarschuwing bij een bereik van meer dan 4096 adressen, want dat is vrijwel altijd een typefout in het prefix.
  • Ook .env van de deploy-map werd bij elke deploy gewist door rsync --delete, omdat het bestand in .gitignore staat en dus niet in de checkout zit. Daardoor raakte je BIND telkens kwijt. --exclude='.env' toegevoegd.
  • Nieuw .env.example met alle instellingen bij elkaar en uitleg per sleutel.

v0.5.40-beta — Netwerken instellen zonder uitzoekwerk

🔍 Automatische detectie

Bij Instellingen → Netwerken → Netwerk toont Server Up nu bovenaan het netwerk dat je server zelf gebruikt: interface, subnet, gateway en een voorgestelde vrije IP-range. Eén klik vult het hele formulier.

De detectie start heel kort een container die de netwerknamespace van de host deelt en leest daar de routetabel. Dat is nodig omdat Server Up in een eigen namespace zit: de vorige versie las /sys/class/net binnen de container en toonde daardoor de bridge-interface van de container zelf in plaats van de netwerkkaart van de host — een lijstje dat er goed uitzag maar niet klopte.

Lukt detecteren niet (geen docker-socket, ouder image), dan staat er een nette melding en vul je het als vanouds handmatig in.

💡 Live meedenken bij het invullen

Terwijl je typt controleert Server Up de combinatie en toont wat het oplevert: "14 adressen beschikbaar: 192.168.1.241 tot en met 192.168.1.254". Klopt er iets niet, dan staat er in gewone taal bij waarom — een range buiten het subnet, een gateway die je beter buiten het bereik kunt houden, of een range zo klein dat er maar een paar stacks in passen.

Een knop stelt een vrij blok voor op basis van het subnet, met de gateway ontweken.

📖 Uitleg waar je hem nodig hebt

In het formulier zit een uitklapbaar blok dat uitlegt hoe subnet, gateway, IP-range en host-interface zich tot elkaar verhouden, met een concreet voorbeeld. docs/netwerken.md heeft dezelfde uitleg met een schema van de adresverdeling, een tabel met veelgebruikte ranges, en één punt dat er echt uit moet springen: het instellen van de IP-range in Server Up doet niets aan je router — je moet daar zelf het DHCP-bereik verkleinen, anders blijven dubbele adressen mogelijk.


v0.5.30-beta — Rollen, app-store-filter en twee planningsfouten

👥 Rollen per gebruiker

Tot nu toe had elk account volledige toegang. Nu drie rollen:

Rol Mag
Beheerder alles
Operator stacks, containers en backups beheren — niet de instellingen, gebruikers, repo's, netwerken of modules
Alleen lezen alleen bekijken
  • Afgedwongen in de before_request-guard, niet alleen in de interface.
  • Je eigen wachtwoord wijzigen mag iedereen, ongeacht rol.
  • De laatste beheerder kan niet gedegradeerd of verwijderd worden.
  • Wie via de reverse proxy binnenkomt zonder eigen account krijgt de rol uit AUTH.proxy_role; bestaat er wél een lokaal account met die naam, dan wint dat account.
  • Accounts van vóór deze versie hebben geen rol en gelden als beheerder.

🔒 Rechtenescalatie verholpen

Bij het wijzigen van een wachtwoord werd het hele gebruikersrecord vervangen, waardoor het role-veld wegviel. Omdat een ontbrekende rol als beheerder geldt (nodig voor bestaande accounts), kon elke operator of viewer zichzelf tot beheerder promoveren door zijn eigen wachtwoord te wijzigen. Gevonden bij het naspelen van de rollenflow; de rol blijft nu behouden en een test dekt het af.

Geplande backups draaiden nooit

De scheduler stempelde ook taken af die zichzelf hadden overgeslagen. De backup-taak bewaakt zelf of het het ingestelde uur is, dus die werd afgestempeld op het moment van de eerste tick — waarna het volgende moment 24 uur later op precies dat verkeerde tijdstip viel. Resultaat: de geplande backup kwam nooit aan de beurt.

Een taak stempelt nu alleen af als hij True teruggeeft. De backup-taak draait daardoor op een kort interval en bepaalt zelf wanneer het zover is, zodat een gewijzigde planning ook meteen werkt zonder herstart. Voor wekelijkse backups is BACKUP_SCHEDULE_DAY toegevoegd.

🔎 Zoeken en filteren in de app store

Zoekveld over naam, omschrijving en tags, plus klikbare tagfilters (de veertien meestgebruikte, op frequentie gesorteerd). Repo's zonder treffers vallen weg.

🔧 Overig

  • Opstartcontrole op de meegeleverde front-end-bestanden. Mislukt het downloaden tijdens de image-build, dan laadde de interface zonder opmaak terwijl de server prima leek te draaien; nu staat er een duidelijke melding in het log.
  • 227 tests.

v0.5.20-beta — Vertalingen, SSO-scherm, complete backups en app-updates

🌍 Vertalingen en SSO

  • De schermen uit v0.5.x (login, Beveiliging, Netwerken, Updates, containerpaneel) gebruikten harde Nederlandse teksten terwijl de rest van de interface via t() loopt. Op Engels gaf dat een mengelmoes. Nu volledig vertaald: 208 sleutels in nl en en.
  • t() ondersteunt plaatshouders: t('port_taken', {port: 8080}). Zo blijven zinnen heel in plaats van in losse stukjes geknipt.
  • Nieuw instelscherm voor reverse-proxy-SSO onder Instellingen → Beveiliging: modus lokaal/proxy/beide, identiteitsheader en de vertrouwde proxy-adressen. De backend bestond al maar was alleen met curl te bereiken.
  • Nieuwe test bewaakt dat nl en en dezelfde sleutels houden, dat de plaatshouders in beide talen gelijk zijn, en dat de interface geen sleutels gebruikt die nergens gedefinieerd staan.

💾 Backups compleet

Backup was één tar-commando waarvan de returncode genegeerd werd — een mislukte backup gold als succes, en terugzetten kon alleen met de hand.

  • Terugzetten met één klik: stack stoppen, huidige map opzij, uitpakken, starten. Mislukt het uitpakken, dan komt de oude situatie terug.
  • Bewaarbeleid: aantal per stack en/of maximale leeftijd. De nieuwste backup van een stack blijft altijd staan.
  • Geplande backups, dagelijks of wekelijks, via een eigen planner in de applicatie (geen cron). Een gemiste ronde loopt bij de eerstvolgende gelegenheid alsnog.
  • Automatisch een backup vóór het bijwerken of verwijderen van een stack. Mislukt die, dan gaat de actie door — anders kun je een kapotte stack niet meer opruimen.
  • Uitpakken gebeurt met tarfile en filter="data": absolute paden en ..-ingangen worden geweigerd. Met een kaal tar xzf kon een geprepareerd archief buiten de stackmap schrijven.
  • Backups binnen dezelfde seconde overschreven elkaar; namen krijgen nu een teller.
  • docs/backups.md legt uit wat er wél en niet in zit — de compose- en .env-bestanden, niet de Docker-volumes met de eigenlijke appdata.

📦 Updates per app

  • Server Up controleert nu ook of er nieuwere images zijn voor de geïnstalleerde stacks, met een badge op de stackkaart en een knop om te controleren. Bijwerken doet de bestaande update-knop.
  • De vergelijking loopt via de Registry API v2 (Docker-Content-Digest), niet via docker manifest inspect: dat laatste geeft per platform een aparte digest terug terwijl RepoDigests de digest van de manifest-list bevat, wat bij elk multi-arch image permanent "update beschikbaar" zou opleveren.
  • Drie statussen per service — update, current en unknown. Een lokaal gebouwd image, een nog niet gepulld image of een onbereikbare registry levert unknown op en dus géén badge, zodat er nooit ten onrechte een update wordt gemeld.
  • Dagelijkse achtergrondcheck via dezelfde planner; resultaten 6 uur gecached.

v0.5.10-beta — Update-systeem met kanalen en één-klik bijwerken

🔄 Kanalen

Server Up kent nu twee update-kanalen in plaats van een vinkje "pre-releases meenemen":

  • stable — alleen echte releases (v0.5.10)
  • beta — ook pre-releases (v0.5.10-beta1), die release.yml automatisch als zodanig markeert bij een tag met een streepje

Een release telt hoger dan zijn eigen beta (0.5.10-beta1 < 0.5.10), dus wie op beta zit krijgt de definitieve versie alsnog aangeboden. De oude instelling UPDATE_INCLUDE_PRERELEASE migreert automatisch naar het beta-kanaal.

⬇️ Bijwerken vanuit de interface

Nieuw: Nu bijwerken haalt het image uit je Forgejo container-registry en vervangt de eigen container.

  • Omdat een container zichzelf niet kan hercreëren, doet Server Up alleen het voorwerk (image ophalen, SU_TAG wegschrijven) en laat het de hercreatie over aan een korte helper-container die op het nieuwe image draait — dat bevat de docker- en compose-CLI al.
  • De deploy-map wordt uitgelezen uit de compose-labels van de eigen container, niet geraden. Ontbreken die labels, draait de container niet vanaf een registry-image, of is de docker-socket er niet, dan meldt de interface precies waaróm bijwerken niet kan in plaats van iets te proberen.
  • De vorige tag wordt onthouden, met een terugrolknop in de instellingen.
  • Voortgang van de docker pull loopt via het bestaande job-logvenster; daarna pollt de browser /healthz tot de nieuwe versie leeft.

🔧 Overig

  • Update-check wordt een uur gecached; Controleren forceert een verse check. Eerder deed elke paginalading een netwerkverzoek.
  • Laatst-gecontroleerd-tijdstip zichtbaar in de interface.
  • Registry-inloggegevens instelbaar voor een privé registry. Het token wordt net als de git-tokens nooit teruggegeven door de API (alleen een has_-vlag) en leeg laten betekent "ongewijzigd".
  • docker-compose.yml gebruikt ${SU_IMAGE:-server-up}:${SU_TAG:-latest}, zodat image en tag los instelbaar zijn. Zonder SU_IMAGE blijft alles werken zoals voorheen (lokaal bouwen).
  • deploy-prod.yml bouwt én pusht het image in dezelfde job wanneer SU_IMAGE ingesteld is. Bewust niet als aparte workflow: bij één runner zou de deploy wachten op een build die zelf nog in de wachtrij staat.
  • build.yml is nu alleen handmatig, voor het herbouwen van een specifieke tag.
  • docs/updates.md toegevoegd: hoe het werkt, de complete Forgejo-instelling (registry, tokens, variables, secrets, runners), de release-procedure voor zowel stable als beta, terugrollen en een probleemoplostabel.

v0.5.00-beta — Authenticatie, beveiliging en eigen IP-adressen

Let op bij het bijwerken. Server Up heeft nu een login. Open na het bijwerken meteen de webinterface en maak een beheerdersaccount aan — zolang dat niet gebeurd is, kan iedereen die de pagina bereikt het account claimen. De poort wordt voortaan standaard op 127.0.0.1 gebonden; zet BIND=0.0.0.0 in je .env als je er van buiten de server bij moet (liefst achter een reverse proxy met TLS — zie docs/beveiliging.md).

🔐 Authenticatie

Tot nu toe was elke /api/*-route open. Omdat Server Up de docker-socket als root gebruikt, betekende dat: wie de poort kon bereiken, had root op de host.

  • Lokale accounts met scrypt-gehashte wachtwoorden, sessiecookie (HttpOnly, SameSite=Strict) en lockout na vijf mislukte pogingen.
  • Optionele SSO via een reverse-proxy-header (Authelia/Authentik/Cloudflare Access). De header wordt alleen vertrouwd vanaf een geconfigureerd proxy-IP.
  • Loginscherm en eerste-account-setup in de interface; gebruikersbeheer en wachtwoord wijzigen onder Instellingen → Beveiliging.

🛡️ Beveiligingsfixes

  • CSRF: elke mutatie vereist een X-CSRF-Token-header. Routes die de toestand wijzigen accepteren geen GET meer — /api/docker/restart was eerder met een <img>-tag vanaf een willekeurige website te triggeren.
  • Path traversal: /api/store/install controleerde de instantienaam niet; "instance": "../../…" schreef buiten de library. Alle stack-, instantie- en repo-namen lopen nu door één safe_name()-validatie.
  • Tokenlek: git-tokens werden teruggegeven door /api/repos en /api/settings. Die zijn vervangen door een has_token-vlag; opslaan met een leeg veld wist het bestaande token niet meer.
  • Git-URL's: alleen http(s)://, ssh:// en git@host:pad. Git's ext::-transport voert een shell-commando uit en wordt nu geweigerd.
  • Templates renderen in een Jinja2-sandbox (server-side template injection).
  • SSH: host-keys worden geverifieerd (accept-new + /data/known_hosts); eerder stond StrictHostKeyChecking=no, waarmee elke MITM onzichtbaar was.
  • Modules: repo's worden niet meer automatisch bij elke start gepulld (AUTO_SYNC_ON_BOOT, standaard uit) en kunnen per repo op een commit worden vastgezet.
  • Productie-WSGI-server (waitress) i.p.v. de Flask-ontwikkelserver, limiet op request-grootte, ProxyFix, en CSP/X-Frame-Options/nosniff/ Referrer-Policy-headers.
  • Tailwind, Alpine en htmx worden meegeleverd in plaats van vanaf een CDN geladen; Google Fonts is eruit. De interface werkt nu ook offline.
  • config.json en de sleutel staan op 0600.

🌐 Stacks met een eigen IP-adres

Nieuw: geef een stack een eigen adres in je LAN in plaats van poorten op de host (macvlan/ipvlan). Geen poortconflicten meer, apps op hun eigen standaardpoort, en je kunt per app firewallen.

  • Netwerkbeheer onder Instellingen → Netwerken (driver, host-interface, subnet, gateway, optionele IP-range).
  • Bij het installeren kies je "Poorten op de host" of een netwerk; Server Up stelt het eerstvolgende vrije adres voor en houdt toegekende adressen vast.
  • Het gerenderde compose-bestand wordt automatisch omgezet: poortmappings eruit, netwerk met ipv4_address erin. De templates in apps/ blijven ongewijzigd.
  • Uitleg en valkuilen (waaronder de shim-interface die de host nodig heeft om zijn eigen macvlan-containers te bereiken) staan in docs/netwerken.md.

🧩 Beheer per container

De stackkaart klapt uit naar de losse containers: per container starten, stoppen, herstarten, logs bekijken en live CPU-/geheugengebruik.

📦 Veiliger installeren

  • Poortvelden krijgen een vrij poortnummer voorgesteld — next_free_port() bestond al maar werd nergens gebruikt. Bezette poorten worden gemeld.
  • Velden voor tokens en wachtwoorden krijgen een genereerknop.
  • Compose wordt gevalideerd (docker compose config) vóór het wegschrijven, dus een typefout in de editor maakt een draaiende stack niet meer onstartbaar.

🔧 Overig

  • Testsuite met pytest (112 tests), ook als stap in beide deploy-workflows.
  • Audit-log gebruikt één gedeelde SQLite-verbinding — elke job lekte eerder een file descriptor. Joblogs worden afgekapt op 2000 regels.
  • Het VERSION-bestand is de enige bron voor het versienummer.
  • Lichte /healthz voor de healthcheck in plaats van docker info.
  • fix-config.sh verwijderd: bevatte een hardgecodeerd intern IP en overschreef de configuratie van de gebruiker.

v0.4.60 — Tweecijferig patch-nummer

Versiebeleid vanaf v0.4.60

Het patch-deel (laatste cijfer) gebruikt nu twee cijfers en loopt in tientallen: 0.4.60, 0.4.61, … 0.4.69, 0.4.70. Blijft semver-compatibel (het deel wordt als geheel getal vergeleken, dus 0.4.60 > 0.4.6).


v0.4.6 — Kaal versienummer op alle builds

Wijziging in v0.4.6

De deploy-workflows tonen nu overal het kale versienummer uit het VERSION- bestand (of de tagnaam), zonder -dev/-rc.<sha>-suffix. Eenvoudiger te lezen; bump bij elke wijziging gewoon VERSION.


v0.4.5 — Releases & in-app update-check

Nieuw in v0.4.5

🏷️ Automatische releases bij een tag

Push je een v*-tag, dan maakt de nieuwe release.yml-workflow automatisch een Forgejo-Release aan met notes uit de bijbehorende CHANGELOG.md-sectie plus een commit-overzicht sinds de vorige tag. Pre-release tags (bv. v0.8.4-beta1) worden als pre-release gemarkeerd. Zo krijg je een Releases-pagina met duidelijke changelog per versie, vergelijkbaar met GitHub Releases.

🔔 In-app "update beschikbaar"

Server Up vergelijkt de draaiende versie met de laatste release via de Forgejo/ GitHub Releases-API (semver-vergelijking, pre-releases optioneel). Is er een nieuwere versie, dan verschijnt een melding in de topbar en een blok in Instellingen → Updates met versie, release-notes en een link. Instelbaar via UPDATE_API_URL (of env SU_UPDATE_API) en de optie "pre-releases meenemen".

🔖 Versiebeleid

Bump bij elke wijziging het centrale VERSION-bestand; tags zijn de bron voor release-versies (vX.Y.Z, of -beta/-rc voor pre-releases).


v0.4.4 — Nette versienummers op alle builds

Nieuw in v0.4.4

Centraal VERSION-bestand (semver) is nu de bron voor het versienummer. De deploy-workflows lezen het en bouwen:

  • tag v0.4.40.4.4 (productie, toont v0.4.4)
  • push naar main0.4.4-rc.<sha>
  • push naar dev0.4.4-dev.<sha>

Zo zie je voortaan het échte versienummer in de topbar i.p.v. alleen de git-SHA (vdev-1eab7c07). Bump bij elke wijziging alleen nog VERSION (en voor de zekerheid de fallback in app.py/index.html voor lokale runs).


v0.4.3 — Auto-logo's legacy-apps + fix lege Docker Images

Nieuw / fixes in v0.4.3

🖼️ Automatische logo's voor legacy-apps

Legacy-apps (eigen app.json/stack.json-stacks) krijgen nu automatisch een logo via de dashboard-icons CDN, afgeleid uit de app-naam (bv. "Nextcloud" → nextcloud.png). Een expliciete logo-URL in de metadata wint; bestaat het geraden icoon niet, dan valt de UI terug op het emoji-icoon. Geldt voor zowel de App Store als geïnstalleerde stacks.

🐛 Fix: Docker Images-pagina was leeg

De afbeeldingenlijst gebruikte :key="img.id", maar meerdere tags kunnen dezelfde image-ID delen → dubbele Alpine-keys waardoor de tabel niet rendert (en de "geen images"-melding ook niet, want er waren wél images). De x-for gebruikt nu de index als key.


v0.4.2 — App-logo's bij stacks

Nieuw in v0.4.2

Geïnstalleerde stacks tonen nu het app-logo (of een emoji-icoon als fallback), zowel op het dashboard als op de Stacks-pagina. Bij installatie wordt het logo/icoon van de bron-app opgeslagen in .serverup.json in de stack-map. Bestaande installaties krijgen hun logo via een naam-match met de App Store, dus ze hoeven niet opnieuw geïnstalleerd te worden. De store toonde al logo's voor boilerplate-apps (via de selfhst/dashboard-icons CDN).


v0.4.1 — Fix: styling werd niet toegepast

Fix bovenop v0.4.0

De volledige Tailwind-stylesheet faalde stil omdat .modal via @apply de eigen CSS-animatieklasse anim toepaste. Tailwind's @apply accepteert alleen Tailwind-utilities, geen losse CSS-klassen, waardoor de hele Play-CDN-compilatie afbrak en de pagina ongestyled (kaal HTML) werd geladen. De animatie staat nu als gewone CSS-regel (.anim, .modal { animation: … }) los van @apply.


v0.4.0 — Volledig nieuw UI-ontwerp (Modern SaaS)

Nieuw in v0.4.0

🎨 Compleet herontworpen interface

Volledig nieuw, licht en ruim "Modern SaaS"-ontwerp (Inter-font, indigo accent, zachte schaduwen, ronde 2xl-kaarten). Nieuwe app-shell: verticale sidebar met merk-header, gegroepeerde navigatie met actieve indicator en live status-footer; slanke sticky topbar met dynamische paginatitel en status-chips. Dashboard, stacks, app store, images-/audittabellen, instellingen, modals, wizard, toasts en het log-paneel zijn allemaal opnieuw vormgegeven. Inklapbaar menu en grote touch-targets blijven behouden; volledig mobielvriendelijk.

🌗 Thema volgt systeemvoorkeur

Standaard volgt het thema de OS-voorkeur (licht/donker) en reageert live op wijzigingen. Handmatige override via Auto / Light / Dark in Instellingen.


v0.3.1 — UI-restyle + taalmodule (add-on)

Nieuw in v0.3.1

🎨 Grondige UI-restyle

Grotere, touch-vriendelijke knoppen (min. 44px), ruimere spacing, grotere typografie en kaarten. Layout gecentreerd met max-breedte voor meer lucht op grote schermen. Volledig mobielvriendelijk.

📐 Inklapbaar menu (ook op desktop)

De hamburger klapt de sidebar nu ook op desktop in/uit; voorkeur wordt onthouden (localStorage), hoofdinhoud en log-paneel schuiven mee. Op mobiel een tap-to-close overlay.

🌐 Taalmodule als add-on

Nieuwe sectie in Instellingen → "Taalmodule": talen toevoegen, bewerken, verwijderen en ingebouwde talen (NL/EN) dupliceren als startpunt. Editor toont per sleutel de Engelse referentie met zoekfilter. Backend-endpoints: POST/DELETE /api/i18n, GET /api/i18n/keys, GET /api/i18n/<code>/raw. Toegevoegde talen komen als JSON in translations/ en zijn direct beschikbaar.


v0.3.01 — Boilerplates fixes + git-driven versioning

Fixes bovenop v0.3.0

🔁 Auto-migratie van default-repos voor upgraders

Bestaande installaties (vanaf v0.2.x) hadden de Boilerplates-repo niet zichtbaar in de App Store omdat hun config.json in de su-data volume al bestond en de nieuwe DEFAULTS["APP_REPOS"] daardoor werd overschreven.

core/__init__.py load() doet nu een eenmalige migratie: ontbrekende default-repos worden bij opstart aangevuld op basis van id, en gemarkeerd in MIGRATIONS_DONE: ["v0.3.0_default_repos"]. Wordt direct gepersisteerd. Verwijdert een gebruiker de Boilerplates-repo expliciet, dan komt-ie niet automatisch terug.

🧹 YAML-poetsstap na boilerplate-render

Bij stacks waar de meeste optionele groepen uit staan (Authentik, Nextcloud, etc.) liet de Jinja-render verlaten mapping-sleutels achter (volumes:, networks:, etc. zonder kinderen). Docker-compose faalt daarop met "block sequence entries are not allowed in this context".

core/boilerplates.py _tidy() heeft nu een _drop_empty_mappings() substep die in meerdere passes mapping-sleutels verwijdert die alleen worden gevolgd door whitespace/commentaar of een sibling op gelijke/lagere indent. Werkt in cascade.

🏷️ Versie komt nu uit git

Dockerfile accepteert ARG SU_VERSION=dev en bakt die in ENV SU_VERSION + OCI image-label. Met docker build --build-arg SU_VERSION=$(git describe ...) weet het image zijn eigen versie. De Server Up UI toont automatisch de juiste waarde, ongeacht wat de productie-compose meegeeft.

Forgejo Actions kan een tag-push automatisch verwerken — zie server-up-deploy/README.md.

Migratie vanaf v0.3.0

docker compose build --no-cache
docker compose up -d

v0.3.0 — UI rebuild + Boilerplates support

Hoogtepunten

🎨 Nieuwe UI (Tailwind + Alpine + HTMX)

  • templates/index.html is volledig herschreven. De handgeschreven CSS (--bg/--s1/... variabelen, ad-hoc grid-classes) is vervangen door Tailwind utility-classes met een gematchte donker/licht-palette.
  • Statebeheer via Alpine.js: één app() component bovenop het hele document, geen $=document.getElementById-spaghetti meer.
  • HTMX is geladen voor toekomstige server-rendered partials. Het bestaande fetch-RPC patroon blijft werken; HTMX kan progressief worden ingezet.
  • Modals, toasts, terminal-overlay en first-run wizard zitten allemaal in één Alpine-state — geen losse globale variabelen meer.
  • Mobiele sidebar gedraagt zich nu correct (slide-in i.p.v. layout-flip).

🧩 ChristianLempa Boilerplates ondersteund

Server Up herkent nu twee stack-formaten naast elkaar:

Formaat Detectie Bron
Compose (origineel) compose.yml / docker-compose.yml (+ optioneel stack.json) bes-r/server-up
Boilerplate (nieuw) template.json + files/ ChristianLempa/boilerplates-library

Nieuw bestand app/core/boilerplates.py:

  • is_boilerplate(d) — detectie
  • metadata(d) — converteert template.json["metadata"] (incl. selfhst-icons) naar Server-Up formaat
  • fields(d) — flattened variable-schema voor de install-modal
  • render_to_dir(src, dest, values) — rendert files/*.yaml met de Jinja-achtige << var >> + <%- if expr %> syntax die de boilerplates gebruiken (Jinja2 met custom delimiters). Niet-tekst bestanden worden verbatim gekopieerd.

app/core/git.py_scan_compose_dirs herkent beide formaten en zet format: "boilerplate" in de stack-entry zodat de UI er een badge bij kan tonen.

app/app.py:

  • _find_stack_src accepteert ook boilerplate-mappen.
  • POST /api/store/preview retourneert voor boilerplates het variabelen-schema (fields) plus een gerenderde preview met defaults.
  • POST /api/store/install met body {values: {...}} rendert de templates voor je voordat de stack gestart wordt.

⚙️ Default-repos

Een nieuwe Server Up komt nu uit de doos met twee app-repositories:

  1. bes-r/server-up — eigen stacks, submap apps
  2. ChristianLempa/boilerplates-library — community templates, submap compose

📦 Versie / Dockerfile

  • SU_VERSION = 0.3.0 in Dockerfile en docker-compose.yml
  • requirements.txt: Jinja2 expliciet toegevoegd (was al een Flask-dep)

Migratie vanaf v0.2.29

  • Build opnieuw: docker compose up -d --build --no-cache
  • Bestaande stacks blijven werken (legacy formaat is intact).
  • De Boilerplates-repo wordt automatisch toegevoegd voor verse installs. Bestaande gebruikers kunnen de repo handmatig toevoegen via Instellingen → Git Repositories met submap compose.

Bekende beperkingen

  • Boilerplate-templates met onbekende custom-filters of complexe Ansible-style conditionals kunnen falen — de fout verschijnt in het terminal-paneel.
  • volumes: blokken die conditioneel zijn (<%- if volume_mode == 'local' %>) werken; complexere render-logica (loops over services) is nog niet getest.
  • HTMX is geladen maar de meeste interacties draaien nog op fetch-RPC. Verdere migratie naar server-rendered partials kan stapsgewijs in volgende releases.