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
FieldTypeRequiredNotes
kindstringyesMust be playground.
namestringyesHostname-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.
basestringcreate-timeEvery 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).
titlestringyesDisplay title (5-120 characters).
descriptionstringnoShort summary (up to 500 characters).
categorieslist of stringsno (create)Catalog categories, e.g. linux, containers, kubernetes. Inherited from the base on create; required on update.
markdownstringnoLanding-page body (up to 100,000 characters).
coverstringnoCover image URL; in practice managed via the UI's /settings page.
playgroundobjectyesThe technical spec - detailed in the following units.

The two commands that consume manifests have different expectations:

  • create -f accepts a partial spec: networks, tabs, categories, and resources are inherited from the base when omitted. Still required: title, a non-empty accessControl (all three lists), and for every machine - name, users, drives, and network.
  • update -f expects a complete manifest: networks, tabs, categories, and accessControl must all be present. The reliable workflow is to dump the current manifest with labctl 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
FieldTypeDefaultNotes
namestringrequiredHostname-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).
userslistrequiredAt least one user; see below.
backendstringfirecrackerfirecracker or cloud-hypervisor; the latter enables nested virtualization (paid feature).
kernel.sourcestring6.1Kernel version to boot, e.g. 5.10, 6.1, 6.12, 6.18.
driveslistrequiredSee below.
network.interfaceslistrequiredAt least one interface; see Networks.
resourcesobjectauto-computedSee below.
startupFileslist-See below.
noSSHboolfalseDisables SSH access and hides the machine's terminal tab.

Users

FieldTypeDefaultNotes
namestringrequiredMust exist in the rootfs image (root always does; official images ship laborant, except Alpine).
defaultboolfirst listed userThe user that terminals and labctl ssh log in as.
welcomestringimage defaultLogin banner; '-' disables it.

root is always available, even if not listed.

Drives

FieldTypeDefaultNotes
sourcestring- (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.
mountstring- (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.
sizestringbase/platform default1GiB to 100GiB per drive; at most 240GiB total per playground.
filesystemstringext4 (when formatting applies)ext4, ext2, ext3, xfs, or btrfs. A source-less drive with no mount and no filesystem stays raw/unformatted.
readOnlyboolfalseAttach 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

FieldTypeDefaultNotes
cpuCountintautoNumber of vCPUs (min 1).
ramSizestringautoHuman-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

FieldTypeDefaultNotes
pathstringrequiredAbsolute path; parent directories are created.
contentstringrequiredFile content.
appendboolfalseAppend instead of overwrite.
ownerstring0:0user, user:group, or numeric UID[:GID].
modestring"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

FieldTypeDefaultNotes
namestringrequiredHostname-like, unique within the playground.
subnetstringrequiredIPv4 CIDR, e.g. 172.16.0.0/24.
gatewaystringfirst free IP in the subnetPlain IPv4 address (no mask).
privateboolfalsetrue = 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

FieldTypeDefaultNotes
networkstringrequired on the first interfaceThe network to join.
addressstringfirst free IP in the subnetStatic 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
FieldTypeApplies toNotes
kindstringallterminal (default), ide, http-port, web-page, kexp.
machinestringterminal, ide, http-portTarget machine; defaults to the first machine.
namestringallTab label; required for http-port and web-page.
numberinthttp-portThe port to render; the app must listen on the machine's main interface or 0.0.0.0 (see Expose HTTP Ports).
tlsboolhttp-portSet true when the in-VM server speaks HTTPS.
urlstringweb-pageThe external page to embed.
idstringallAuto-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
FieldTypeDefaultNotes
initboolfalseMust be true for playground init tasks (they run once, at startup, before the playground is handed to the user).
machinestringrequiredThe VM to run on.
userstringrootThe user to run the script as.
runstringrequiredThe shell script (executed with bash).
needslist of strings-Task names that must complete first; cycles are rejected.
timeout_secondsint60Increase for anything network- or package-manager-bound.
conditionslist 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://.*$"
FieldTypeNotes
keystringThe parameter name referenced by tasks' conditions.
defaultstringPre-selected value.
optionslist of stringsRenders as a dropdown when set.
nullableboolAllows an empty value.
placeholderstringInput hint in the UI.
validationRegexstringClient-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
FieldMeaning
canListWho sees the playground in catalogs and search.
canReadWho can open the playground's landing page.
canStartWho 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.