Self-hosted apps on hardware you own, without the networking headaches.
Boot the installer on any machine — bare metal, a VPS, an old laptop. It provisions NixOS, connects to a WireGuard hub, and joins a K3s cluster. Install apps from a web UI: each one gets its own tunnel endpoint, subdomain, and TLS certificate. No port-forwarding, no DDNS, no reverse proxy config to maintain every time you add something new.
Add more machines with the same account token and they join the cluster automatically. Three nodes and the control plane is HA.
| App | Description | |
|---|---|---|
| 🐙 | Gitea | Self-hosted Git |
| 📸 | Immich | Photo and video backup |
| 🔐 | Vaultwarden | Bitwarden-compatible password manager |
| 🔑 | 2FAuth | Two-factor authentication manager |
| 🔔 | ntfy | Push notifications to any device |
| ⚡ | LibreSpeed | Self-hosted speed test |
| 💬 | Cinny | Matrix client |
| 📡 | Strfry | Nostr relay |
| ⛏️ | Minecraft | Java Edition server |
- A YoLab platform account — provides the WireGuard hub and DNS
- A machine to install on (bare-metal, VM, or VPS with KVM)
Download from Releases, or build it:
nix build .#nixosConfigurations.yolab-installer.config.system.build.isoImagesudo dd if=result/iso/*.iso of=/dev/sdX bs=4M status=progress && syncBoot from USB. The installer UI starts on tty1 and is also reachable as a web app on port 80.
Enter your account token, configure the disk, and let it run. When the machine reboots, the management UI is live at your node's tunnel address.
Go to Apps, pick something from the catalog, fill in the form, click Install.
Each app is a standard Helm chart under apps/catalog/:
apps/catalog/my-app/
Chart.yaml # name, version, display name, icon, category
values.yaml # defaults
values.schema.json # the install form AND what the app page shows
templates/ # the Kubernetes manifests, including the gateway
Chart.yaml carries three annotations: yolab.io/display-name, yolab.io/icon and
yolab.io/category. Everything else a developer controls lives in one standard JSON
Schema, values.schema.json, with two sections:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["config"],
"properties": {
"config": {
"type": "object",
"properties": {
"subdomain": { "type": "string", "format": "tunnel", "default": "my-app" },
"password": { "type": "string", "title": "Admin password", "writeOnly": true, "generate": true },
"tor_enabled": { "type": "boolean", "title": "Reachable over Tor", "default": false }
}
},
"outputs": {
"type": "object",
"readOnly": true,
"properties": {
"url": { "type": "string", "title": "Web URL", "format": "uri" },
"password": { "type": "string", "title": "Admin password", "format": "secret" },
"onion": { "type": "string", "title": "Tor address", "format": "uri" }
}
}
}
}config is the install form, rendered in the order written. format: "tunnel"
is the app's address (a chart with none registers no DNS name). writeOnly: true
marks a credential: masked in the form, never written to non-secret places, kept when
the app is duplicated. Add generate: true and YoLab fills it with a random value.
Fields revealed by a toggle (standard dependencies/oneOf) render under that toggle.
outputs is what the app page shows once it runs. Each output has only a title
and a format (text, uri, secret or multiline). Its value is whatever the app
prints, in any container's logs, init containers included, as one line:
YOLAB_OUTPUT <key> <value>
The value is the rest of the line. YoLab rescans every minute and keeps the latest value
found, so a value printed once is not lost when the logs roll over. An output appears on
the page once it has been printed, and not before. To take one away, for example the Tor
address after Tor is switched off, the chart prints YOLAB_OUTPUT <key> NONE. A
generated setting such as the admin password is printed the same way, from the Secret
that holds it (yolab-common.printOutputs).
Outputs are for people. No app reads another app's outputs.
Talking to another app. There are no links between installed apps. A setting that needs another app's address or key is a plain object with one box per value, filled in by the person:
"download_client": {
"type": "object",
"properties": {
"internal_url": { "type": "string", "format": "uri" },
"username": { "type": "string" },
"password": { "type": "string" }
}
}Apps that should always work together belong in one pack, which wires them itself.
Packs. A pack is a chart built from other catalog charts: every dependency that is
not a library (yolab-common, yolab-stack) is one of its apps, each pinned at its
current version. It installs, updates and uninstalls as one release. Each app chart keeps its manifests in
define "<chart>.manifests" and renders nothing itself when .Values.global.stack is
set; the pack renders each one through yolab-stack.member with its own settings,
under its own names (<release>-<app>), address (<subdomain>-<app>) and outputs
(<app>_<key>). The pack wires its apps together in its own template and shares one
folder between them. See apps/catalog/movies-tv.
apps/catalog/check_charts.py enforces all of this. No installer code changes are
needed for a new app.
nix develop
pre-commit run --all-filesA machine's own config.toml lives outside the repo, in /var/lib/yolab/machine,
and comes in as the yolab-machine flake input (see flake.nix). Never use
path:.: it copies the whole working directory into the store (8.6G against
4.8M) every time.
sudo nixos-rebuild switch --flake .#yolab \
--override-input yolab-machine path:/var/lib/yolab/machine --no-write-lock-fileEvery push lints, builds and pushes wg-sidecar and wg-register to ghcr.io/demycode/, and publishes an installer ISO as a GitHub release. Everything is tagged <branch>-latest.
GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later) — see LICENSE.
You may use, study, change and share YoLab. If you distribute a modified version, or let people use one over a network, you must publish its source under the same licence.