Playground Manifest Reference
Manifest structure
A playground manifest is a single YAML document. At the top level:
kind: playground # always "playground"
name: my-playground # unique name (also the URL slug)
base: flexbox # the base playground (informational in dumps;
# on create, the base is set via --base)
title: My Playground
description: A one-paragraph summary shown on the playground card.
categories:
- linux
markdown: |
The long-form landing page body (markdown).
cover: https://... # cover image URL (uploaded via the UI)
playground:
networks: [...] # see Networks
machines: [...] # see Machines
tabs: [...] # see Tabs
initTasks: {...} # see Init tasks
initConditions: {...} # see Init tasks
registryAuth: user:pass # see Access control & registry
accessControl: {...} # see Access control & registry
| Field | Type | Required | Notes |
|---|---|---|---|
kind | string | yes | Must be playground. |
name | string | yes | Hostname-like identifier. On create, the platform appends a unique suffix (e.g. my-playground becomes my-playground-51c9d61a) - the full name is printed by create and is what all other commands expect. |
base | string | create-time | Every custom playground derives from a base; list bases with labctl playground catalog --filter base. Only flexbox allows an arbitrary machine set; other bases keep their original machines (subsets and tweaks allowed, new/renamed machines rejected). |
title | string | yes | Display title (5-120 characters). |
description | string | no | Short summary (up to 500 characters). |
categories | list of strings | no (create) | Catalog categories, e.g. linux, containers, kubernetes. Inherited from the base on create; required on update. |
markdown | string | no | Landing-page body (up to 100,000 characters). |
cover | string | no | Cover image URL; in practice managed via the UI's /settings page. |
playground | object | yes | The technical spec - detailed in the following units. |
The two commands that consume manifests have different expectations:
create -faccepts a partial spec: networks, tabs, categories, and resources are inherited from the base when omitted. Still required:title, a non-emptyaccessControl(all three lists), and for every machine -name,users,drives, andnetwork.update -fexpects a complete manifest:networks,tabs,categories, andaccessControlmust all be present. The reliable workflow is to dump the current manifest withlabctl playground manifest, edit it, and submit the result.
Working with manifests
# List available bases and your own playgrounds:
labctl playground catalog --filter base
labctl playground catalog --filter my-custom
# Create (--base is mandatory; -f is optional - without it you get a clone of the base):
labctl playground create <name> --base <base> [-f manifest.yaml]
# Dump the current (effective) manifest of a playground:
labctl playground manifest <name>
# Apply an updated manifest:
labctl playground update <name> -f manifest.yaml
# Start / stop / remove:
labctl playground start <name> [--open] [--ide] [--ssh] [-i key=value]
labctl playground stop <playground-instance-id>
labctl playground remove <name>
Both create and update accept -f - to read the manifest from stdin - convenient for heredocs and pipelines.
Machines
playground.machines is a list of VM definitions (1 to 5 machines per playground):
machines:
- name: dev-01
backend: firecracker
kernel:
source: "6.1"
users:
- name: laborant
default: true
welcome: |
Hello there!
drives:
- source: ubuntu-24-04
mount: /
size: 10GiB
network:
interfaces:
- network: local
resources:
cpuCount: 2
ramSize: 2GiB
startupFiles:
- path: /home/laborant/.bashrc
append: true
content: |
alias k=kubectl
noSSH: false
| Field | Type | Default | Notes |
|---|---|---|---|
name | string | required | Hostname-like; also the machine's hostname and DNS name inside the playground. With any base other than flexbox, machine names must match the base's machines (subsets allowed, new names rejected). |
users | list | required | At least one user; see below. |
backend | string | firecracker | firecracker or cloud-hypervisor; the latter enables nested virtualization (paid feature). |
kernel.source | string | 6.1 | Kernel version to boot, e.g. 5.10, 6.1, 6.12, 6.18. |
drives | list | required | See below. |
network.interfaces | list | required | At least one interface; see Networks. |
resources | object | auto-computed | See below. |
startupFiles | list | - | See below. |
noSSH | bool | false | Disables SSH access and hides the machine's terminal tab. |
Users
| Field | Type | Default | Notes |
|---|---|---|---|
name | string | required | Must exist in the rootfs image (root always does; official images ship laborant, except Alpine). |
default | bool | first listed user | The user that terminals and labctl ssh log in as. |
welcome | string | image default | Login banner; '-' disables it. |
root is always available, even if not listed.
Drives
| Field | Type | Default | Notes |
|---|---|---|---|
source | string | - (empty drive) | A named rootfs image (ubuntu-24-04, docker, ...) or an OCI reference oci://ghcr.io/user/image:tag (Docker Hub not supported). Required for the root drive only. |
mount | string | - (not mounted) | Mount point inside the VM. Exactly one drive per machine must have mount: /. Non-root drives with a mount are auto-formatted and auto-mounted. |
size | string | base/platform default | 1GiB to 100GiB per drive; at most 240GiB total per playground. |
filesystem | string | ext4 (when formatting applies) | ext4, ext2, ext3, xfs, or btrfs. A source-less drive with no mount and no filesystem stays raw/unformatted. |
readOnly | bool | false | Attach the drive read-only. |
Drives map to devices in list order: /dev/vda, /dev/vdb, ... Up to 24 drives per machine.
(You may also see source: snapshot with a snapshot object in dumped manifests - that's how playgrounds saved from a stopped instance reference their drive snapshots; these entries are platform-generated, not hand-written.)
Resources
| Field | Type | Default | Notes |
|---|---|---|---|
cpuCount | int | auto | Number of vCPUs (min 1). |
ramSize | string | auto | Human-readable size, e.g. 512MiB, 2GiB. |
Per-VM and per-playground totals are capped by your plan (free tier: 2 vCPU / 4 GiB per VM, 5 vCPU / 8 GiB per playground; paid plans: 4 vCPU / 10 GiB per VM, 10 vCPU / 16 GiB per playground). Requests exceeding the budget are scaled down proportionally rather than rejected.
Startup files
| Field | Type | Default | Notes |
|---|---|---|---|
path | string | required | Absolute path; parent directories are created. |
content | string | required | File content. |
append | bool | false | Append instead of overwrite. |
owner | string | 0:0 | user, user:group, or numeric UID[:GID]. |
mode | string | "644" | Octal permissions, without the leading zero (e.g. "600", not "0600"). |
Up to 10 startup files per machine.
Networks
playground.networks declares the isolated bridge networks of the playground; machines join them via network.interfaces:
networks:
- name: frontend
subnet: 10.0.1.0/24
- name: backend
subnet: 10.0.2.0/24
gateway: 10.0.2.254
private: true
machines:
- name: web-01
network:
interfaces:
- network: frontend
address: 10.0.1.10
- network: backend # address auto-assigned
Network fields
| Field | Type | Default | Notes |
|---|---|---|---|
name | string | required | Hostname-like, unique within the playground. |
subnet | string | required | IPv4 CIDR, e.g. 172.16.0.0/24. |
gateway | string | first free IP in the subnet | Plain IPv4 address (no mask). |
private | bool | false | true = no NAT to the internet and no default route via this network. |
A playground has at least one network; when the manifest defines none, the base's default network is used (conventionally local, 172.16.0.0/24).
Networks are isolated from each other - inter-network traffic flows only through machines attached to both.
Interface fields
| Field | Type | Default | Notes |
|---|---|---|---|
network | string | required on the first interface | The network to join. |
address | string | first free IP in the subnet | Static IPv4, with or without a mask (10.0.1.10 or 10.0.1.10/24; the mask defaults to the subnet's). |
Interfaces appear in the VM in list order as eth0, eth1, ...
The machine's default route goes via the first non-private network among its interfaces.
Machines resolve each other by machine name and by <machine>.<network> names.
Tabs
playground.tabs defines the panes of the playground page (1-10 entries). Omitting it yields the defaults (an IDE tab for most base playgrounds + a terminal per SSH-enabled machine, plus the Kubernetes Explorer for Kubernetes playgrounds); defining it replaces the defaults entirely:
tabs:
- kind: ide
- kind: http-port
name: Web UI
machine: dev-01
number: 8080
- kind: terminal
machine: dev-01
- kind: web-page
name: Docs
url: https://example.com/docs
| Field | Type | Applies to | Notes |
|---|---|---|---|
kind | string | all | terminal (default), ide, http-port, web-page, kexp. |
machine | string | terminal, ide, http-port | Target machine; defaults to the first machine. |
name | string | all | Tab label; required for http-port and web-page. |
number | int | http-port | The port to render; the app must listen on the machine's main interface or 0.0.0.0 (see Expose HTTP Ports). |
tls | bool | http-port | Set true when the in-VM server speaks HTTPS. |
url | string | web-page | The external page to embed. |
id | string | all | Auto-generated (<kind>-<machine>); set explicitly only to disambiguate multiple tabs of the same kind on one machine. |
A bare - machine: <name> entry is shorthand for a terminal tab on that machine.
Init tasks and conditions
playground.initTasks is a map of named provisioning tasks executed inside the machines during startup
(see the init tasks lesson for a guided tour):
initTasks:
init_install_tools:
init: true
machine: dev-01
user: root
timeout_seconds: 120
run: |
apt-get update && apt-get install -y jq
init_seed_data:
init: true
machine: dev-01
user: laborant
needs:
- init_install_tools
run: |
mkdir -p ~/data && echo hello > ~/data/seed.txt
| Field | Type | Default | Notes |
|---|---|---|---|
init | bool | false | Must be true for playground init tasks (they run once, at startup, before the playground is handed to the user). |
machine | string | required | The VM to run on. |
user | string | root | The user to run the script as. |
run | string | required | The shell script (executed with bash). |
needs | list of strings | - | Task names that must complete first; cycles are rejected. |
timeout_seconds | int | 60 | Increase for anything network- or package-manager-bound. |
conditions | list of {key, value} | - | Run the task only when the given init-condition values were selected. |
Init conditions
playground.initConditions declares start-time parameters that users provide in the UI or via labctl playground start -i key=value:
initConditions:
values:
- key: k8s_flavor
default: k3s
options: [k3s, kubeadm]
- key: repo_url
default: ""
nullable: true
placeholder: https://github.com/you/repo
validationRegex: "^https://.*$"
| Field | Type | Notes |
|---|---|---|
key | string | The parameter name referenced by tasks' conditions. |
default | string | Pre-selected value. |
options | list of strings | Renders as a dropdown when set. |
nullable | bool | Allows an empty value. |
placeholder | string | Input hint in the UI. |
validationRegex | string | Client-side validation for free-form values. |
Access control and registry
Access control
playground.accessControl holds three lists of principals (see Sharing and Access Control for recipes):
accessControl:
canList:
- anyone
canRead:
- anyone
canStart:
- owner
| Field | Meaning |
|---|---|
canList | Who sees the playground in catalogs and search. |
canRead | Who can open the playground's landing page. |
canStart | Who can start an instance. |
Principals include owner, anyone, authenticated, user:<...>, and student:<training-name>; the full vocabulary is documented in
How to Control Access to Your Content.
When a manifest is submitted via labctl, all three lists must be present and non-empty;
playgrounds cloned without a manifest inherit the access settings of their base (official bases are public).
Older manifests may contain a deprecated access: {mode: private|public} block - labctl transparently converts it to the equivalent accessControl on update.
Registry auth
Every playground comes with a built-in container registry reachable from inside the VMs at registry.iximiuz.com.
By default it's unauthenticated (but only accessible from within the playground); registryAuth puts it behind credentials:
registryAuth: someuser:somepassword
This is handy for practicing docker login flows and private-registry scenarios without leaving the sandbox.
- Previous
- Sharing and Access Control