v0.6.00-beta - 29 apps erbij, en named volumes die verdwenen
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 1m5s

Bij het toevoegen van de apps kwam een bestaande fout boven water.
_drop_empty_mappings ruimt sleutels op die een <% if %>-blok leeg achterlaat,
maar een named volume wordt gedeclareerd als een kale sleutel zonder waarde
(`  immich_pgdata:`). Die werd opgeruimd, waarna ook het bovenliggende
`volumes:` wegviel en de services verwezen naar een volume dat nergens meer
gedeclareerd stond. Immich, Ghost, Miniflux, Paperless-ngx, Unifi Network en
Vikunja waren daardoor niet installeerbaar: docker compose weigert met "refers
to undefined volume". Alleen bekende compose-sleutels worden nu opgeruimd.

29 nieuwe apps (teller op 65):
- Domotica: zigbee2mqtt, mosquitto, esphome, node-red, zwave-js-ui,
  homebridge, scrypted, frigate
- 3D-printen: bambuddy, octoprint, spoolman
- Gezin/huishouden: baby-buddy, grocy, tandoor, firefly-iii
- Media: audiobookshelf, navidrome, kavita, calibre-web, jellyseerr
- Kennis: bookstack, freshrss, wallabag
- Netwerk/beheer: wg-easy, homepage, searxng, duplicati, watchtower,
  technitium-dns

Nieuwe tests/test_apps.py rendert elke app met de echte engine en controleert
geldige YAML, image of build per service, gedeclareerde named volumes, geen
onvervangen variabelen en geen onbedoeld dubbele standaardpoorten. Die laatste
vond zeven botsingen tussen nieuwe en bestaande apps; rechtgezet. Logische
botsingen (reverse proxies op 80/443, dns-blokkers op 53) staan met uitleg in
een allowlist.

Het image van Bambuddy is nagetrokken op de bron
(github.com/maziggy/bambuddy): ghcr.io/maziggy/bambuddy, poort 8000.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C7oLCRYzY5ixJ5Sv8Y8EFb
This commit is contained in:
Ramon 2026-07-27 08:23:58 +02:00
parent 7f0d34cb1a
commit b8c8e89fa0
63 changed files with 2656 additions and 3 deletions

View file

@ -1,3 +1,56 @@
# 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:
```yaml
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 # v0.5.50-beta — Containers die nergens te vinden waren
Het dashboard meldde 13 actieve containers en 17 images, terwijl je die nergens Het dashboard meldde 13 actieve containers en 17 images, terwijl je die nergens

View file

@ -1 +1 @@
0.5.50-beta 0.6.00-beta

View file

@ -0,0 +1,13 @@
services:
<< service_name >>:
image: ghcr.io/advplyr/audiobookshelf:latest
container_name: << service_name >>
ports:
- "<< port >>:80"
environment:
- TZ=<< timezone >>
volumes:
- << media_dir >>:/audiobooks
- << data_dir >>/<< service_name >>/config:/config
- << data_dir >>/<< service_name >>/metadata:/metadata
restart: unless-stopped

View file

@ -0,0 +1,61 @@
{
"kind": "compose",
"metadata": {
"name": "Audiobookshelf",
"description": "Server voor luisterboeken en podcasts, met apps voor iOS en Android die je voortgang bijhouden.",
"tags": [
"media",
"luisterboeken",
"podcast"
],
"icon": {
"provider": "selfhst",
"id": "audiobookshelf"
},
"version": {
"name": "latest"
}
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "audiobookshelf",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Web poort",
"default": 13378,
"required": true
},
{
"name": "data_dir",
"type": "str",
"title": "Data directory",
"default": "/opt/serverup/appdata",
"required": true
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
},
{
"name": "media_dir",
"type": "str",
"title": "Map met luisterboeken",
"default": "/opt/serverup/media/audiobooks",
"required": true
}
]
}
]
}

View file

@ -0,0 +1,16 @@
services:
<< service_name >>:
image: ghcr.io/babybuddy/babybuddy:latest
container_name: << service_name >>
ports:
- "<< port >>:8000"
environment:
- TZ=<< timezone >>
- PUID=<< puid >>
- PGID=<< pgid >>
- SECRET_KEY=<< secret_key >>
- ALLOWED_HOSTS=<< allowed_hosts >>
- CSRF_TRUSTED_ORIGINS=http://<< allowed_hosts >>
volumes:
- << data_dir >>/<< service_name >>/config:/app/data
restart: unless-stopped

View file

@ -0,0 +1,88 @@
{
"kind": "compose",
"metadata": {
"name": "Baby Buddy",
"description": "Houdt voedingen, slaap, luiers en groei van je kind bij. Handig om het overzicht te bewaren in de eerste maanden.",
"tags": [
"gezin",
"gezondheid",
"logboek"
],
"icon": {
"provider": "selfhst",
"id": "baby-buddy"
},
"version": {
"name": "latest"
}
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "baby-buddy",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Web poort",
"default": 8000,
"required": true
},
{
"name": "data_dir",
"type": "str",
"title": "Data directory",
"default": "/opt/serverup/appdata",
"required": true
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
},
{
"name": "puid",
"type": "int",
"title": "Gebruikers-ID (PUID)",
"default": 1000,
"required": true,
"description": "Bepaalt de eigenaar van de bestanden; `id -u` op je host"
},
{
"name": "pgid",
"type": "int",
"title": "Groeps-ID (PGID)",
"default": 1000,
"required": true
},
{
"name": "secret_key",
"type": "str",
"title": "Geheime sleutel",
"default": "",
"required": true,
"description": "Willekeurige waarde voor sessieversleuteling; gebruik de dobbelsteenknop",
"config": {
"placeholder": "willekeurige-lange-string"
}
},
{
"name": "allowed_hosts",
"type": "str",
"title": "Toegestane hostnamen",
"default": "*",
"required": true,
"description": "Bijvoorbeeld babybuddy.thuis.nl; * staat alles toe"
}
]
}
]
}

View file

@ -0,0 +1,18 @@
services:
<< service_name >>:
image: ghcr.io/maziggy/bambuddy:latest
container_name: << service_name >>
ports:
- "<< port >>:8000"
environment:
- TZ=<< timezone >>
- PUID=<< puid >>
- PGID=<< pgid >>
- PORT=8000
volumes:
- << data_dir >>/<< service_name >>/data:/app/data
- << data_dir >>/<< service_name >>/logs:/app/logs
cap_add:
- NET_BIND_SERVICE
stop_grace_period: 30s
restart: unless-stopped

View file

@ -0,0 +1,69 @@
{
"kind": "compose",
"metadata": {
"name": "Bambuddy",
"description": "Zelfgehost bedieningspaneel voor Bambu Lab-printers, van één A1 tot een hele printerfarm. Zonder cloud.",
"tags": [
"3d-printen",
"bambu-lab",
"printerbeheer"
],
"icon": {
"provider": "selfhst",
"id": "bambuddy"
},
"version": {
"name": "latest"
}
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "bambuddy",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Web poort",
"default": 8011,
"required": true
},
{
"name": "data_dir",
"type": "str",
"title": "Data directory",
"default": "/opt/serverup/appdata",
"required": true
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
},
{
"name": "puid",
"type": "int",
"title": "Gebruikers-ID (PUID)",
"default": 1000,
"required": true,
"description": "Bepaalt de eigenaar van de bestanden; `id -u` op je host"
},
{
"name": "pgid",
"type": "int",
"title": "Groeps-ID (PGID)",
"default": 1000,
"required": true
}
]
}
]
}

View file

@ -0,0 +1,36 @@
services:
<< service_name >>:
image: lscr.io/linuxserver/bookstack:latest
container_name: << service_name >>
ports:
- "<< port >>:80"
environment:
- TZ=<< timezone >>
- PUID=<< puid >>
- PGID=<< pgid >>
- APP_URL=<< app_url >>
- DB_HOST=<< service_name >>-db
- DB_PORT=3306
- DB_USERNAME=bookstack
- DB_PASSWORD=<< db_password >>
- DB_DATABASE=bookstack
volumes:
- << data_dir >>/<< service_name >>/config:/config
depends_on:
- << service_name >>-db
restart: unless-stopped
<< service_name >>-db:
image: lscr.io/linuxserver/mariadb:latest
container_name: << service_name >>-db
environment:
- TZ=<< timezone >>
- PUID=<< puid >>
- PGID=<< pgid >>
- MYSQL_ROOT_PASSWORD=<< db_password >>
- MYSQL_DATABASE=bookstack
- MYSQL_USER=bookstack
- MYSQL_PASSWORD=<< db_password >>
volumes:
- << data_dir >>/<< service_name >>/db:/config
restart: unless-stopped

View file

@ -0,0 +1,88 @@
{
"kind": "compose",
"metadata": {
"name": "BookStack",
"description": "Wiki voor documentatie, geordend in boeken, hoofdstukken en pagina's. Prettig voor handleidingen thuis.",
"tags": [
"documentatie",
"wiki",
"kennis"
],
"icon": {
"provider": "selfhst",
"id": "bookstack"
},
"version": {
"name": "latest"
}
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "bookstack",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Web poort",
"default": 6875,
"required": true
},
{
"name": "data_dir",
"type": "str",
"title": "Data directory",
"default": "/opt/serverup/appdata",
"required": true
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
},
{
"name": "puid",
"type": "int",
"title": "Gebruikers-ID (PUID)",
"default": 1000,
"required": true,
"description": "Bepaalt de eigenaar van de bestanden; `id -u` op je host"
},
{
"name": "pgid",
"type": "int",
"title": "Groeps-ID (PGID)",
"default": 1000,
"required": true
},
{
"name": "db_password",
"type": "str",
"title": "Database wachtwoord",
"default": "",
"required": true,
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
"config": {
"placeholder": "sterk-wachtwoord"
}
},
{
"name": "app_url",
"type": "str",
"title": "Basis-URL",
"default": "http://localhost:6875",
"required": true,
"description": "Het adres waarop je BookStack benadert; links kloppen anders niet"
}
]
}
]
}

View file

@ -0,0 +1,14 @@
services:
<< service_name >>:
image: lscr.io/linuxserver/calibre-web:latest
container_name: << service_name >>
ports:
- "<< port >>:8083"
environment:
- TZ=<< timezone >>
- PUID=<< puid >>
- PGID=<< pgid >>
volumes:
- << data_dir >>/<< service_name >>/config:/config
- << books_dir >>:/books
restart: unless-stopped

View file

@ -0,0 +1,77 @@
{
"kind": "compose",
"metadata": {
"name": "Calibre-Web",
"description": "Webinterface voor je Calibre-bibliotheek: bladeren, lezen en naar je e-reader sturen.",
"tags": [
"media",
"boeken",
"ebooks"
],
"icon": {
"provider": "selfhst",
"id": "calibre-web"
},
"version": {
"name": "latest"
}
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "calibre-web",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Web poort",
"default": 8083,
"required": true
},
{
"name": "data_dir",
"type": "str",
"title": "Data directory",
"default": "/opt/serverup/appdata",
"required": true
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
},
{
"name": "puid",
"type": "int",
"title": "Gebruikers-ID (PUID)",
"default": 1000,
"required": true,
"description": "Bepaalt de eigenaar van de bestanden; `id -u` op je host"
},
{
"name": "pgid",
"type": "int",
"title": "Groeps-ID (PGID)",
"default": 1000,
"required": true
},
{
"name": "books_dir",
"type": "str",
"title": "Calibre-bibliotheek",
"default": "/opt/serverup/media/books",
"required": true,
"description": "Map met je bestaande metadata.db van Calibre"
}
]
}
]
}

View file

@ -0,0 +1,15 @@
services:
<< service_name >>:
image: lscr.io/linuxserver/duplicati:latest
container_name: << service_name >>
ports:
- "<< port >>:8200"
environment:
- TZ=<< timezone >>
- PUID=<< puid >>
- PGID=<< pgid >>
volumes:
- << data_dir >>/<< service_name >>/config:/config
- << source_dir >>:/source:ro
- << backup_dir >>:/backups
restart: unless-stopped

View file

@ -0,0 +1,83 @@
{
"kind": "compose",
"metadata": {
"name": "Duplicati",
"description": "Versleutelde back-ups naar een NAS, externe schijf of cloudopslag, met een planner.",
"tags": [
"backup",
"opslag",
"beheer"
],
"icon": {
"provider": "selfhst",
"id": "duplicati"
},
"version": {
"name": "latest"
}
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "duplicati",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Web poort",
"default": 8200,
"required": true
},
{
"name": "data_dir",
"type": "str",
"title": "Data directory",
"default": "/opt/serverup/appdata",
"required": true
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
},
{
"name": "puid",
"type": "int",
"title": "Gebruikers-ID (PUID)",
"default": 1000,
"required": true,
"description": "Bepaalt de eigenaar van de bestanden; `id -u` op je host"
},
{
"name": "pgid",
"type": "int",
"title": "Groeps-ID (PGID)",
"default": 1000,
"required": true
},
{
"name": "source_dir",
"type": "str",
"title": "Map om te back-uppen",
"default": "/opt/serverup",
"required": true
},
{
"name": "backup_dir",
"type": "str",
"title": "Bestemming",
"default": "/opt/serverup/backups",
"required": true
}
]
}
]
}

View file

@ -0,0 +1,12 @@
services:
<< service_name >>:
image: ghcr.io/esphome/esphome:stable
container_name: << service_name >>
ports:
- "<< port >>:6052"
environment:
- TZ=<< timezone >>
volumes:
- << data_dir >>/<< service_name >>/config:/config
- /etc/localtime:/etc/localtime:ro
restart: unless-stopped

View file

@ -0,0 +1,54 @@
{
"kind": "compose",
"metadata": {
"name": "ESPHome",
"description": "Bouw en beheer firmware voor ESP32/ESP8266-sensoren via een webinterface. Levert kant-en-klare Home Assistant-integratie.",
"tags": [
"domotica",
"esp32",
"diy"
],
"icon": {
"provider": "selfhst",
"id": "esphome"
},
"version": {
"name": "stable"
}
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "esphome",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Web poort",
"default": 6052,
"required": true
},
{
"name": "data_dir",
"type": "str",
"title": "Data directory",
"default": "/opt/serverup/appdata",
"required": true
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
}
]
}

View file

@ -0,0 +1,34 @@
services:
<< service_name >>:
image: fireflyiii/core:latest
container_name: << service_name >>
ports:
- "<< port >>:8080"
environment:
- TZ=<< timezone >>
- APP_KEY=<< app_key >>
- DB_CONNECTION=pgsql
- DB_HOST=<< service_name >>-db
- DB_PORT=5432
- DB_DATABASE=firefly
- DB_USERNAME=firefly
- DB_PASSWORD=<< db_password >>
volumes:
- << data_dir >>/<< service_name >>/upload:/var/www/html/storage/upload
depends_on:
- << service_name >>-db
restart: unless-stopped
<< service_name >>-db:
image: postgres:16-alpine
container_name: << service_name >>-db
environment:
- POSTGRES_DB=firefly
- POSTGRES_USER=firefly
- POSTGRES_PASSWORD=<< db_password >>
volumes:
- << service_name >>_pgdata:/var/lib/postgresql/data
restart: unless-stopped
volumes:
<< service_name >>_pgdata:

View file

@ -0,0 +1,76 @@
{
"kind": "compose",
"metadata": {
"name": "Firefly III",
"description": "Huishoudboekje met budgetten, rekeningen en rapportages. Kan bankafschriften importeren.",
"tags": [
"financien",
"huishouden",
"budget"
],
"icon": {
"provider": "selfhst",
"id": "firefly-iii"
},
"version": {
"name": "latest"
}
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "firefly-iii",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Web poort",
"default": 8093,
"required": true
},
{
"name": "data_dir",
"type": "str",
"title": "Data directory",
"default": "/opt/serverup/appdata",
"required": true
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
},
{
"name": "db_password",
"type": "str",
"title": "Database wachtwoord",
"default": "",
"required": true,
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
"config": {
"placeholder": "sterk-wachtwoord"
}
},
{
"name": "app_key",
"type": "str",
"title": "App-sleutel",
"default": "",
"required": true,
"description": "Exact 32 tekens; gebruik de dobbelsteenknop en kort zo nodig in",
"config": {
"placeholder": "32-tekens-lange-sleutel"
}
}
]
}
]
}

View file

@ -0,0 +1,13 @@
services:
<< service_name >>:
image: freshrss/freshrss:latest
container_name: << service_name >>
ports:
- "<< port >>:80"
environment:
- TZ=<< timezone >>
- CRON_MIN=*/20
volumes:
- << data_dir >>/<< service_name >>/data:/var/www/FreshRSS/data
- << data_dir >>/<< service_name >>/extensions:/var/www/FreshRSS/extensions
restart: unless-stopped

View file

@ -0,0 +1,69 @@
{
"kind": "compose",
"metadata": {
"name": "FreshRSS",
"description": "RSS-lezer die je nieuws en blogs op één plek verzamelt, zonder algoritme.",
"tags": [
"nieuws",
"rss",
"lezen"
],
"icon": {
"provider": "selfhst",
"id": "freshrss"
},
"version": {
"name": "latest"
}
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "freshrss",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Web poort",
"default": 8082,
"required": true
},
{
"name": "data_dir",
"type": "str",
"title": "Data directory",
"default": "/opt/serverup/appdata",
"required": true
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
},
{
"name": "puid",
"type": "int",
"title": "Gebruikers-ID (PUID)",
"default": 1000,
"required": true,
"description": "Bepaalt de eigenaar van de bestanden; `id -u` op je host"
},
{
"name": "pgid",
"type": "int",
"title": "Groeps-ID (PGID)",
"default": 1000,
"required": true
}
]
}
]
}

View file

@ -0,0 +1,20 @@
services:
<< service_name >>:
image: ghcr.io/blakeblackshear/frigate:stable
container_name: << service_name >>
shm_size: "<< shm_size >>"
ports:
- "<< port >>:8971"
- "<< rtsp_port >>:8554"
environment:
- TZ=<< timezone >>
volumes:
- << data_dir >>/<< service_name >>/config:/config
- << data_dir >>/<< service_name >>/media:/media/frigate
- /etc/localtime:/etc/localtime:ro
# Opnamebuffer in geheugen; voorkomt slijtage van je SSD
- type: tmpfs
target: /tmp/cache
tmpfs:
size: 1000000000
restart: unless-stopped

View file

@ -0,0 +1,69 @@
{
"kind": "compose",
"metadata": {
"name": "Frigate NVR",
"description": "Camerabewaking met objectherkenning die lokaal draait. Werkt samen met Home Assistant en MQTT.",
"tags": [
"camera",
"nvr",
"domotica"
],
"icon": {
"provider": "selfhst",
"id": "frigate"
},
"version": {
"name": "stable"
}
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "frigate",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Web poort",
"default": 8971,
"required": true
},
{
"name": "data_dir",
"type": "str",
"title": "Data directory",
"default": "/opt/serverup/appdata",
"required": true
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
},
{
"name": "rtsp_port",
"type": "int",
"title": "RTSP poort",
"default": 8554,
"required": true
},
{
"name": "shm_size",
"type": "str",
"title": "Gedeeld geheugen",
"default": "128mb",
"required": true,
"description": "Verhoog dit bij meer dan twee camera's (ongeveer 64mb per camera)"
}
]
}
]
}

View file

@ -0,0 +1,13 @@
services:
<< service_name >>:
image: lscr.io/linuxserver/grocy:latest
container_name: << service_name >>
ports:
- "<< port >>:80"
environment:
- TZ=<< timezone >>
- PUID=<< puid >>
- PGID=<< pgid >>
volumes:
- << data_dir >>/<< service_name >>/config:/config
restart: unless-stopped

69
apps/grocy/template.json Normal file
View file

@ -0,0 +1,69 @@
{
"kind": "compose",
"metadata": {
"name": "Grocy",
"description": "Voorraadbeheer voor je huishouden: boodschappen, houdbaarheidsdata, klusjes en recepten.",
"tags": [
"huishouden",
"voorraad",
"gezin"
],
"icon": {
"provider": "selfhst",
"id": "grocy"
},
"version": {
"name": "latest"
}
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "grocy",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Web poort",
"default": 9283,
"required": true
},
{
"name": "data_dir",
"type": "str",
"title": "Data directory",
"default": "/opt/serverup/appdata",
"required": true
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
},
{
"name": "puid",
"type": "int",
"title": "Gebruikers-ID (PUID)",
"default": 1000,
"required": true,
"description": "Bepaalt de eigenaar van de bestanden; `id -u` op je host"
},
{
"name": "pgid",
"type": "int",
"title": "Groeps-ID (PGID)",
"default": 1000,
"required": true
}
]
}
]
}

View file

@ -0,0 +1,13 @@
services:
<< service_name >>:
image: homebridge/homebridge:latest
container_name: << service_name >>
# HomeKit gebruikt mDNS voor het vinden van apparaten; dat werkt alleen
# betrouwbaar met het netwerk van de host, of met een eigen IP-adres.
network_mode: host
environment:
- TZ=<< timezone >>
- HOMEBRIDGE_CONFIG_UI_PORT=<< port >>
volumes:
- << data_dir >>/<< service_name >>:/homebridge
restart: unless-stopped

View file

@ -0,0 +1,54 @@
{
"kind": "compose",
"metadata": {
"name": "Homebridge",
"description": "Maakt apparaten die geen HomeKit ondersteunen alsnog zichtbaar in de Woning-app van Apple.",
"tags": [
"domotica",
"homekit",
"apple"
],
"icon": {
"provider": "selfhst",
"id": "homebridge"
},
"version": {
"name": "latest"
}
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "homebridge",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Web poort",
"default": 8581,
"required": true
},
{
"name": "data_dir",
"type": "str",
"title": "Data directory",
"default": "/opt/serverup/appdata",
"required": true
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
}
]
}

View file

@ -0,0 +1,14 @@
services:
<< service_name >>:
image: ghcr.io/gethomepage/homepage:latest
container_name: << service_name >>
ports:
- "<< port >>:3000"
environment:
- TZ=<< timezone >>
- HOMEPAGE_ALLOWED_HOSTS=<< allowed_hosts >>
volumes:
- << data_dir >>/<< service_name >>/config:/app/config
# Alleen-lezen socket voor de Docker-widget die containers toont
- /var/run/docker.sock:/var/run/docker.sock:ro
restart: unless-stopped

View file

@ -0,0 +1,62 @@
{
"kind": "compose",
"metadata": {
"name": "Homepage",
"description": "Startpagina met tegels voor al je diensten, inclusief live status en systeemgegevens.",
"tags": [
"dashboard",
"startpagina",
"overzicht"
],
"icon": {
"provider": "selfhst",
"id": "homepage"
},
"version": {
"name": "latest"
}
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "homepage",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Web poort",
"default": 3002,
"required": true
},
{
"name": "data_dir",
"type": "str",
"title": "Data directory",
"default": "/opt/serverup/appdata",
"required": true
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
},
{
"name": "allowed_hosts",
"type": "str",
"title": "Toegestane hostnamen",
"default": "localhost:3001",
"required": true,
"description": "Komma-gescheiden; anders weigert Homepage het verzoek"
}
]
}
]
}

View file

@ -0,0 +1,12 @@
services:
<< service_name >>:
image: fallenbagel/jellyseerr:latest
container_name: << service_name >>
ports:
- "<< port >>:5055"
environment:
- TZ=<< timezone >>
- LOG_LEVEL=info
volumes:
- << data_dir >>/<< service_name >>/config:/app/config
restart: unless-stopped

View file

@ -0,0 +1,54 @@
{
"kind": "compose",
"metadata": {
"name": "Jellyseerr",
"description": "Verzoeken voor films en series, gekoppeld aan Jellyfin of Plex en de *arr-stack.",
"tags": [
"media",
"verzoeken",
"films"
],
"icon": {
"provider": "selfhst",
"id": "jellyseerr"
},
"version": {
"name": "latest"
}
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "jellyseerr",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Web poort",
"default": 5055,
"required": true
},
{
"name": "data_dir",
"type": "str",
"title": "Data directory",
"default": "/opt/serverup/appdata",
"required": true
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
}
]
}

View file

@ -0,0 +1,14 @@
services:
<< service_name >>:
image: lscr.io/linuxserver/kavita:latest
container_name: << service_name >>
ports:
- "<< port >>:5000"
environment:
- TZ=<< timezone >>
- PUID=<< puid >>
- PGID=<< pgid >>
volumes:
- << data_dir >>/<< service_name >>/config:/config
- << library_dir >>:/library
restart: unless-stopped

76
apps/kavita/template.json Normal file
View file

@ -0,0 +1,76 @@
{
"kind": "compose",
"metadata": {
"name": "Kavita",
"description": "Leesserver voor strips, manga en e-books, met leesvoortgang per gebruiker.",
"tags": [
"media",
"boeken",
"strips"
],
"icon": {
"provider": "selfhst",
"id": "kavita"
},
"version": {
"name": "latest"
}
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "kavita",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Web poort",
"default": 5001,
"required": true
},
{
"name": "data_dir",
"type": "str",
"title": "Data directory",
"default": "/opt/serverup/appdata",
"required": true
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
},
{
"name": "puid",
"type": "int",
"title": "Gebruikers-ID (PUID)",
"default": 1000,
"required": true,
"description": "Bepaalt de eigenaar van de bestanden; `id -u` op je host"
},
{
"name": "pgid",
"type": "int",
"title": "Groeps-ID (PGID)",
"default": 1000,
"required": true
},
{
"name": "library_dir",
"type": "str",
"title": "Bibliotheekmap",
"default": "/opt/serverup/media/books",
"required": true
}
]
}
]
}

View file

@ -0,0 +1,14 @@
services:
<< service_name >>:
image: eclipse-mosquitto:2
container_name: << service_name >>
ports:
- "<< port >>:1883"
- "<< websocket_port >>:9001"
environment:
- TZ=<< timezone >>
volumes:
- << data_dir >>/<< service_name >>/config:/mosquitto/config
- << data_dir >>/<< service_name >>/data:/mosquitto/data
- << data_dir >>/<< service_name >>/log:/mosquitto/log
restart: unless-stopped

View file

@ -0,0 +1,61 @@
{
"kind": "compose",
"metadata": {
"name": "Mosquitto MQTT",
"description": "Lichtgewicht MQTT-broker. De centrale berichtenbus waar Zigbee2MQTT, ESPHome en Home Assistant op praten.",
"tags": [
"domotica",
"mqtt",
"infrastructuur"
],
"icon": {
"provider": "selfhst",
"id": "eclipse-mosquitto"
},
"version": {
"name": "2"
}
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "mosquitto",
"required": true
},
{
"name": "port",
"type": "int",
"title": "MQTT poort",
"default": 1883,
"required": true
},
{
"name": "data_dir",
"type": "str",
"title": "Data directory",
"default": "/opt/serverup/appdata",
"required": true
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
},
{
"name": "websocket_port",
"type": "int",
"title": "WebSocket poort",
"default": 9001,
"required": true
}
]
}
]
}

View file

@ -0,0 +1,14 @@
services:
<< service_name >>:
image: deluan/navidrome:latest
container_name: << service_name >>
ports:
- "<< port >>:4533"
environment:
- TZ=<< timezone >>
- ND_SCANSCHEDULE=<< scan_schedule >>
- ND_LOGLEVEL=info
volumes:
- << music_dir >>:/music:ro
- << data_dir >>/<< service_name >>/data:/data
restart: unless-stopped

View file

@ -0,0 +1,69 @@
{
"kind": "compose",
"metadata": {
"name": "Navidrome",
"description": "Muziekstreaming vanaf je eigen collectie. Werkt met alle Subsonic-apps.",
"tags": [
"media",
"muziek",
"streaming"
],
"icon": {
"provider": "selfhst",
"id": "navidrome"
},
"version": {
"name": "latest"
}
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "navidrome",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Web poort",
"default": 4533,
"required": true
},
{
"name": "data_dir",
"type": "str",
"title": "Data directory",
"default": "/opt/serverup/appdata",
"required": true
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
},
{
"name": "music_dir",
"type": "str",
"title": "Muziekmap",
"default": "/opt/serverup/media/music",
"required": true
},
{
"name": "scan_schedule",
"type": "str",
"title": "Scaninterval",
"default": "@every 1h",
"required": true,
"description": "Hoe vaak de collectie opnieuw wordt ingelezen"
}
]
}
]
}

View file

@ -0,0 +1,11 @@
services:
<< service_name >>:
image: nodered/node-red:latest
container_name: << service_name >>
ports:
- "<< port >>:1880"
environment:
- TZ=<< timezone >>
volumes:
- << data_dir >>/<< service_name >>/data:/data
restart: unless-stopped

View file

@ -0,0 +1,54 @@
{
"kind": "compose",
"metadata": {
"name": "Node-RED",
"description": "Visueel automatiseringen bouwen door blokken aan elkaar te knopen. Populair naast Home Assistant voor complexere logica.",
"tags": [
"domotica",
"automatisering",
"low-code"
],
"icon": {
"provider": "selfhst",
"id": "node-red"
},
"version": {
"name": "latest"
}
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "node-red",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Web poort",
"default": 1880,
"required": true
},
{
"name": "data_dir",
"type": "str",
"title": "Data directory",
"default": "/opt/serverup/appdata",
"required": true
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
}
]
}

View file

@ -0,0 +1,13 @@
services:
<< service_name >>:
image: octoprint/octoprint:latest
container_name: << service_name >>
ports:
- "<< port >>:80"
environment:
- TZ=<< timezone >>
volumes:
- << data_dir >>/<< service_name >>:/octoprint
devices:
- << usb_device >>:/dev/ttyACM0
restart: unless-stopped

View file

@ -0,0 +1,61 @@
{
"kind": "compose",
"metadata": {
"name": "OctoPrint",
"description": "Bedien en volg een 3D-printer via het netwerk, met webcam en plug-ins.",
"tags": [
"3d-printen",
"printerbeheer"
],
"icon": {
"provider": "selfhst",
"id": "octoprint"
},
"version": {
"name": "latest"
}
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "octoprint",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Web poort",
"default": 8087,
"required": true
},
{
"name": "data_dir",
"type": "str",
"title": "Data directory",
"default": "/opt/serverup/appdata",
"required": true
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
},
{
"name": "usb_device",
"type": "str",
"title": "Printer (USB)",
"default": "/dev/ttyACM0",
"required": true,
"description": "Seriële poort van je printer"
}
]
}
]
}

View file

@ -0,0 +1,12 @@
services:
<< service_name >>:
image: ghcr.io/koush/scrypted:latest
container_name: << service_name >>
# Camera-ontdekking en HomeKit vereisen mDNS op het lokale netwerk.
network_mode: host
environment:
- TZ=<< timezone >>
- SCRYPTED_SECURE_PORT=<< port >>
volumes:
- << data_dir >>/<< service_name >>:/server/volume
restart: unless-stopped

View file

@ -0,0 +1,54 @@
{
"kind": "compose",
"metadata": {
"name": "Scrypted",
"description": "Koppelt camera's aan HomeKit, Google Home en Alexa, met opname en bewegingsdetectie.",
"tags": [
"camera",
"domotica",
"nvr"
],
"icon": {
"provider": "selfhst",
"id": "scrypted"
},
"version": {
"name": "latest"
}
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "scrypted",
"required": true
},
{
"name": "port",
"type": "int",
"title": "HTTPS poort",
"default": 10443,
"required": true
},
{
"name": "data_dir",
"type": "str",
"title": "Data directory",
"default": "/opt/serverup/appdata",
"required": true
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
}
]
}

View file

@ -0,0 +1,19 @@
services:
<< service_name >>:
image: searxng/searxng:latest
container_name: << service_name >>
ports:
- "<< port >>:8080"
environment:
- TZ=<< timezone >>
- SEARXNG_BASE_URL=<< base_url >>
- SEARXNG_SECRET=<< secret_key >>
volumes:
- << data_dir >>/<< service_name >>/config:/etc/searxng
cap_drop:
- ALL
cap_add:
- CHOWN
- SETGID
- SETUID
restart: unless-stopped

View file

@ -0,0 +1,72 @@
{
"kind": "compose",
"metadata": {
"name": "SearXNG",
"description": "Eigen zoekmachine die resultaten van andere zoekmachines samenvoegt, zonder je te volgen.",
"tags": [
"privacy",
"zoeken",
"web"
],
"icon": {
"provider": "selfhst",
"id": "searxng"
},
"version": {
"name": "latest"
}
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "searxng",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Web poort",
"default": 8095,
"required": true
},
{
"name": "data_dir",
"type": "str",
"title": "Data directory",
"default": "/opt/serverup/appdata",
"required": true
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
},
{
"name": "base_url",
"type": "str",
"title": "Basis-URL",
"default": "http://localhost:8095/",
"required": true
},
{
"name": "secret_key",
"type": "str",
"title": "Geheime sleutel",
"default": "",
"required": true,
"description": "Willekeurige waarde; gebruik de dobbelsteenknop",
"config": {
"placeholder": "sterk-wachtwoord"
}
}
]
}
]
}

View file

@ -0,0 +1,11 @@
services:
<< service_name >>:
image: ghcr.io/donkie/spoolman:latest
container_name: << service_name >>
ports:
- "<< port >>:8000"
environment:
- TZ=<< timezone >>
volumes:
- << data_dir >>/<< service_name >>/data:/home/app/.local/share/spoolman
restart: unless-stopped

View file

@ -0,0 +1,53 @@
{
"kind": "compose",
"metadata": {
"name": "Spoolman",
"description": "Houdt bij hoeveel filament je nog hebt en wat elke print verbruikt. Koppelt aan OctoPrint, Klipper en Bambuddy.",
"tags": [
"3d-printen",
"voorraad"
],
"icon": {
"provider": "selfhst",
"id": "spoolman"
},
"version": {
"name": "latest"
}
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "spoolman",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Web poort",
"default": 7912,
"required": true
},
{
"name": "data_dir",
"type": "str",
"title": "Data directory",
"default": "/opt/serverup/appdata",
"required": true
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
}
]
}

View file

@ -0,0 +1,34 @@
services:
<< service_name >>:
image: ghcr.io/tandoorrecipes/recipes:latest
container_name: << service_name >>
ports:
- "<< port >>:8080"
environment:
- TZ=<< timezone >>
- SECRET_KEY=<< secret_key >>
- DB_ENGINE=django.db.backends.postgresql
- POSTGRES_HOST=<< service_name >>-db
- POSTGRES_DB=tandoor
- POSTGRES_USER=tandoor
- POSTGRES_PASSWORD=<< db_password >>
volumes:
- << data_dir >>/<< service_name >>/staticfiles:/opt/recipes/staticfiles
- << data_dir >>/<< service_name >>/mediafiles:/opt/recipes/mediafiles
depends_on:
- << service_name >>-db
restart: unless-stopped
<< service_name >>-db:
image: postgres:16-alpine
container_name: << service_name >>-db
environment:
- POSTGRES_DB=tandoor
- POSTGRES_USER=tandoor
- POSTGRES_PASSWORD=<< db_password >>
volumes:
- << service_name >>_pgdata:/var/lib/postgresql/data
restart: unless-stopped
volumes:
<< service_name >>_pgdata:

View file

@ -0,0 +1,76 @@
{
"kind": "compose",
"metadata": {
"name": "Tandoor Recipes",
"description": "Receptenboek met boodschappenlijst en maaltijdplanner. Importeert recepten van websites.",
"tags": [
"huishouden",
"recepten",
"gezin"
],
"icon": {
"provider": "selfhst",
"id": "tandoor"
},
"version": {
"name": "latest"
}
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "tandoor",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Web poort",
"default": 8092,
"required": true
},
{
"name": "data_dir",
"type": "str",
"title": "Data directory",
"default": "/opt/serverup/appdata",
"required": true
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
},
{
"name": "db_password",
"type": "str",
"title": "Database wachtwoord",
"default": "",
"required": true,
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
"config": {
"placeholder": "sterk-wachtwoord"
}
},
{
"name": "secret_key",
"type": "str",
"title": "Geheime sleutel",
"default": "",
"required": true,
"description": "Willekeurige waarde voor sessieversleuteling",
"config": {
"placeholder": "willekeurige-lange-string"
}
}
]
}
]
}

View file

@ -0,0 +1,16 @@
services:
<< service_name >>:
image: technitium/dns-server:latest
container_name: << service_name >>
ports:
- "<< port >>:5380"
- "53:53/udp"
- "53:53/tcp"
environment:
- TZ=<< timezone >>
- DNS_SERVER_ADMIN_PASSWORD=<< admin_password >>
volumes:
- << data_dir >>/<< service_name >>/config:/etc/dns
sysctls:
- net.ipv4.ip_local_port_range=1024 65000
restart: unless-stopped

View file

@ -0,0 +1,65 @@
{
"kind": "compose",
"metadata": {
"name": "Technitium DNS",
"description": "Eigen DNS-server met advertentieblokkering, DNS-over-HTTPS en gedetailleerde statistieken.",
"tags": [
"netwerk",
"dns",
"privacy"
],
"icon": {
"provider": "selfhst",
"id": "technitium-dns-server"
},
"version": {
"name": "latest"
}
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "technitium-dns",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Web poort",
"default": 5380,
"required": true
},
{
"name": "data_dir",
"type": "str",
"title": "Data directory",
"default": "/opt/serverup/appdata",
"required": true
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
},
{
"name": "admin_password",
"type": "str",
"title": "Beheerderswachtwoord",
"default": "",
"required": true,
"description": "Wachtwoord voor de webinterface",
"config": {
"placeholder": "sterk-wachtwoord"
}
}
]
}
]
}

View file

@ -0,0 +1,34 @@
services:
<< service_name >>:
image: wallabag/wallabag:latest
container_name: << service_name >>
ports:
- "<< port >>:80"
environment:
- TZ=<< timezone >>
- SYMFONY__ENV__DATABASE_DRIVER=pdo_pgsql
- SYMFONY__ENV__DATABASE_HOST=<< service_name >>-db
- SYMFONY__ENV__DATABASE_PORT=5432
- SYMFONY__ENV__DATABASE_NAME=wallabag
- SYMFONY__ENV__DATABASE_USER=wallabag
- SYMFONY__ENV__DATABASE_PASSWORD=<< db_password >>
- SYMFONY__ENV__DOMAIN_NAME=<< app_url >>
volumes:
- << data_dir >>/<< service_name >>/images:/var/www/wallabag/web/assets/images
depends_on:
- << service_name >>-db
restart: unless-stopped
<< service_name >>-db:
image: postgres:16-alpine
container_name: << service_name >>-db
environment:
- POSTGRES_DB=wallabag
- POSTGRES_USER=wallabag
- POSTGRES_PASSWORD=<< db_password >>
volumes:
- << service_name >>_pgdata:/var/lib/postgresql/data
restart: unless-stopped
volumes:
<< service_name >>_pgdata:

View file

@ -0,0 +1,72 @@
{
"kind": "compose",
"metadata": {
"name": "Wallabag",
"description": "Artikelen bewaren om later te lezen, zonder reclame en advertentietrackers. Alternatief voor Pocket.",
"tags": [
"lezen",
"artikelen",
"archief"
],
"icon": {
"provider": "selfhst",
"id": "wallabag"
},
"version": {
"name": "latest"
}
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "wallabag",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Web poort",
"default": 8094,
"required": true
},
{
"name": "data_dir",
"type": "str",
"title": "Data directory",
"default": "/opt/serverup/appdata",
"required": true
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
},
{
"name": "db_password",
"type": "str",
"title": "Database wachtwoord",
"default": "",
"required": true,
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
"config": {
"placeholder": "sterk-wachtwoord"
}
},
{
"name": "app_url",
"type": "str",
"title": "Basis-URL",
"default": "http://localhost:8094",
"required": true
}
]
}
]
}

View file

@ -0,0 +1,13 @@
services:
<< service_name >>:
image: containrrr/watchtower:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
- WATCHTOWER_SCHEDULE=<< schedule >>
- WATCHTOWER_CLEANUP=<< cleanup | lower >>
# Alleen containers met het label com.centurylinklabs.watchtower.enable=true
- WATCHTOWER_LABEL_ENABLE=true
volumes:
- /var/run/docker.sock:/var/run/docker.sock
restart: unless-stopped

View file

@ -0,0 +1,55 @@
{
"kind": "compose",
"metadata": {
"name": "Watchtower",
"description": "Werkt containers automatisch bij zodra er een nieuw image is. Let op: Server Up beheert stacks zelf, gebruik dit alleen voor containers daarbuiten.",
"tags": [
"beheer",
"updates",
"onderhoud"
],
"icon": {
"provider": "selfhst",
"id": "watchtower"
},
"version": {
"name": "latest"
}
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "watchtower",
"required": true
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
},
{
"name": "schedule",
"type": "str",
"title": "Planning (cron)",
"default": "0 0 4 * * *",
"required": true,
"description": "Zes velden, seconden eerst. Standaard elke nacht om 4 uur."
},
{
"name": "cleanup",
"type": "bool",
"title": "Oude images opruimen",
"default": true,
"description": "Verwijdert het vorige image na een update"
}
]
}
]
}

View file

@ -0,0 +1,21 @@
services:
<< service_name >>:
image: ghcr.io/wg-easy/wg-easy:latest
container_name: << service_name >>
ports:
- "<< vpn_port >>:51820/udp"
- "<< port >>:51821/tcp"
environment:
- TZ=<< timezone >>
- WG_HOST=<< host >>
- PASSWORD_HASH=<< password_hash >>
- WG_PORT=<< vpn_port >>
volumes:
- << data_dir >>/<< service_name >>/config:/etc/wireguard
cap_add:
- NET_ADMIN
- SYS_MODULE
sysctls:
- net.ipv4.ip_forward=1
- net.ipv4.conf.all.src_valid_mark=1
restart: unless-stopped

View file

@ -0,0 +1,80 @@
{
"kind": "compose",
"metadata": {
"name": "WireGuard Easy",
"description": "VPN naar je eigen netwerk, met een webinterface om apparaten toe te voegen via een QR-code.",
"tags": [
"vpn",
"netwerk",
"beveiliging"
],
"icon": {
"provider": "selfhst",
"id": "wg-easy"
},
"version": {
"name": "latest"
}
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "wg-easy",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Web poort",
"default": 51821,
"required": true
},
{
"name": "data_dir",
"type": "str",
"title": "Data directory",
"default": "/opt/serverup/appdata",
"required": true
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
},
{
"name": "vpn_port",
"type": "int",
"title": "VPN poort (UDP)",
"default": 51820,
"required": true
},
{
"name": "host",
"type": "str",
"title": "Publiek adres",
"default": "vpn.example.com",
"required": true,
"description": "Het adres of IP waarop je van buitenaf binnenkomt"
},
{
"name": "password_hash",
"type": "str",
"title": "Wachtwoordhash beheerder",
"default": "",
"required": true,
"description": "Bcrypt-hash van je wachtwoord; zie de documentatie van wg-easy",
"config": {
"placeholder": "sterk-wachtwoord"
}
}
]
}
]
}

View file

@ -0,0 +1,15 @@
services:
<< service_name >>:
image: ghcr.io/koenkk/zigbee2mqtt:latest
container_name: << service_name >>
ports:
- "<< port >>:8080"
environment:
- TZ=<< timezone >>
- ZIGBEE2MQTT_CONFIG_MQTT_SERVER=<< mqtt_server >>
volumes:
- << data_dir >>/<< service_name >>/data:/app/data
- /run/udev:/run/udev:ro
devices:
- << usb_device >>:/dev/ttyACM0
restart: unless-stopped

View file

@ -0,0 +1,70 @@
{
"kind": "compose",
"metadata": {
"name": "Zigbee2MQTT",
"description": "Koppelt Zigbee-apparaten aan MQTT, zonder de hub van de fabrikant. Werkt met Home Assistant.",
"tags": [
"domotica",
"zigbee",
"mqtt"
],
"icon": {
"provider": "selfhst",
"id": "zigbee2mqtt"
},
"version": {
"name": "latest"
}
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "zigbee2mqtt",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Web poort",
"default": 8088,
"required": true
},
{
"name": "data_dir",
"type": "str",
"title": "Data directory",
"default": "/opt/serverup/appdata",
"required": true
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
},
{
"name": "usb_device",
"type": "str",
"title": "USB-adapter",
"default": "/dev/ttyACM0",
"required": true,
"description": "Pad naar je Zigbee-stick; vind hem met `ls -l /dev/serial/by-id/`"
},
{
"name": "mqtt_server",
"type": "str",
"title": "MQTT-server",
"default": "mqtt://mosquitto:1883",
"required": true,
"description": "Adres van je MQTT-broker (zie de Mosquitto-app)"
}
]
}
]
}

View file

@ -0,0 +1,14 @@
services:
<< service_name >>:
image: zwavejs/zwave-js-ui:latest
container_name: << service_name >>
ports:
- "<< port >>:8091"
- "<< ws_port >>:3000"
environment:
- TZ=<< timezone >>
volumes:
- << data_dir >>/<< service_name >>/store:/usr/src/app/store
devices:
- << usb_device >>:/dev/zwave
restart: unless-stopped

View file

@ -0,0 +1,70 @@
{
"kind": "compose",
"metadata": {
"name": "Z-Wave JS UI",
"description": "Beheer je Z-Wave-netwerk en koppel het aan Home Assistant of MQTT.",
"tags": [
"domotica",
"zwave",
"mqtt"
],
"icon": {
"provider": "selfhst",
"id": "zwave-js-ui"
},
"version": {
"name": "latest"
}
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "zwave-js-ui",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Web poort",
"default": 8091,
"required": true
},
{
"name": "data_dir",
"type": "str",
"title": "Data directory",
"default": "/opt/serverup/appdata",
"required": true
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
},
{
"name": "usb_device",
"type": "str",
"title": "USB-adapter",
"default": "/dev/ttyUSB0",
"required": true,
"description": "Pad naar je Z-Wave-stick"
},
{
"name": "ws_port",
"type": "int",
"title": "WebSocket poort",
"default": 3003,
"required": true,
"description": "Poort waarop Home Assistant verbinding maakt"
}
]
}
]
}

View file

@ -257,6 +257,21 @@ def _tidy(text: str) -> str:
_KEY_RE = re.compile(r"^(\s*)([A-Za-z_][\w\-]*)\s*:\s*$") _KEY_RE = re.compile(r"^(\s*)([A-Za-z_][\w\-]*)\s*:\s*$")
# Alleen deze compose-sleutels worden opgeruimd als ze leeg achterblijven. Dat
# zijn de sleutels die een `<% if %>`-blok leeg kan maken.
#
# De beperking is wezenlijk: een named volume wordt gedeclareerd als een kale
# sleutel zonder waarde (` immich_pgdata:`), en zonder deze lijst werd die als
# "lege mapping" opgeruimd. Daarna viel ook het bovenliggende `volumes:` weg,
# en verwees de compose naar een volume dat nergens meer gedeclareerd stond —
# waarop docker compose weigert te starten.
_OPRUIMBARE_SLEUTELS = frozenset({
"ports", "environment", "volumes", "depends_on", "networks", "labels",
"devices", "cap_add", "cap_drop", "sysctls", "extra_hosts", "dns",
"expose", "profiles", "tmpfs", "secrets", "configs", "command",
"entrypoint", "healthcheck", "deploy", "links", "env_file", "build",
})
def _drop_empty_mappings(text: str) -> str: def _drop_empty_mappings(text: str) -> str:
"""Verwijder ´keys´ die op een lege regel of een gelijk/minder-indent sibling """Verwijder ´keys´ die op een lege regel of een gelijk/minder-indent sibling
@ -269,6 +284,8 @@ def _drop_empty_mappings(text: str) -> str:
m = _KEY_RE.match(line) m = _KEY_RE.match(line)
if not m: if not m:
continue continue
if m.group(2) not in _OPRUIMBARE_SLEUTELS:
continue
indent = len(m.group(1)) indent = len(m.group(1))
# Zoek de eerste niet-lege, niet-commentaar regel hierna # Zoek de eerste niet-lege, niet-commentaar regel hierna
j = i + 1 j = i + 1

122
tests/test_apps.py Normal file
View file

@ -0,0 +1,122 @@
"""Elke app in apps/ moet renderen naar geldige, startbare compose-YAML.
Deze suite draait over de echte templates, niet over voorbeelden. Zo valt een
kapot template op vóórdat iemand het probeert te installeren.
"""
import sys
import tempfile
from pathlib import Path
import pytest
import yaml
WORTEL = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(WORTEL / "server-up"))
from core import boilerplates as bp
APPS = sorted(d for d in (WORTEL / "apps").iterdir() if d.is_dir())
NAMEN = [d.name for d in APPS]
def _render(d: Path, tmp: Path) -> dict:
velden = bp.fields(d)
waarden = {f["name"]: f["default"] for f in velden if f.get("default") is not None}
waarden.setdefault("service_name", d.name)
# Verplichte velden zonder default zijn geheimen; die vult de gebruiker in.
for f in velden:
if f.get("required") and not waarden.get(f["name"]):
waarden[f["name"]] = "testwaarde12345678901234567890ab"
bp.render_to_dir(d, tmp, waarden)
return yaml.safe_load((tmp / "docker-compose.yml").read_text())
def test_er_zijn_apps():
assert len(APPS) >= 60
@pytest.mark.parametrize("app", APPS, ids=NAMEN)
def test_app_is_geldige_boilerplate(app):
assert bp.is_boilerplate(app), "template.json of files/ ontbreekt"
meta = bp.metadata(app)
assert meta["name"], "naam ontbreekt in template.json"
assert meta["description"], "omschrijving ontbreekt"
assert meta["tags"], "tags ontbreken"
assert any(f["name"] == "service_name" for f in bp.fields(app)), \
"service_name-variabele ontbreekt"
@pytest.mark.parametrize("app", APPS, ids=NAMEN)
def test_app_rendert_naar_geldige_compose(app):
with tempfile.TemporaryDirectory() as t:
doc = _render(app, Path(t))
assert isinstance(doc, dict) and doc.get("services"), "geen services"
for naam, svc in doc["services"].items():
assert isinstance(svc, dict), f"{naam} is geen mapping"
assert svc.get("image") or svc.get("build"), f"{naam}: geen image of build"
@pytest.mark.parametrize("app", APPS, ids=NAMEN)
def test_named_volumes_zijn_gedeclareerd(app):
"""Named volumes moeten in het top-level volumes-blok staan.
_drop_empty_mappings ruimde dat blok weg omdat een volumedeclaratie een
kale sleutel zonder waarde is. Zes apps (immich, ghost, miniflux,
paperless-ngx, unifi-network, vikunja) renderden daardoor naar een compose
die docker weigert met "refers to undefined volume".
"""
with tempfile.TemporaryDirectory() as t:
doc = _render(app, Path(t))
gedeclareerd = set((doc.get("volumes") or {}).keys())
for naam, svc in doc["services"].items():
for v in svc.get("volumes") or []:
if isinstance(v, str) and not v.startswith(("/", ".")):
bron = v.split(":")[0]
assert bron in gedeclareerd, \
f"{naam}: volume '{bron}' wordt gebruikt maar niet gedeclareerd"
@pytest.mark.parametrize("app", APPS, ids=NAMEN)
def test_geen_onvervangen_variabelen(app):
"""Een typefout in een variabelenaam laat << … >> in het resultaat staan."""
with tempfile.TemporaryDirectory() as t:
tmp = Path(t)
_render(app, tmp)
tekst = (tmp / "docker-compose.yml").read_text()
assert "<<" not in tekst and ">>" not in tekst, "onvervangen variabele"
# Poorten die meerdere apps bewust delen: alternatieven voor dezelfde taak,
# die je nooit tegelijk draait. Alles daarbuiten hoort uniek te zijn.
GEDEELDE_POORTEN = {
80: {"nginx-proxy-manager", "traefik"}, # reverse proxies
443: {"nginx-proxy-manager", "traefik"},
53: {"adguard-home", "pihole"}, # dns-blokkers
5055: {"arr-stack", "jellyseerr"}, # zelfde app
# Hieronder: botsingen die er al waren vóór deze testsuite. De
# vrije-poortsuggestie bij installeren vangt ze op, dus ze zijn hinderlijk
# maar niet kapot. Nieuwe apps horen hier niet bij te komen.
3000: {"adguard-home", "forgejo", "grafana"},
8080: {"miniflux", "nextcloud-aio", "pihole", "qbittorrent",
"stirling-pdf", "traefik", "unifi-network"},
9000: {"mealie", "portainer"},
}
def test_poorten_botsen_niet_tussen_apps():
"""Twee apps met dezelfde standaardpoort geven bij installeren meteen een
conflict. De vrije-poortsuggestie vangt dat op, maar standaardwaarden die
al botsen zijn onnodig verwarrend."""
per_poort: dict[int, list[str]] = {}
for app in APPS:
for f in bp.fields(app):
if f.get("type") == "int" and "port" in f["name"].lower():
d = f.get("default")
if isinstance(d, int):
per_poort.setdefault(d, []).append(f"{app.name}:{f['name']}")
botsingen = {}
for poort, gebruikers in per_poort.items():
apps = {g.split(":")[0] for g in gebruikers}
if len(apps) > 1 and not apps <= GEDEELDE_POORTEN.get(poort, set()):
botsingen[poort] = sorted(apps)
assert not botsingen, f"onbedoeld dubbele standaardpoorten: {botsingen}"

View file

@ -72,9 +72,11 @@ def test_sandbox_geeft_geen_python_internals_prijs(tmp_path):
src = _maak_boilerplate(tmp_path, "x: << service_name.__class__ >>\n") src = _maak_boilerplate(tmp_path, "x: << service_name.__class__ >>\n")
bp.render_to_dir(src, tmp_path / "out", {"service_name": "web"}) bp.render_to_dir(src, tmp_path / "out", {"service_name": "web"})
tekst = (tmp_path / "out" / "docker-compose.yml").read_text() tekst = (tmp_path / "out" / "docker-compose.yml").read_text()
# `x:` blijft leeg achter en wordt daarna door _drop_empty_mappings opgeruimd. # De waarde blijft leeg; `x:` zelf blijft staan omdat alleen bekende
assert tekst.strip() == "" # compose-sleutels worden opgeruimd (zie _OPRUIMBARE_SLEUTELS).
assert tekst.strip() == "x:"
assert "class" not in tekst assert "class" not in tekst
assert "object" not in tekst
def test_sandbox_laat_aanroep_van_internals_falen(tmp_path): def test_sandbox_laat_aanroep_van_internals_falen(tmp_path):