Getting started · tutorial

Your first lab

8 min read · 2026-09-02 · vmlab 0.9

In this tutorial you write a lab file with one segment and one container running nginx, bring it up, reach it from the host through a port forward, run a command inside it, and take it down again. Along the way you meet every lifecycle verb: validate, up, status, down and destroy.

A container is the quickest machine to start with because there is nothing to build. vmlab pulls the image like a docker client would and runs it inside a small micro-VM with vmlab's own init.

Before you start

Write the lab file

Create an empty directory and put a file named vmlab.wcl in it. vmlab finds the lab file by walking up from the current directory, the way git finds .git, so every command in this tutorial runs from inside that directory.

vmlab.wclwcl
import <vmlab.wcl>

lab "first-lab" {

  segment "lan" {
    subnet = "10.90.0.0/24"
    nat    = true
  }

  container "web" {
    image   = "nginx:1.27"
    profile = "container"
    nic { segment = "lan" }
    port { host = 18081 container = 80 }
    healthcheck {
      command  = ["curl", "-fsS", "http://localhost/"]
      interval = 5s
    }
  }
}

Each line does one thing.

Every field of these blocks, with its type and default, is in Lab file: lab and segment and Lab file: container and its children.

Validate it

vmlab validate parses the file, checks it against the schema, and runs the semantic checks: the segment the NIC names exists, the port is unique across the lab, the container has a size. It starts nothing and writes nothing.

sh
vmlab validate

A clean run prints one line, `ok: lab "first-lab" — 0 vm(s), 1 container(s), 1 segment(s)`, and exits 0. An error names the file, the line and what is wrong. Try misspelling segment as segmnet inside the nic block and run it again to see the shape of a schema error, then put it back.

The lab file validates

vmlab validate exits 0 with no errors.

Bring it up

sh
vmlab up

The first up starts the supervisor daemon, vmlabd, if it is not already running, starts a lab daemon for this lab, pulls the nginx image, flattens it, boots the micro-VM, and waits for the healthcheck to pass. Progress streams to the terminal. The pull happens once; the flattened image is cached by digest and later runs skip it.

up returns when every machine is ready and every provision script has finished. This lab has no provision scripts, so it returns as soon as the healthcheck passes. The lab keeps running after the command exits. It is owned by the lab daemon, not by your terminal.

Look at it

sh
vmlab status

status prints one row per machine with what it is doing and its IP address, and one row per segment. Add -v for the raw power state, the readiness flag and the container's image, health and last exit code.

sh
vmlab status -v

Two more verbs answer narrower questions. The container's address on its own, and the log of its console, which carries the kernel messages and the process's stdout and stderr.

sh
vmlab container ip web
vmlab container logs web -n 50

The lab is up

vmlab status shows web running and ready with an address in 10.90.0.0/24.

Reach it from the host

The port {} block made the host listen on 18081 and forward to the container. Fetch the default nginx page.

sh
curl http://localhost:18081/

The response is nginx's welcome page. The forward is served by the lab daemon's userspace network stack, so nothing on the host was configured to make this work: no iptables rule, no bridge, no capability.

Run a command inside it

Commands reach the guest over the agent's virtio-serial channel, not over the network. vmlab container exec runs one command and mirrors its output and exit code. Everything after -- is the command.

sh
vmlab container exec web -- nginx -v

vmlab exec does the same thing and accepts a container name as well as a VM name. The two verbs share one implementation. Both wait up to 120 seconds by default; --timeout <SECS> changes that.

sh
vmlab exec web -- cat /etc/os-release

For an interactive session, attach a shell. Press Ctrl-] to detach and leave the container running.

sh
vmlab container shell web

With no login {} block on the machine, exec and shell run as the agent's own identity, which is root in a container. Logins explains how to declare the account a person's commands run as.

Take it down

sh
vmlab down

down stops every machine gracefully and keeps their state. For a container that means its scratch disk survives, so files you wrote inside it are still there after the next up. The lab daemon exits. `vmlab status from this directory now prints lab "first-lab": not running`.

Bring it back up to confirm the state is intact, then take it down again.

sh
vmlab up
curl http://localhost:18081/
vmlab down

The second up is faster. The image is cached and there is no healthcheck history to wait for beyond the first pass.

Destroy it

sh
vmlab destroy

destroy stops the lab if it is running and then deletes everything vmlab created for it: the container's scratch state, the lab-local .vmlab/ directory beside the lab file, and any dynamically added network rules. The lab file itself and the cached image are untouched. The next up starts from a fresh container.

destroy is not undoable

Anything written inside a machine's disk is gone after destroy. Keep data you care about on the host, in a share {} or a volume {}, or copy it out with vmlab cp first.

You have completed the lifecycle

The directory holds only vmlab.wcl. vmlab lab list no longer shows first-lab.

Next steps