v0.8.10-beta - Vervangers voor big tech, en sleutels in het juiste formaat
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 2m31s

- Sjablonen kunnen nu secret_format ("hex" of "laravel") en secret_bytes
  opgeven. De generator maakte altijd url-veilige base64, en dat is precies wat
  Laravel, Homarr en LibreChat weigeren
- Daarmee kwamen twee fouten uit v0.8.00 aan het licht: Homarr en LibreChat
  kregen sleutels die hun containers niet accepteren. Rechtgezet
- Snipe-IT en Invoice Ninja alsnog toegevoegd; die vielen vorige ronde af omdat
  hun APP_KEY niet te genereren was
- 19 vervangers voor big tech erbij, van 197 naar 218 apps:
  Baikal en Radicale (Google Agenda/Contacten), LibreTranslate (Translate),
  CryptPad, Collabora en ONLYOFFICE (Google Docs), Formbricks (Forms),
  Jitsi Meet (Meet/Zoom), PeerTube, Invidious en Tube Archivist (YouTube),
  Owncast (Twitch), Mastodon (X), Pixelfed (Instagram), Lemmy (Reddit),
  Cal.com (Calendly), Rallly (Doodle), DocuSeal (DocuSign), Penpot (Figma)
- Communicatie van 12 naar 16 apps, productiviteit van 21 naar 31
- Nieuwe JS-test voor de sleutelformaten; alle 242 images in de catalogus
  opnieuw tegen hun registry gecontroleerd

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q9eqpADJSRs49SoGGr4NAy
This commit is contained in:
Ramon 2026-08-01 03:49:10 +02:00
parent 7d178a68b0
commit c9b73d8b69
55 changed files with 2738 additions and 9 deletions

View file

@ -1,3 +1,46 @@
# v0.8.10-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.8.00-beta — 55 apps erbij, en drie kapotte images gevonden # v0.8.00-beta — 55 apps erbij, en drie kapotte images gevonden
**De catalogus stond scheef.** Na het ARR-werk telde media 42 apps en downloaden **De catalogus stond scheef.** Na het ARR-werk telde media 42 apps en downloaden

View file

@ -16,7 +16,7 @@ IP-adres in je netwerk geven.
| | | | | |
|---|---| |---|---|
| **App-catalogus** | 197 kant-en-klare apps, ingedeeld in categorieën met kleur. Zoeken, filteren en installeren met een formulier in plaats van YAML. | | **App-catalogus** | 218 kant-en-klare apps, ingedeeld in categorieën met kleur. Zoeken, filteren en installeren met een formulier in plaats van YAML. |
| **Stacks beheren** | Starten, stoppen, bijwerken en herstarten — per stack of per losse container, met logs en live CPU-/geheugengebruik. | | **Stacks beheren** | Starten, stoppen, bijwerken en herstarten — per stack of per losse container, met logs en live CPU-/geheugengebruik. |
| **Apps koppelen** | Stacks komen op een gedeeld netwerk, zodat ze elkaar op naam bereiken. Een veld dat naar een andere app verwijst toont een keuzelijst met wat je al draait. | | **Apps koppelen** | Stacks komen op een gedeeld netwerk, zodat ze elkaar op naam bereiken. Een veld dat naar een andere app verwijst toont een keuzelijst met wat je al draait. |
| **Eigen IP per stack** | Met macvlan of ipvlan krijgt een app een eigen adres in je LAN in plaats van poorten op de host. | | **Eigen IP per stack** | Met macvlan of ipvlan krijgt een app een eigen adres in je LAN in plaats van poorten op de host. |

View file

@ -1 +1 @@
0.8.00-beta 0.8.10-beta

View file

@ -1086,6 +1086,8 @@
"required": true, "required": true,
"secret": true, "secret": true,
"generate": "auto", "generate": "auto",
"secret_format": "hex",
"secret_bytes": 32,
"description": "64 tekens; versleutelt de opgeslagen API-sleutels van je apps" "description": "64 tekens; versleutelt de opgeslagen API-sleutels van je apps"
} }
] ]

View file

@ -0,0 +1,12 @@
services:
<< service_name >>:
image: ckulka/baikal:nginx
container_name: << service_name >>
environment:
- TZ=<< timezone >>
volumes:
- << appdata_dir >>/<< service_name >>:/var/www/baikal/config
- << appdata_dir >>/<< service_name >>-specific:/var/www/baikal/Specific
ports:
- "<< port >>:80"
restart: unless-stopped

61
apps/baikal/template.json Normal file
View file

@ -0,0 +1,61 @@
{
"kind": "compose",
"metadata": {
"name": "Baïkal",
"description": "Je agenda en contacten op je eigen server, via CalDAV en CardDAV. iPhone, Android en Thunderbird praten er zo mee. Vervangt Google Agenda en Google Contacten.",
"tags": [
"agenda",
"contacten",
"caldav",
"carddav"
],
"icon": {
"provider": "selfhst",
"id": "baikal"
},
"version": {
"name": "latest"
},
"categories": [
"gezin",
"productiviteit"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "baikal",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8180,
"required": true,
"description": "Standaard 80; hier verplaatst omdat die poort al bezet is"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
}
]
}

View file

@ -0,0 +1,26 @@
services:
<< service_name >>:
image: calcom/cal.com:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
- DATABASE_URL=postgresql://calcom:<< db_password >>@<< service_name >>-db:5432/calcom
- NEXTAUTH_SECRET=<< nextauth_secret >>
- CALENDSO_ENCRYPTION_KEY=<< encryption_key >>
- NEXT_PUBLIC_WEBAPP_URL=<< app_url >>
- NEXTAUTH_URL=<< app_url >>/api/auth
ports:
- "<< port >>:3000"
depends_on:
- << service_name >>-db
restart: unless-stopped
<< service_name >>-db:
image: postgres:16-alpine
container_name: << service_name >>-db
environment:
- POSTGRES_DB=calcom
- POSTGRES_USER=calcom
- POSTGRES_PASSWORD=<< db_password >>
volumes:
- << appdata_dir >>/<< service_name >>-db:/var/lib/postgresql/data
restart: unless-stopped

107
apps/cal-com/template.json Normal file
View file

@ -0,0 +1,107 @@
{
"kind": "compose",
"metadata": {
"name": "Cal.com",
"description": "Laat anderen een afspraak in je agenda prikken, zonder heen-en-weer mailen over wanneer het uitkomt. Vervangt Calendly.",
"tags": [
"agenda",
"afspraken",
"planning"
],
"icon": {
"provider": "selfhst",
"id": "cal-com"
},
"version": {
"name": "latest"
},
"categories": [
"productiviteit",
"zakelijk"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "calcom",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8194,
"required": true,
"description": "Standaard 3000; hier verplaatst omdat die poort al bezet is"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
},
{
"title": "Instellingen",
"items": [
{
"name": "app_url",
"type": "str",
"title": "Publiek adres",
"default": "http://localhost:8194",
"required": true
}
]
},
{
"title": "Toegang",
"items": [
{
"name": "db_password",
"type": "str",
"title": "Databasewachtwoord",
"default": "",
"required": true,
"secret": true,
"generate": "auto",
"description": "Wordt automatisch ingevuld en in .env bewaard"
},
{
"name": "nextauth_secret",
"type": "str",
"title": "Sleutel voor de inlogsessies",
"default": "",
"required": true,
"secret": true,
"generate": "auto"
},
{
"name": "encryption_key",
"type": "str",
"title": "Sleutel voor gekoppelde agenda's",
"default": "",
"required": true,
"secret": true,
"generate": "auto",
"secret_format": "hex",
"secret_bytes": 16
}
]
}
]
}

View file

@ -0,0 +1,15 @@
services:
<< service_name >>:
image: collabora/code:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
- aliasgroup1=<< nextcloud_url >>
- extra_params=--o:ssl.enable=false --o:ssl.termination=true
- username=<< admin_user >>
- password=<< admin_password >>
ports:
- "<< port >>:9980"
cap_add:
- MKNOD
restart: unless-stopped

View file

@ -0,0 +1,91 @@
{
"kind": "compose",
"metadata": {
"name": "Collabora Online",
"description": "Documenten, spreadsheets en presentaties bewerken in je browser, samen met anderen. Koppelt aan Nextcloud. Vervangt Google Docs en Microsoft Office.",
"tags": [
"documenten",
"kantoor",
"samenwerken"
],
"icon": {
"provider": "selfhst",
"id": "collabora"
},
"version": {
"name": "latest"
},
"categories": [
"documenten",
"productiviteit"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "collabora",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8201,
"required": true,
"description": "Standaard 9980; hier verplaatst omdat die poort al bezet is"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
},
{
"title": "Instellingen",
"items": [
{
"name": "nextcloud_url",
"type": "str",
"title": "Adres van Nextcloud",
"default": "https://cloud.example.com",
"required": true,
"connect": {
"app": "nextcloud-aio",
"scheme": "https",
"port": 443
},
"help": "Collabora accepteert alleen verzoeken van dit adres. Klopt het niet, dan blijft de bewerker leeg."
},
{
"name": "admin_user",
"type": "str",
"title": "Beheerdersnaam",
"default": "admin",
"required": true
}
]
},
{
"title": "Toegang",
"items": [
{
"name": "admin_password",
"type": "str",
"title": "Beheerderswachtwoord",
"default": "",
"required": true,
"secret": true,
"eigen_invoer": true
}
]
}
]
}

View file

@ -0,0 +1,15 @@
services:
<< service_name >>:
image: cryptpad/cryptpad:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
- CPAD_MAIN_DOMAIN=<< hoofddomein >>
- CPAD_SANDBOX_DOMAIN=<< sandbox_domein >>
volumes:
- << appdata_dir >>/<< service_name >>:/cryptpad/data
- << appdata_dir >>/<< service_name >>-blob:/cryptpad/blob
- << appdata_dir >>/<< service_name >>-block:/cryptpad/block
ports:
- "<< port >>:3000"
restart: unless-stopped

View file

@ -0,0 +1,82 @@
{
"kind": "compose",
"metadata": {
"name": "CryptPad",
"description": "Documenten, spreadsheets en presentaties die in je browser versleuteld worden. De server ziet alleen onleesbare blokken. Vervangt Google Docs.",
"tags": [
"documenten",
"samenwerken",
"privacy",
"versleuteling"
],
"icon": {
"provider": "selfhst",
"id": "cryptpad"
},
"version": {
"name": "latest"
},
"categories": [
"documenten",
"productiviteit"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "cryptpad",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8190,
"required": true,
"description": "Standaard 3000; hier verplaatst omdat die poort al bezet is"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
},
{
"title": "Instellingen",
"items": [
{
"name": "hoofddomein",
"type": "str",
"title": "Hoofddomein",
"default": "https://pad.example.com",
"required": true
},
{
"name": "sandbox_domein",
"type": "str",
"title": "Sandbox-domein",
"default": "https://pad-sandbox.example.com",
"required": true,
"description": "Moet een ánder domein zijn dan het hoofddomein",
"help": "CryptPad zet de documentbewerker in een aparte oorsprong, zodat een lek in de bewerker niet bij je sleutels kan. Twee keer hetzelfde domein invullen haalt die scheiding weg."
}
]
}
]
}

View file

@ -0,0 +1,26 @@
services:
<< service_name >>:
image: docuseal/docuseal:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
- DATABASE_URL=postgresql://docuseal:<< db_password >>@<< service_name >>-db:5432/docuseal
- SECRET_KEY_BASE=<< secret_key_base >>
- FORCE_SSL=false
volumes:
- << appdata_dir >>/<< service_name >>:/data
ports:
- "<< port >>:3000"
depends_on:
- << service_name >>-db
restart: unless-stopped
<< service_name >>-db:
image: postgres:16-alpine
container_name: << service_name >>-db
environment:
- POSTGRES_DB=docuseal
- POSTGRES_USER=docuseal
- POSTGRES_PASSWORD=<< db_password >>
volumes:
- << appdata_dir >>/<< service_name >>-db:/var/lib/postgresql/data
restart: unless-stopped

View file

@ -0,0 +1,84 @@
{
"kind": "compose",
"metadata": {
"name": "DocuSeal",
"description": "Documenten laten ondertekenen met een digitale handtekening, inclusief invulvelden en een controlespoor. Vervangt DocuSign.",
"tags": [
"documenten",
"handtekening",
"formulieren"
],
"icon": {
"provider": "selfhst",
"id": "docuseal"
},
"version": {
"name": "latest"
},
"categories": [
"documenten",
"zakelijk"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "docuseal",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8196,
"required": true,
"description": "Standaard 3000; hier verplaatst omdat die poort al bezet is"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
},
{
"title": "Toegang",
"items": [
{
"name": "db_password",
"type": "str",
"title": "Databasewachtwoord",
"default": "",
"required": true,
"secret": true,
"generate": "auto",
"description": "Wordt automatisch ingevuld en in .env bewaard"
},
{
"name": "secret_key_base",
"type": "str",
"title": "Rails-sleutel",
"default": "",
"required": true,
"secret": true,
"generate": "auto"
}
]
}
]
}

View file

@ -0,0 +1,26 @@
services:
<< service_name >>:
image: formbricks/formbricks:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
- DATABASE_URL=postgresql://formbricks:<< db_password >>@<< service_name >>-db:5432/formbricks
- NEXTAUTH_SECRET=<< nextauth_secret >>
- ENCRYPTION_KEY=<< encryption_key >>
- WEBAPP_URL=<< app_url >>
- NEXTAUTH_URL=<< app_url >>
ports:
- "<< port >>:3000"
depends_on:
- << service_name >>-db
restart: unless-stopped
<< service_name >>-db:
image: postgres:16-alpine
container_name: << service_name >>-db
environment:
- POSTGRES_DB=formbricks
- POSTGRES_USER=formbricks
- POSTGRES_PASSWORD=<< db_password >>
volumes:
- << appdata_dir >>/<< service_name >>-db:/var/lib/postgresql/data
restart: unless-stopped

View file

@ -0,0 +1,107 @@
{
"kind": "compose",
"metadata": {
"name": "Formbricks",
"description": "Enquêtes en formulieren maken en de antwoorden zelf houden, met logica per vraag. Vervangt Google Forms.",
"tags": [
"formulieren",
"enquetes",
"onderzoek"
],
"icon": {
"provider": "selfhst",
"id": "formbricks"
},
"version": {
"name": "latest"
},
"categories": [
"productiviteit",
"zakelijk"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "formbricks",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8197,
"required": true,
"description": "Standaard 3000; hier verplaatst omdat die poort al bezet is"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
},
{
"title": "Instellingen",
"items": [
{
"name": "app_url",
"type": "str",
"title": "Publiek adres",
"default": "http://localhost:8197",
"required": true
}
]
},
{
"title": "Toegang",
"items": [
{
"name": "db_password",
"type": "str",
"title": "Databasewachtwoord",
"default": "",
"required": true,
"secret": true,
"generate": "auto",
"description": "Wordt automatisch ingevuld en in .env bewaard"
},
{
"name": "nextauth_secret",
"type": "str",
"title": "Sleutel voor de inlogsessies",
"default": "",
"required": true,
"secret": true,
"generate": "auto"
},
{
"name": "encryption_key",
"type": "str",
"title": "Sleutel voor de antwoorden",
"default": "",
"required": true,
"secret": true,
"generate": "auto",
"secret_format": "hex",
"secret_bytes": 32
}
]
}
]
}

View file

@ -64,6 +64,8 @@
"required": true, "required": true,
"secret": true, "secret": true,
"generate": "auto", "generate": "auto",
"secret_format": "hex",
"secret_bytes": 32,
"description": "64 tekens; versleutelt de opgeslagen API-sleutels van je apps" "description": "64 tekens; versleutelt de opgeslagen API-sleutels van je apps"
} }
] ]

View file

@ -0,0 +1,23 @@
services:
<< service_name >>:
image: quay.io/invidious/invidious:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
volumes:
- ./config.yml:/invidious/config/config.yml:ro
ports:
- "<< port >>:3000"
depends_on:
- << service_name >>-db
restart: unless-stopped
<< service_name >>-db:
image: postgres:16-alpine
container_name: << service_name >>-db
environment:
- POSTGRES_DB=invidious
- POSTGRES_USER=invidious
- POSTGRES_PASSWORD=<< db_password >>
volumes:
- << appdata_dir >>/<< service_name >>-db:/var/lib/postgresql/data
restart: unless-stopped

View file

@ -0,0 +1,8 @@
db:
dbname: invidious
user: invidious
password: << db_password >>
host: << service_name >>-db
port: 5432
check_tables: true
hmac_key: << hmac_key >>

View file

@ -0,0 +1,83 @@
{
"kind": "compose",
"metadata": {
"name": "Invidious",
"description": "Kijkt YouTube zonder reclame, zonder account en zonder dat Google meekijkt. Abonnementen blijven op je eigen server. Vervangt YouTube.",
"tags": [
"video",
"youtube",
"privacy"
],
"icon": {
"provider": "selfhst",
"id": "invidious"
},
"version": {
"name": "latest"
},
"categories": [
"media"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "invidious",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8186,
"required": true,
"description": "Standaard 3000; hier verplaatst omdat die poort al bezet is"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
},
{
"title": "Toegang",
"items": [
{
"name": "db_password",
"type": "str",
"title": "Databasewachtwoord",
"default": "",
"required": true,
"secret": true,
"generate": "auto",
"description": "Wordt automatisch ingevuld en in .env bewaard"
},
{
"name": "hmac_key",
"type": "str",
"title": "HMAC-sleutel",
"default": "",
"required": true,
"secret": true,
"generate": "auto"
}
]
}
]
}

View file

@ -0,0 +1,36 @@
services:
<< service_name >>:
image: invoiceninja/invoiceninja:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
- APP_KEY=<< app_key >>
- APP_URL=<< app_url >>
- APP_ENV=production
- APP_DEBUG=false
- DB_HOST=<< service_name >>-db
- DB_PORT=3306
- DB_DATABASE=ninja
- DB_USERNAME=ninja
- DB_PASSWORD=<< db_password >>
- IN_USER_EMAIL=<< admin_email >>
- IN_PASSWORD=<< admin_password >>
volumes:
- << appdata_dir >>/<< service_name >>:/var/www/app/public
- << appdata_dir >>/<< service_name >>-storage:/var/www/app/storage
ports:
- "<< port >>:80"
depends_on:
- << service_name >>-db
restart: unless-stopped
<< service_name >>-db:
image: mariadb:11
container_name: << service_name >>-db
environment:
- MARIADB_DATABASE=ninja
- MARIADB_USER=ninja
- MARIADB_PASSWORD=<< db_password >>
- MARIADB_ROOT_PASSWORD=<< db_password >>
volumes:
- << appdata_dir >>/<< service_name >>-db:/var/lib/mysql
restart: unless-stopped

View file

@ -0,0 +1,115 @@
{
"kind": "compose",
"metadata": {
"name": "Invoice Ninja",
"description": "Facturen, offertes en urenregistratie voor zzp'ers en kleine bedrijven, met online betalen en herinneringen.",
"tags": [
"facturen",
"boekhouding",
"zzp"
],
"icon": {
"provider": "selfhst",
"id": "invoiceninja"
},
"version": {
"name": "latest"
},
"categories": [
"zakelijk",
"financien"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "invoiceninja",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8179,
"required": true,
"description": "Standaard 80; hier verplaatst omdat die poort al bezet is"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
},
{
"title": "Instellingen",
"items": [
{
"name": "app_url",
"type": "str",
"title": "Publiek adres",
"default": "http://localhost:8179",
"required": true
},
{
"name": "admin_email",
"type": "str",
"title": "E-mailadres beheerder",
"default": "admin@example.com",
"required": true
}
]
},
{
"title": "Toegang",
"items": [
{
"name": "db_password",
"type": "str",
"title": "Databasewachtwoord",
"default": "",
"required": true,
"secret": true,
"generate": "auto",
"description": "Wordt automatisch ingevuld en in .env bewaard"
},
{
"name": "app_key",
"type": "str",
"title": "Applicatiesleutel",
"default": "",
"required": true,
"secret": true,
"generate": "auto",
"secret_format": "laravel",
"secret_bytes": 32,
"description": "Laravel-formaat; wordt automatisch in de juiste vorm gemaakt"
},
{
"name": "admin_password",
"type": "str",
"title": "Beheerderswachtwoord",
"default": "",
"required": true,
"secret": true,
"eigen_invoer": true
}
]
}
]
}

View file

@ -0,0 +1,69 @@
services:
<< service_name >>:
image: jitsi/web:stable
container_name: << service_name >>
environment:
- TZ=<< timezone >>
- ENABLE_AUTH=0
- ENABLE_GUESTS=1
- PUBLIC_URL=<< public_url >>
- XMPP_SERVER=<< service_name >>-prosody
- XMPP_DOMAIN=meet.jitsi
- XMPP_AUTH_DOMAIN=auth.meet.jitsi
- XMPP_MUC_DOMAIN=muc.meet.jitsi
- XMPP_BOSH_URL_BASE=http://<< service_name >>-prosody:5280
volumes:
- << appdata_dir >>/<< service_name >>:/config
ports:
- "<< port >>:80"
depends_on:
- << service_name >>-prosody
restart: unless-stopped
<< service_name >>-prosody:
image: jitsi/prosody:stable
container_name: << service_name >>-prosody
environment:
- XMPP_DOMAIN=meet.jitsi
- XMPP_AUTH_DOMAIN=auth.meet.jitsi
- XMPP_MUC_DOMAIN=muc.meet.jitsi
- XMPP_INTERNAL_MUC_DOMAIN=internal-muc.meet.jitsi
- JICOFO_AUTH_USER=focus
- JICOFO_AUTH_PASSWORD=<< jicofo_password >>
- JVB_AUTH_USER=jvb
- JVB_AUTH_PASSWORD=<< jvb_password >>
volumes:
- << appdata_dir >>/<< service_name >>-prosody:/config
restart: unless-stopped
<< service_name >>-jicofo:
image: jitsi/jicofo:stable
container_name: << service_name >>-jicofo
environment:
- XMPP_SERVER=<< service_name >>-prosody
- XMPP_DOMAIN=meet.jitsi
- XMPP_AUTH_DOMAIN=auth.meet.jitsi
- XMPP_INTERNAL_MUC_DOMAIN=internal-muc.meet.jitsi
- JICOFO_AUTH_USER=focus
- JICOFO_AUTH_PASSWORD=<< jicofo_password >>
volumes:
- << appdata_dir >>/<< service_name >>-jicofo:/config
depends_on:
- << service_name >>-prosody
restart: unless-stopped
<< service_name >>-jvb:
image: jitsi/jvb:stable
container_name: << service_name >>-jvb
environment:
- XMPP_SERVER=<< service_name >>-prosody
- XMPP_AUTH_DOMAIN=auth.meet.jitsi
- XMPP_INTERNAL_MUC_DOMAIN=internal-muc.meet.jitsi
- JVB_AUTH_USER=jvb
- JVB_AUTH_PASSWORD=<< jvb_password >>
- JVB_PORT=<< port_media >>
- PUBLIC_URL=<< public_url >>
ports:
- "<< port_media >>:<< port_media >>/udp"
volumes:
- << appdata_dir >>/<< service_name >>-jvb:/config
depends_on:
- << service_name >>-prosody
restart: unless-stopped

102
apps/jitsi/template.json Normal file
View file

@ -0,0 +1,102 @@
{
"kind": "compose",
"metadata": {
"name": "Jitsi Meet",
"description": "Videovergaderen in de browser, zonder account en zonder installatie. Deel een link en je zit erin. Vervangt Google Meet en Zoom.",
"tags": [
"video",
"vergaderen",
"communicatie"
],
"icon": {
"provider": "selfhst",
"id": "jitsi"
},
"version": {
"name": "latest"
},
"categories": [
"communicatie"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "jitsi",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8193,
"required": true,
"description": "Standaard 80; hier verplaatst omdat die poort al bezet is"
},
{
"name": "port_media",
"type": "int",
"title": "Mediapoort (UDP)",
"default": 10000,
"required": true,
"description": "Hier loopt het beeld en geluid; zet deze open in je router"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
},
{
"title": "Instellingen",
"items": [
{
"name": "public_url",
"type": "str",
"title": "Publiek adres",
"default": "https://meet.example.com",
"required": true
}
]
},
{
"title": "Toegang",
"items": [
{
"name": "jicofo_password",
"type": "str",
"title": "Wachtwoord voor de conferentiebeheerder",
"default": "",
"required": true,
"secret": true,
"generate": "auto"
},
{
"name": "jvb_password",
"type": "str",
"title": "Wachtwoord voor de mediabrug",
"default": "",
"required": true,
"secret": true,
"generate": "auto"
}
]
}
]
}

View file

@ -0,0 +1,43 @@
services:
<< service_name >>:
image: dessalines/lemmy:0.19.11
container_name: << service_name >>
environment:
- TZ=<< timezone >>
- RUST_LOG=warn
volumes:
- << appdata_dir >>/<< service_name >>:/config
- ./lemmy.hjson:/config/config.hjson:ro
depends_on:
- << service_name >>-db
restart: unless-stopped
<< service_name >>-ui:
image: dessalines/lemmy-ui:0.19.11
container_name: << service_name >>-ui
environment:
- LEMMY_UI_LEMMY_INTERNAL_HOST=<< service_name >>:8536
- LEMMY_UI_LEMMY_EXTERNAL_HOST=<< domein >>
- LEMMY_UI_HTTPS=true
ports:
- "<< port_ui >>:1234"
depends_on:
- << service_name >>
restart: unless-stopped
<< service_name >>-pictrs:
image: asonix/pictrs:0.5
container_name: << service_name >>-pictrs
environment:
- PICTRS__SERVER__API_KEY=<< pictrs_key >>
volumes:
- << appdata_dir >>/<< service_name >>-pictrs:/mnt
restart: unless-stopped
<< service_name >>-db:
image: postgres:16-alpine
container_name: << service_name >>-db
environment:
- POSTGRES_DB=lemmy
- POSTGRES_USER=lemmy
- POSTGRES_PASSWORD=<< db_password >>
volumes:
- << appdata_dir >>/<< service_name >>-db:/var/lib/postgresql/data
restart: unless-stopped

View file

@ -0,0 +1,16 @@
{
hostname: "<< domein >>"
bind: "0.0.0.0"
port: 8536
database: {
host: "<< service_name >>-db"
user: "lemmy"
password: "<< db_password >>"
database: "lemmy"
port: 5432
}
pictrs: {
url: "http://<< service_name >>-pictrs:8080/"
api_key: "<< pictrs_key >>"
}
}

95
apps/lemmy/template.json Normal file
View file

@ -0,0 +1,95 @@
{
"kind": "compose",
"metadata": {
"name": "Lemmy",
"description": "Forum in gemeenschappen, opgebouwd uit servers die elkaar volgen. Berichten stemmen omhoog of omlaag. Vervangt Reddit.",
"tags": [
"forum",
"sociaal",
"federatie"
],
"icon": {
"provider": "selfhst",
"id": "lemmy"
},
"version": {
"name": "latest"
},
"categories": [
"communicatie"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "lemmy",
"required": true
},
{
"name": "port_ui",
"type": "int",
"title": "Webinterface",
"default": 8189,
"required": true
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
},
{
"title": "Instellingen",
"items": [
{
"name": "domein",
"type": "str",
"title": "Publiek domein",
"default": "forum.example.com",
"required": true,
"description": "Ligt vast zodra andere servers je kennen"
}
]
},
{
"title": "Toegang",
"items": [
{
"name": "db_password",
"type": "str",
"title": "Databasewachtwoord",
"default": "",
"required": true,
"secret": true,
"generate": "auto",
"description": "Wordt automatisch ingevuld en in .env bewaard"
},
{
"name": "pictrs_key",
"type": "str",
"title": "Sleutel voor de afbeeldingsdienst",
"default": "",
"required": true,
"secret": true,
"generate": "auto"
}
]
}
]
}

View file

@ -75,7 +75,9 @@
"default": "", "default": "",
"required": true, "required": true,
"secret": true, "secret": true,
"generate": "auto" "generate": "auto",
"secret_format": "hex",
"secret_bytes": 32
}, },
{ {
"name": "creds_iv", "name": "creds_iv",
@ -84,7 +86,9 @@
"default": "", "default": "",
"required": true, "required": true,
"secret": true, "secret": true,
"generate": "auto" "generate": "auto",
"secret_format": "hex",
"secret_bytes": 16
}, },
{ {
"name": "jwt_secret", "name": "jwt_secret",

View file

@ -0,0 +1,13 @@
services:
<< service_name >>:
image: libretranslate/libretranslate:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
- LT_LOAD_ONLY=<< talen >>
- LT_DISABLE_WEB_UI=false
volumes:
- << appdata_dir >>/<< service_name >>:/home/libretranslate/.local/share/argos-translate
ports:
- "<< port >>:5000"
restart: unless-stopped

View file

@ -0,0 +1,73 @@
{
"kind": "compose",
"metadata": {
"name": "LibreTranslate",
"description": "Vertaalt tekst met modellen die op je eigen server draaien. Je zinnen verlaten je netwerk niet. Vervangt Google Translate. Let op: Bij de eerste start worden de taalmodellen gedownload; dat duurt even.",
"tags": [
"vertalen",
"taal",
"privacy"
],
"icon": {
"provider": "selfhst",
"id": "libretranslate"
},
"version": {
"name": "latest"
},
"categories": [
"productiviteit",
"ai"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "libretranslate",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8183,
"required": true,
"description": "Standaard 5000; hier verplaatst omdat die poort al bezet is"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
},
{
"title": "Instellingen",
"items": [
{
"name": "talen",
"type": "str",
"title": "Talen",
"default": "en,nl,de,fr",
"required": true,
"description": "Komma-gescheiden; minder talen betekent een veel kleinere download"
}
]
}
]
}

View file

@ -0,0 +1,46 @@
services:
<< service_name >>:
image: ghcr.io/mastodon/mastodon:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
- LOCAL_DOMAIN=<< domein >>
- RAILS_ENV=production
- DB_HOST=<< service_name >>-db
- DB_USER=mastodon
- DB_NAME=mastodon
- DB_PASS=<< db_password >>
- DB_PORT=5432
- REDIS_HOST=<< service_name >>-redis
- REDIS_PORT=6379
- SECRET_KEY_BASE=<< secret_key_base >>
- OTP_SECRET=<< otp_secret >>
- ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=<< ar_primary_key >>
- ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=<< ar_deterministic_key >>
- ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=<< ar_salt >>
volumes:
- << appdata_dir >>/<< service_name >>:/mastodon/public/system
command: bash -c "rails db:migrate && rails s -p 3000"
ports:
- "<< port >>:3000"
depends_on:
- << service_name >>-db
- << service_name >>-redis
restart: unless-stopped
<< service_name >>-db:
image: postgres:16-alpine
container_name: << service_name >>-db
environment:
- POSTGRES_DB=mastodon
- POSTGRES_USER=mastodon
- POSTGRES_PASSWORD=<< db_password >>
volumes:
- << appdata_dir >>/<< service_name >>-db:/var/lib/postgresql/data
restart: unless-stopped
<< service_name >>-redis:
image: valkey/valkey:8-alpine
container_name: << service_name >>-redis
command: valkey-server --save 60 1
volumes:
- << appdata_dir >>/<< service_name >>-redis:/data
restart: unless-stopped

132
apps/mastodon/template.json Normal file
View file

@ -0,0 +1,132 @@
{
"kind": "compose",
"metadata": {
"name": "Mastodon",
"description": "Sociaal netwerk dat met duizenden andere servers samenwerkt. Jij bepaalt de regels op je eigen server. Vervangt X (Twitter). Let op: Maak na de eerste start een beheerder aan met `docker exec -it <container> bin/tootctl accounts create ... --role Owner`.",
"tags": [
"sociaal",
"federatie",
"microblog"
],
"icon": {
"provider": "selfhst",
"id": "mastodon"
},
"version": {
"name": "latest"
},
"categories": [
"communicatie"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "mastodon",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8187,
"required": true,
"description": "Standaard 3000; hier verplaatst omdat die poort al bezet is"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
},
{
"title": "Instellingen",
"items": [
{
"name": "domein",
"type": "str",
"title": "Publiek domein",
"default": "social.example.com",
"required": true,
"description": "Zit in het adres van elke gebruiker en ligt daarna vast"
}
]
},
{
"title": "Toegang",
"items": [
{
"name": "db_password",
"type": "str",
"title": "Databasewachtwoord",
"default": "",
"required": true,
"secret": true,
"generate": "auto",
"description": "Wordt automatisch ingevuld en in .env bewaard"
},
{
"name": "secret_key_base",
"type": "str",
"title": "Rails-sleutel",
"default": "",
"required": true,
"secret": true,
"generate": "auto"
},
{
"name": "otp_secret",
"type": "str",
"title": "Sleutel voor tweefactorcodes",
"default": "",
"required": true,
"secret": true,
"generate": "auto"
},
{
"name": "ar_primary_key",
"type": "str",
"title": "Sleutel voor versleutelde velden",
"default": "",
"required": true,
"secret": true,
"generate": "auto"
},
{
"name": "ar_deterministic_key",
"type": "str",
"title": "Sleutel voor doorzoekbare velden",
"default": "",
"required": true,
"secret": true,
"generate": "auto"
},
{
"name": "ar_salt",
"type": "str",
"title": "Salt voor de sleutelafleiding",
"default": "",
"required": true,
"secret": true,
"generate": "auto"
}
]
}
]
}

View file

@ -0,0 +1,14 @@
services:
<< service_name >>:
image: onlyoffice/documentserver:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
- JWT_ENABLED=true
- JWT_SECRET=<< jwt_secret >>
volumes:
- << appdata_dir >>/<< service_name >>:/var/www/onlyoffice/Data
- << appdata_dir >>/<< service_name >>-log:/var/log/onlyoffice
ports:
- "<< port >>:80"
restart: unless-stopped

View file

@ -0,0 +1,75 @@
{
"kind": "compose",
"metadata": {
"name": "ONLYOFFICE",
"description": "Kantoorsuite in je browser die goed overweg kan met docx, xlsx en pptx. Koppelt aan Nextcloud en Seafile. Vervangt Google Docs en Microsoft Office.",
"tags": [
"documenten",
"kantoor",
"samenwerken"
],
"icon": {
"provider": "selfhst",
"id": "onlyoffice"
},
"version": {
"name": "latest"
},
"categories": [
"documenten",
"productiviteit"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "onlyoffice",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8192,
"required": true,
"description": "Standaard 80; hier verplaatst omdat die poort al bezet is"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
},
{
"title": "Toegang",
"items": [
{
"name": "jwt_secret",
"type": "str",
"title": "JWT-sleutel",
"default": "",
"required": true,
"secret": true,
"generate": "auto",
"description": "Dezelfde waarde vul je in bij Nextcloud of Seafile"
}
]
}
]
}

View file

@ -0,0 +1,12 @@
services:
<< service_name >>:
image: owncast/owncast:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
volumes:
- << appdata_dir >>/<< service_name >>:/app/data
ports:
- "<< port >>:8080"
- "<< port_rtmp >>:1935"
restart: unless-stopped

View file

@ -0,0 +1,67 @@
{
"kind": "compose",
"metadata": {
"name": "Owncast",
"description": "Je eigen livestream met chat, zonder platform ertussen. Zendt uit vanuit OBS via RTMP. Vervangt Twitch.",
"tags": [
"streaming",
"video",
"live"
],
"icon": {
"provider": "selfhst",
"id": "owncast"
},
"version": {
"name": "latest"
},
"categories": [
"media"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "owncast",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8184,
"required": true,
"description": "Standaard 8080; hier verplaatst omdat die poort al bezet is"
},
{
"name": "port_rtmp",
"type": "int",
"title": "RTMP-poort",
"default": 1935,
"required": true,
"description": "Hier zendt OBS naartoe"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
}
]
}

View file

@ -0,0 +1,41 @@
services:
<< service_name >>:
image: chocobozzz/peertube:production-bookworm
container_name: << service_name >>
environment:
- TZ=<< timezone >>
- PEERTUBE_WEBSERVER_HOSTNAME=<< hostname >>
- PEERTUBE_WEBSERVER_PORT=443
- PEERTUBE_WEBSERVER_HTTPS=true
- PEERTUBE_DB_HOSTNAME=<< service_name >>-db
- PEERTUBE_DB_USERNAME=peertube
- PEERTUBE_DB_PASSWORD=<< db_password >>
- PEERTUBE_REDIS_HOSTNAME=<< service_name >>-redis
- PEERTUBE_SECRET=<< peertube_secret >>
- PEERTUBE_ADMIN_EMAIL=<< admin_email >>
volumes:
- << appdata_dir >>/<< service_name >>:/data
- << appdata_dir >>/<< service_name >>-config:/config
ports:
- "<< port >>:9000"
depends_on:
- << service_name >>-db
- << service_name >>-redis
restart: unless-stopped
<< service_name >>-db:
image: postgres:16-alpine
container_name: << service_name >>-db
environment:
- POSTGRES_DB=peertube
- POSTGRES_USER=peertube
- POSTGRES_PASSWORD=<< db_password >>
volumes:
- << appdata_dir >>/<< service_name >>-db:/var/lib/postgresql/data
restart: unless-stopped
<< service_name >>-redis:
image: valkey/valkey:8-alpine
container_name: << service_name >>-redis
command: valkey-server --save 60 1
volumes:
- << appdata_dir >>/<< service_name >>-redis:/data
restart: unless-stopped

103
apps/peertube/template.json Normal file
View file

@ -0,0 +1,103 @@
{
"kind": "compose",
"metadata": {
"name": "PeerTube",
"description": "Videoplatform dat met andere PeerTube-servers samenwerkt en kijkers de video onderling laat doorgeven. Vervangt YouTube.",
"tags": [
"video",
"streaming",
"federatie"
],
"icon": {
"provider": "selfhst",
"id": "peertube"
},
"version": {
"name": "latest"
},
"categories": [
"media"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "peertube",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8185,
"required": true,
"description": "Standaard 9000; hier verplaatst omdat die poort al bezet is"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
},
{
"title": "Instellingen",
"items": [
{
"name": "hostname",
"type": "str",
"title": "Publiek domein",
"default": "video.example.com",
"required": true,
"description": "Ligt vast zodra je server bekend is bij andere servers"
},
{
"name": "admin_email",
"type": "str",
"title": "E-mailadres beheerder",
"default": "admin@example.com",
"required": true
}
]
},
{
"title": "Toegang",
"items": [
{
"name": "db_password",
"type": "str",
"title": "Databasewachtwoord",
"default": "",
"required": true,
"secret": true,
"generate": "auto",
"description": "Wordt automatisch ingevuld en in .env bewaard"
},
{
"name": "peertube_secret",
"type": "str",
"title": "Geheime sleutel",
"default": "",
"required": true,
"secret": true,
"generate": "auto"
}
]
}
]
}

View file

@ -0,0 +1,56 @@
services:
<< service_name >>:
image: penpotapp/frontend:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
- PENPOT_FLAGS=enable-registration enable-login-with-password
- PENPOT_BACKEND_URI=http://<< service_name >>-backend:6060
- PENPOT_EXPORTER_URI=http://<< service_name >>-exporter:6061
ports:
- "<< port >>:8080"
depends_on:
- << service_name >>-backend
restart: unless-stopped
<< service_name >>-backend:
image: penpotapp/backend:latest
container_name: << service_name >>-backend
environment:
- PENPOT_FLAGS=enable-registration enable-login-with-password
- PENPOT_PUBLIC_URI=<< app_url >>
- PENPOT_SECRET_KEY=<< secret_key >>
- PENPOT_DATABASE_URI=postgresql://<< service_name >>-db/penpot
- PENPOT_DATABASE_USERNAME=penpot
- PENPOT_DATABASE_PASSWORD=<< db_password >>
- PENPOT_REDIS_URI=redis://<< service_name >>-redis/0
- PENPOT_ASSETS_STORAGE_BACKEND=assets-fs
- PENPOT_STORAGE_ASSETS_FS_DIRECTORY=/opt/data/assets
volumes:
- << appdata_dir >>/<< service_name >>-assets:/opt/data
depends_on:
- << service_name >>-db
- << service_name >>-redis
restart: unless-stopped
<< service_name >>-exporter:
image: penpotapp/exporter:latest
container_name: << service_name >>-exporter
environment:
- PENPOT_PUBLIC_URI=http://<< service_name >>:8080
restart: unless-stopped
<< service_name >>-db:
image: postgres:16-alpine
container_name: << service_name >>-db
environment:
- POSTGRES_DB=penpot
- POSTGRES_USER=penpot
- POSTGRES_PASSWORD=<< db_password >>
volumes:
- << appdata_dir >>/<< service_name >>-db:/var/lib/postgresql/data
restart: unless-stopped
<< service_name >>-redis:
image: valkey/valkey:8-alpine
container_name: << service_name >>-redis
command: valkey-server --save 60 1
volumes:
- << appdata_dir >>/<< service_name >>-redis:/data
restart: unless-stopped

96
apps/penpot/template.json Normal file
View file

@ -0,0 +1,96 @@
{
"kind": "compose",
"metadata": {
"name": "Penpot",
"description": "Ontwerpen en prototypes maken in je browser, met open bestandsformaten in plaats van een gesloten dienst. Vervangt Figma.",
"tags": [
"ontwerp",
"prototypes",
"samenwerken"
],
"icon": {
"provider": "selfhst",
"id": "penpot"
},
"version": {
"name": "latest"
},
"categories": [
"productiviteit",
"ontwikkeling"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "penpot",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8198,
"required": true,
"description": "Standaard 8080; hier verplaatst omdat die poort al bezet is"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
},
{
"title": "Instellingen",
"items": [
{
"name": "app_url",
"type": "str",
"title": "Publiek adres",
"default": "http://localhost:8198",
"required": true
}
]
},
{
"title": "Toegang",
"items": [
{
"name": "db_password",
"type": "str",
"title": "Databasewachtwoord",
"default": "",
"required": true,
"secret": true,
"generate": "auto",
"description": "Wordt automatisch ingevuld en in .env bewaard"
},
{
"name": "secret_key",
"type": "str",
"title": "Geheime sleutel",
"default": "",
"required": true,
"secret": true,
"generate": "auto"
}
]
}
]
}

View file

@ -0,0 +1,35 @@
services:
<< service_name >>:
image: zknt/pixelfed:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
- APP_KEY=<< app_key >>
- APP_URL=<< app_url >>
- APP_DOMAIN=<< domein >>
- ADMIN_DOMAIN=<< domein >>
- SESSION_DOMAIN=<< domein >>
- DB_CONNECTION=mysql
- DB_HOST=<< service_name >>-db
- DB_PORT=3306
- DB_DATABASE=pixelfed
- DB_USERNAME=pixelfed
- DB_PASSWORD=<< db_password >>
volumes:
- << appdata_dir >>/<< service_name >>:/var/www/storage
ports:
- "<< port >>:80"
depends_on:
- << service_name >>-db
restart: unless-stopped
<< service_name >>-db:
image: mariadb:11
container_name: << service_name >>-db
environment:
- MARIADB_DATABASE=pixelfed
- MARIADB_USER=pixelfed
- MARIADB_PASSWORD=<< db_password >>
- MARIADB_ROOT_PASSWORD=<< db_password >>
volumes:
- << appdata_dir >>/<< service_name >>-db:/var/lib/mysql
restart: unless-stopped

106
apps/pixelfed/template.json Normal file
View file

@ -0,0 +1,106 @@
{
"kind": "compose",
"metadata": {
"name": "Pixelfed",
"description": "Foto's delen op een netwerk dat met Mastodon en de rest van de fediverse praat. Vervangt Instagram.",
"tags": [
"fotos",
"sociaal",
"federatie"
],
"icon": {
"provider": "selfhst",
"id": "pixelfed"
},
"version": {
"name": "latest"
},
"categories": [
"fotos",
"communicatie"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "pixelfed",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8188,
"required": true,
"description": "Standaard 80; hier verplaatst omdat die poort al bezet is"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
},
{
"title": "Instellingen",
"items": [
{
"name": "domein",
"type": "str",
"title": "Publiek domein",
"default": "foto.example.com",
"required": true
},
{
"name": "app_url",
"type": "str",
"title": "Publiek adres",
"default": "https://foto.example.com",
"required": true
}
]
},
{
"title": "Toegang",
"items": [
{
"name": "db_password",
"type": "str",
"title": "Databasewachtwoord",
"default": "",
"required": true,
"secret": true,
"generate": "auto",
"description": "Wordt automatisch ingevuld en in .env bewaard"
},
{
"name": "app_key",
"type": "str",
"title": "Applicatiesleutel",
"default": "",
"required": true,
"secret": true,
"generate": "auto",
"secret_format": "laravel",
"secret_bytes": 32,
"description": "Laravel-formaat; wordt automatisch in de juiste vorm gemaakt"
}
]
}
]
}

View file

@ -0,0 +1,12 @@
services:
<< service_name >>:
image: tomsquest/docker-radicale:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
volumes:
- << appdata_dir >>/<< service_name >>:/config
- << appdata_dir >>/<< service_name >>-data:/data
ports:
- "<< port >>:5232"
restart: unless-stopped

View file

@ -0,0 +1,61 @@
{
"kind": "compose",
"metadata": {
"name": "Radicale",
"description": "Kleine, rustige CalDAV- en CardDAV-server. Bestanden op schijf, geen database, weinig onderhoud. Vervangt Google Agenda en Google Contacten.",
"tags": [
"agenda",
"contacten",
"caldav",
"carddav"
],
"icon": {
"provider": "selfhst",
"id": "radicale"
},
"version": {
"name": "latest"
},
"categories": [
"gezin",
"productiviteit"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "radicale",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8182,
"required": true,
"description": "Standaard 5232; hier verplaatst omdat die poort al bezet is"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
}
]
}

View file

@ -0,0 +1,25 @@
services:
<< service_name >>:
image: lukevella/rallly:latest
container_name: << service_name >>
environment:
- TZ=<< timezone >>
- DATABASE_URL=postgresql://rallly:<< db_password >>@<< service_name >>-db:5432/rallly
- SECRET_PASSWORD=<< secret_password >>
- NEXT_PUBLIC_BASE_URL=<< app_url >>
- NOREPLY_EMAIL=<< afzender >>
ports:
- "<< port >>:3000"
depends_on:
- << service_name >>-db
restart: unless-stopped
<< service_name >>-db:
image: postgres:16-alpine
container_name: << service_name >>-db
environment:
- POSTGRES_DB=rallly
- POSTGRES_USER=rallly
- POSTGRES_PASSWORD=<< db_password >>
volumes:
- << appdata_dir >>/<< service_name >>-db:/var/lib/postgresql/data
restart: unless-stopped

104
apps/rallly/template.json Normal file
View file

@ -0,0 +1,104 @@
{
"kind": "compose",
"metadata": {
"name": "Rallly",
"description": "Prikt een datum met een groep: iedereen vinkt aan wanneer het kan, jij ziet meteen wat wint. Vervangt Doodle.",
"tags": [
"planning",
"agenda",
"groep"
],
"icon": {
"provider": "selfhst",
"id": "rallly"
},
"version": {
"name": "latest"
},
"categories": [
"productiviteit",
"gezin"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "rallly",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8195,
"required": true,
"description": "Standaard 3000; hier verplaatst omdat die poort al bezet is"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
},
{
"title": "Instellingen",
"items": [
{
"name": "app_url",
"type": "str",
"title": "Publiek adres",
"default": "http://localhost:8195",
"required": true
},
{
"name": "afzender",
"type": "str",
"title": "Afzenderadres",
"default": "noreply@example.com",
"required": true
}
]
},
{
"title": "Toegang",
"items": [
{
"name": "db_password",
"type": "str",
"title": "Databasewachtwoord",
"default": "",
"required": true,
"secret": true,
"generate": "auto",
"description": "Wordt automatisch ingevuld en in .env bewaard"
},
{
"name": "secret_password",
"type": "str",
"title": "Geheime sleutel",
"default": "",
"required": true,
"secret": true,
"generate": "auto",
"description": "Minimaal 32 tekens"
}
]
}
]
}

View file

@ -0,0 +1,35 @@
services:
<< service_name >>:
image: snipe/snipe-it:latest
container_name: << service_name >>
environment:
- APP_KEY=<< app_key >>
- APP_URL=<< app_url >>
- APP_TIMEZONE=<< timezone >>
- MYSQL_DATABASE=snipeit
- MYSQL_USER=snipeit
- MYSQL_PASSWORD=<< db_password >>
- DB_CONNECTION=mysql
- DB_HOST=<< service_name >>-db
- DB_PORT=3306
- DB_DATABASE=snipeit
- DB_USERNAME=snipeit
- DB_PASSWORD=<< db_password >>
volumes:
- << appdata_dir >>/<< service_name >>:/var/lib/snipeit
ports:
- "<< port >>:80"
depends_on:
- << service_name >>-db
restart: unless-stopped
<< service_name >>-db:
image: mariadb:11
container_name: << service_name >>-db
environment:
- MARIADB_DATABASE=snipeit
- MARIADB_USER=snipeit
- MARIADB_PASSWORD=<< db_password >>
- MARIADB_ROOT_PASSWORD=<< db_password >>
volumes:
- << appdata_dir >>/<< service_name >>-db:/var/lib/mysql
restart: unless-stopped

View file

@ -0,0 +1,98 @@
{
"kind": "compose",
"metadata": {
"name": "Snipe-IT",
"description": "Houdt bij welke apparaten je hebt, wie ze in gebruik heeft en wanneer de garantie afloopt. Met barcodes en een uitleenregistratie.",
"tags": [
"assetbeheer",
"inventaris",
"hardware"
],
"icon": {
"provider": "selfhst",
"id": "snipe-it"
},
"version": {
"name": "latest"
},
"categories": [
"zakelijk"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "snipeit",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8178,
"required": true,
"description": "Standaard 80; hier verplaatst omdat die poort al bezet is"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
},
{
"title": "Instellingen",
"items": [
{
"name": "app_url",
"type": "str",
"title": "Publiek adres",
"default": "http://localhost:8178",
"required": true
}
]
},
{
"title": "Toegang",
"items": [
{
"name": "db_password",
"type": "str",
"title": "Databasewachtwoord",
"default": "",
"required": true,
"secret": true,
"generate": "auto",
"description": "Wordt automatisch ingevuld en in .env bewaard"
},
{
"name": "app_key",
"type": "str",
"title": "Applicatiesleutel",
"default": "",
"required": true,
"secret": true,
"generate": "auto",
"secret_format": "laravel",
"secret_bytes": 32,
"description": "Laravel-formaat; wordt automatisch in de juiste vorm gemaakt"
}
]
}
]
}

View file

@ -0,0 +1,41 @@
services:
<< service_name >>:
image: bbilly1/tubearchivist:latest
container_name: << service_name >>
environment:
- ES_URL=http://<< service_name >>-es:9200
- REDIS_CON=redis://<< service_name >>-redis:6379
- HOST_UID=<< puid >>
- HOST_GID=<< pgid >>
- TA_HOST=<< app_url >>
- TA_USERNAME=<< admin_user >>
- TA_PASSWORD=<< admin_password >>
- ELASTIC_PASSWORD=<< db_password >>
- TZ=<< timezone >>
volumes:
- << appdata_dir >>/<< service_name >>:/cache
- << data_root >>:/data
ports:
- "<< port >>:8000"
depends_on:
- << service_name >>-es
- << service_name >>-redis
restart: unless-stopped
<< service_name >>-es:
image: bbilly1/tubearchivist-es:latest
container_name: << service_name >>-es
environment:
- ELASTIC_PASSWORD=<< db_password >>
- discovery.type=single-node
- xpack.security.enabled=true
- path.repo=/usr/share/elasticsearch/data/snapshot
volumes:
- << appdata_dir >>/<< service_name >>-es:/usr/share/elasticsearch/data
restart: unless-stopped
<< service_name >>-redis:
image: valkey/valkey:8-alpine
container_name: << service_name >>-redis
command: valkey-server --save 60 1
volumes:
- << appdata_dir >>/<< service_name >>-redis:/data
restart: unless-stopped

View file

@ -0,0 +1,132 @@
{
"kind": "compose",
"metadata": {
"name": "Tube Archivist",
"description": "Bewaart YouTube-kanalen en afspeellijsten op je eigen schijf, met eigen zoekfunctie en kijkgeschiedenis. Vervangt YouTube-abonnementen.",
"tags": [
"video",
"youtube",
"archief"
],
"icon": {
"provider": "selfhst",
"id": "tubearchivist"
},
"version": {
"name": "latest"
},
"categories": [
"media",
"downloaden"
]
},
"variables": [
{
"title": "Algemeen",
"items": [
{
"name": "service_name",
"type": "str",
"title": "Servicenaam",
"default": "tubearchivist",
"required": true
},
{
"name": "port",
"type": "int",
"title": "Webinterface",
"default": 8199,
"required": true,
"description": "Standaard 8000; hier verplaatst omdat die poort al bezet is"
},
{
"name": "appdata_dir",
"type": "str",
"title": "Appdata-map",
"default": "/opt/serverup/appdata",
"required": true,
"description": "Basismap voor de gegevens van deze app"
},
{
"name": "data_root",
"type": "str",
"title": "Gedeelde datamap",
"default": "/mnt/data",
"required": true,
"description": "Eén map met je media, gekoppeld als /data",
"help": "Media en downloads horen onder één map te staan, die als /data in elke container komt. Alleen dan kan een voltooide download met een hardlink naar je bibliotheek: geen kopie, geen dubbele opslag. Eronder: media/films, media/series, torrents/ en usenet/."
},
{
"name": "timezone",
"type": "str",
"title": "Tijdzone",
"default": "Europe/Amsterdam",
"required": true
}
]
},
{
"title": "Gebruiker",
"items": [
{
"name": "puid",
"type": "int",
"title": "Gebruikers-ID (PUID)",
"default": 1000,
"required": true,
"description": "`id -u` op je server"
},
{
"name": "pgid",
"type": "int",
"title": "Groeps-ID (PGID)",
"default": 1000,
"required": true
}
]
},
{
"title": "Instellingen",
"items": [
{
"name": "app_url",
"type": "str",
"title": "Publiek adres",
"default": "http://localhost:8199",
"required": true
},
{
"name": "admin_user",
"type": "str",
"title": "Beheerdersnaam",
"default": "admin",
"required": true
}
]
},
{
"title": "Toegang",
"items": [
{
"name": "db_password",
"type": "str",
"title": "Databasewachtwoord",
"default": "",
"required": true,
"secret": true,
"generate": "auto",
"description": "Wordt automatisch ingevuld en in .env bewaard"
},
{
"name": "admin_password",
"type": "str",
"title": "Beheerderswachtwoord",
"default": "",
"required": true,
"secret": true,
"eigen_invoer": true
}
]
}
]
}

View file

@ -373,6 +373,24 @@ containernamen zonder. Het wachtwoord is een veld met `"secret": true` en
belandt in plaats van in het compose-bestand. Vergeet `depends_on` niet, anders belandt in plaats van in het compose-bestand. Vergeet `depends_on` niet, anders
start de app vóór zijn database en valt hij om op de eerste verbinding. start de app vóór zijn database en valt hij om op de eerste verbinding.
**Sleutels in een bepaald formaat.** `"generate": "auto"` maakt url-veilige
base64 van 24 bytes — 32 tekens. Wil de app iets anders, zeg dat er dan bij:
| `secret_format` | Levert | Voor |
|---|---|---|
| *(weglaten)* | url-veilige base64, 32 tekens | het meeste |
| `hex` | hexadecimaal, `secret_bytes` × 2 tekens | Homarr, LibreChat |
| `laravel` | `base64:` + standaard-base64 | Snipe-IT, Invoice Ninja, Pixelfed |
```json
{"name": "app_key", "type": "str", "title": "Applicatiesleutel",
"default": "", "required": true, "secret": true, "generate": "auto",
"secret_format": "laravel", "secret_bytes": 32}
```
Dit is geen kosmetiek: een sleutel in het verkeerde formaat levert een container
op die niet start, met een foutmelding die nergens naar de sleutel wijst.
**Een geheim dat je nergens vandaan kunt genereren** — het client-secret van een **Een geheim dat je nergens vandaan kunt genereren** — het client-secret van een
OIDC-toepassing, een wachtwoord dat de gebruiker zelf kiest — markeer je met OIDC-toepassing, een wachtwoord dat de gebruiker zelf kiest — markeer je met
`"eigen_invoer": true`. Anders klaagt de testsuite terecht dat er een verplicht `"eigen_invoer": true`. Anders klaagt de testsuite terecht dat er een verplicht

View file

@ -152,6 +152,12 @@ def fields(d: Path) -> list[dict]:
# willekeurigs. "auto" = ook meteen invullen, True = alleen een # willekeurigs. "auto" = ook meteen invullen, True = alleen een
# knop (voor wachtwoorden waarmee je zelf inlogt). # knop (voor wachtwoorden waarmee je zelf inlogt).
"generate": item.get("generate") or "", "generate": item.get("generate") or "",
# In welke vorm de gegenereerde waarde moet komen. Laravel wil
# `base64:` ervoor, Homarr en LibreChat willen hexadecimaal van
# een vaste lengte. Zonder dit kreeg je een geldige willekeurige
# string die de app niet accepteert.
"secret_format": item.get("secret_format") or "",
"secret_bytes": item.get("secret_bytes") or 0,
# Een geheim dat de gebruiker ergens anders vandaan haalt: het # Een geheim dat de gebruiker ergens anders vandaan haalt: het
# client-secret van een OIDC-toepassing, een wachtwoord dat hij # client-secret van een OIDC-toepassing, een wachtwoord dat hij
# zelf kiest. Daar valt niets voor te genereren, en het is geen # zelf kiest. Daar valt niets voor te genereren, en het is geen

View file

@ -1738,7 +1738,7 @@ tailwind.config = {
<span class="mdi" :class="revealed[f.name] ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"></span> <span class="mdi" :class="revealed[f.name] ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"></span>
</button> </button>
<button type="button" class="btn-sm shrink-0" x-show="f.generate" :title="t('generate_value')" <button type="button" class="btn-sm shrink-0" x-show="f.generate" :title="t('generate_value')"
@click="install.values[f.name] = randomSecret(); revealed[f.name] = true"><span class="mdi mdi-dice-5-outline"></span></button> @click="install.values[f.name] = veldSecret(f); revealed[f.name] = true"><span class="mdi mdi-dice-5-outline"></span></button>
</div> </div>
</template> </template>
<p x-show="f.port_taken" class="text-[11px] text-wr mt-1" <p x-show="f.port_taken" class="text-[11px] text-wr mt-1"
@ -2838,11 +2838,22 @@ function app() {
// Cryptografisch willekeurige waarde voor admin-tokens en wachtwoorden. // Cryptografisch willekeurige waarde voor admin-tokens en wachtwoorden.
// Lokaal gegenereerd, dus het geheim gaat niet heen en weer over het net. // Lokaal gegenereerd, dus het geheim gaat niet heen en weer over het net.
randomSecret(bytes = 24) { // Niet elke sleutel mag er hetzelfde uitzien. Laravel wil `base64:` met
// standaard-base64 ervoor, Homarr en LibreChat willen hexadecimaal van een
// vaste lengte. Een waarde in het verkeerde formaat levert een container op
// die niet start — met een foutmelding die nergens naar een sleutel wijst.
randomSecret(bytes = 24, formaat = 'base64url') {
const buf = new Uint8Array(bytes); const buf = new Uint8Array(bytes);
crypto.getRandomValues(buf); crypto.getRandomValues(buf);
return btoa(String.fromCharCode(...buf)) if (formaat === 'hex')
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); return Array.from(buf, b => b.toString(16).padStart(2, '0')).join('');
const b64 = btoa(String.fromCharCode(...buf));
if (formaat === 'laravel') return 'base64:' + b64;
return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
},
// De sleutel zoals dít veld hem wil hebben.
veldSecret(f) {
return this.randomSecret(f.secret_bytes || 24, f.secret_format || 'base64url');
}, },
// ── Containers op deze host ────────────────────────────────────────────── // ── Containers op deze host ──────────────────────────────────────────────
@ -2975,7 +2986,7 @@ function app() {
for (const f of (d.fields || [])) { for (const f of (d.fields || [])) {
// 'auto': waarden die je nooit hoeft te kennen. Een inlogwachtwoord // 'auto': waarden die je nooit hoeft te kennen. Een inlogwachtwoord
// krijgt alleen een knop, want dat moet je zelf noteren. // krijgt alleen een knop, want dat moet je zelf noteren.
if (f.generate === 'auto' && !init[f.name]) init[f.name] = this.randomSecret(); if (f.generate === 'auto' && !init[f.name]) init[f.name] = this.veldSecret(f);
} }
this.install.values = init; this.install.values = init;
this.install.preview_files = d.preview_files || {}; this.install.preview_files = d.preview_files || {};

View file

@ -92,6 +92,27 @@ check('één groep: geen kop', c.stepGroups('basis')[0].showHeader === false);
c.install.fields.push(veld('c','basis',{group:'Tweede'})); c.install.fields.push(veld('c','basis',{group:'Tweede'}));
check('twee groepen: wel koppen', c.stepGroups('basis').every(g => g.showHeader)); check('twee groepen: wel koppen', c.stepGroups('basis').every(g => g.showHeader));
// ── Sleutelformaten ─────────────────────────────────────────────────────────
// Een willekeurige string is niet genoeg: Laravel wil `base64:` ervoor, Homarr
// en LibreChat willen hexadecimaal van een vaste lengte. In het verkeerde
// formaat start de container niet, met een foutmelding die nergens naar de
// sleutel wijst.
const standaard = c.veldSecret({});
check('standaard is url-veilig', /^[A-Za-z0-9_-]+$/.test(standaard));
check('standaard is 32 tekens', standaard.length === 32);
const hex = c.veldSecret({secret_format: 'hex', secret_bytes: 32});
check('hex is 64 tekens', hex.length === 64);
check('hex bevat alleen hexcijfers', /^[0-9a-f]{64}$/.test(hex));
check('hex van 16 bytes is 32 tekens',
c.veldSecret({secret_format: 'hex', secret_bytes: 16}).length === 32);
const lar = c.veldSecret({secret_format: 'laravel', secret_bytes: 32});
check('laravel begint met base64:', lar.startsWith('base64:'));
check('laravel is standaard-base64 van 32 bytes',
Buffer.from(lar.slice(7), 'base64').length === 32);
check('twee sleutels zijn niet gelijk', c.veldSecret({}) !== c.veldSecret({}));
// ── Stap Onderdelen ───────────────────────────────────────────────────────── // ── Stap Onderdelen ─────────────────────────────────────────────────────────
// Een stack als de ARR-stack heeft vijftig schakelbare onderdelen. Die horen in // Een stack als de ARR-stack heeft vijftig schakelbare onderdelen. Die horen in
// één eigen stap, gegroepeerd per kopje — en nergens anders nóg een keer. // één eigen stap, gegroepeerd per kopje — en nergens anders nóg een keer.

View file

@ -535,6 +535,9 @@ def _speciaal(app):
{"name": "homarr_encryption_key", "type": "str", {"name": "homarr_encryption_key", "type": "str",
"title": "Versleutelingssleutel", "default": "", "required": True, "title": "Versleutelingssleutel", "default": "", "required": True,
"secret": True, "generate": "auto", "secret": True, "generate": "auto",
# Homarr accepteert alleen 64 hexadecimale tekens; een gewone
# willekeurige string weigert hij bij het opstarten.
"secret_format": "hex", "secret_bytes": 32,
"description": "64 tekens; versleutelt de opgeslagen API-sleutels van je apps"}, "description": "64 tekens; versleutelt de opgeslagen API-sleutels van je apps"},
]}] ]}]
app["env_speciaal"] = ["SECRET_ENCRYPTION_KEY=<< homarr_encryption_key >>"] app["env_speciaal"] = ["SECRET_ENCRYPTION_KEY=<< homarr_encryption_key >>"]