Until today Labtris asked for a machine. The shell installer wants Ubuntu 24.04 and takes it over — Postgres, a service user, systemd units, nginx. The ISO wants the whole disk. Both are fine when the box exists to run Labtris, and awkward when it does not.
There is now a third way in, and it is one command:
curl -fsSL https://labtris.com/compose | docker compose -f - up -dNo clone and no distribution requirement — Docker and a working /dev/kvm are the only two things it asks for. (The command originally fetched the file from the repository; since the repository went private in October 2026 the site serves it.)
The host keeps its network stack
The obvious way to containerise Labtris is network_mode: host. It works, and it also puts lab bridges, nftables rules and sysctl changes on whatever machine you installed it on. That is not a reasonable thing for a container install to do to its host.
So every Labtris process shares one private namespace instead, the way containers in a Kubernetes pod do. An idle container owns it; the API, netd, guacd and a private Docker daemon all join it. Lab bridges, taps, veths and NAT live in there. Run ip link on the host while a lab is running and you will see nothing new — which is the point, and also means you need docker compose exec netd ip link to look at a lab’s wiring.
Container nodes get their own dockerd rather than a bind-mounted /var/run/docker.sock. The host socket would have put guest containers in host-side namespaces where netd cannot wire them, left them behind on compose down, and handed root on the host to anything that could reach the API.
Four bugs that only appeared when we ran a lab
The compose file existed before today and had never run a lab. Every one of these was found by trying.
- The image had no web UI. The API resolved the built SPA to a directory that does not exist in an installed wheel, and the mount was guarded by an
is_dir()check — so every page 404’d with nothing in the log to say why. - A NAT network could not come up. No
nftfor the masquerade and nodnsmasqfor DHCP, so the path a lab node uses to reach the internet did not exist at all. - Every image went to
/root. The settings derive their paths from$HOMEand nothing set them, so downloaded base images, node disks and the session secret all landed in the container’s writable layer — discarded by the nextdocker pull. - Deleting a lab leaked its DHCP server. This one is not Docker-specific and affects the ISO too: deleting a network tore down its dnsmasq, but deleting the whole lab never did. The process outlived the lab holding the gateway address, and since the deleted row freed that subnet for reuse, the next NAT network failed with
Address already in use.
The last one is the interesting failure. Two code paths that do the same job had quietly diverged, and the symptom appeared one step away from the cause — a confusing error on the next lab rather than an error when the leak happened.
What it cannot do
Two features need the host’s own network stack, so they return HTTP 501 with an explanation rather than half-working: cloud networks, which put a lab on a physical segment by enslaving a real NIC, and the management-network settings, which rewrite the host’s netplan. If a lab genuinely has to sit on a physical segment, the shell installer or the ISO is still the answer.
Docker Desktop on macOS and Windows is not a supported path either. There is no /dev/kvm to hand it, and the “host” whose namespace matters would be Docker’s own Linux VM rather than your machine. Run a Linux VM instead.
Try it
The install guide has the command and the caveats; the Docker reference covers how images get in and what each container is for. There is an end-to-end check in the repo at packaging/docker-e2e.sh that brings up container nodes, a NAT node that reaches the internet, and a QEMU node under KVM, then asserts that no lab device leaked to the host namespace.