Getting started · tutorial

Your first VM

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

In this tutorial you run a full virtual machine without building anything. The VM's template field names a template published on a public OCI registry, and vmlab up pulls it into your local store the first time. You then open a shell in the guest, attach its console, take a screenshot, and take and restore a snapshot.

Before you start

Write the lab file

Create a new directory with this vmlab.wcl.

vmlab.wclwcl
import <vmlab.wcl>

lab "first-vm" {

  vm "alp" {
    template = "ghcr.io/vmlabdev/vmlab-templates/alpine-3.23"
    arch     = "x86_64"
    memory   = 1GiB
    nic { nat = true }
  }
}

template is an OCI registry reference of the form host/owner/[group/]name[:tag]. With no tag it tracks the moving latest tag, the newest stable version of the published template. :latest-prerelease tracks pre-releases and :<version> pins one. A registry reference always needs arch, because one tag can carry several architectures and vmlab never assumes the host's.

nic { nat = true } is shorthand for attaching the VM to the lab's built-in NAT segment, so you do not have to declare one. The guest gets a DHCP lease and internet egress. memory = 1GiB overrides the size the template recorded; cpus and everything else are inherited from the template and, below it, from its guest OS profile. See Lab file: vm and its children for the whole surface.

Bring it up

sh
vmlab validate
vmlab up

up resolves the tag, finds no matching version in the local store, pulls the template, installs it under x86_64/alpine-3.23@<version>, and creates the VM as a linked clone of it. The clone is a qcow2 file whose backing file is the sealed template, so it starts small and the template is never modified. The VM boots and up waits until the guest agent answers, which is what ready means.

A cached version is reused on every later up and never re-pulled implicitly. To fetch a template without starting anything, for instance ahead of a trip, run vmlab pull. To see what the store holds, list it.

sh
vmlab status
vmlab template list

The VM is running

vmlab status shows alp running and ready with an address. vmlab template list shows x86_64/alpine-3.23 with the version that was pulled.

Open a shell

vmlab shell attaches an interactive terminal inside the guest over the agent's virtio-serial channel. No SSH and no guest network are involved, so it works even on a VM with no NIC. This VM declares no login {}, so the shell runs as the agent's identity, which is root on Linux.

sh
vmlab shell alp

Inside the guest, confirm where you are, then detach with Ctrl-]. Detaching leaves the VM running.

sh
cat /etc/alpine-release
ip addr show eth0

For a single command, use exec instead. Its exit code becomes the exit code of vmlab exec.

sh
vmlab exec alp -- uname -a

Attach the console

Every VM has a VNC display served on a unix socket whether or not anyone is looking at it. vmlab console attaches a viewer to it. vmlab picks the viewer from your host configuration, else the first of remote-viewer, gvncviewer and vncviewer on your PATH. Closing the viewer window only disconnects. The VM keeps running.

sh
vmlab console alp

You see the Alpine login prompt on the virtual screen. With no viewer installed, or on WSL 2 where the viewer lives on the Windows side, ask for a TCP bridge instead. vmlab forwards the display to a localhost port and prints the address for you to point any VNC client at.

sh
vmlab console --tcp alp

To open a viewer automatically on every up, set gui = true on the VM or on the lab. The VM still runs headless; the viewer is a separate client process.

Take a screenshot

The same display that the console shows can be captured to a PNG at any moment. This is the basis of screen-driven automation, where a script waits for an image or a piece of text to appear.

sh
vmlab vm screenshot alp login.png

Open login.png and you see the same login prompt. With tesseract installed, vmlab vm ocr alp reads the text off the screen instead, and vmlab vm sendkeys alp <chord> types into it. Screens, input and vision covers the whole surface.

You have three doors into the guest

You have run a command over the agent, watched the screen over VNC, and captured it to a file.

Snapshot and restore

A snapshot records a machine's disk, and if the machine is running, its RAM and device state too. Every snapshot remembers the power state it was taken in. Restoring an online snapshot resumes the guest exactly where it was; restoring an offline one leaves it powered off. Write a marker file in the guest, take a snapshot, delete the marker, then restore.

sh
vmlab exec alp -- sh -c 'echo before > /root/marker'
vmlab snapshot create clean --vm alp
vmlab snapshot list alp

--vm alp narrows the snapshot to one machine. Without it, `snapshot create` captures every VM and container in the lab under one name. Consistency across machines in a lab-wide snapshot is best-effort, not coordinated. Now change the guest and roll it back.

sh
vmlab exec alp -- rm /root/marker
vmlab snapshot restore clean --vm alp
vmlab exec alp -- cat /root/marker

The last command prints before. The VM was running when the snapshot was taken, so it is running again after the restore, with the marker back. Delete the snapshot when you no longer need it.

sh
vmlab snapshot delete alp clean

Snapshots are not a workspace backup

On a dev machine the workspace is re-converged from the host after a restore rather than rolled back with the disk. The source of truth for a workspace is the host directory. See Dev machines and the workspace syncer.

Clean up

sh
vmlab down
vmlab destroy

destroy deletes the clone and the lab-local state. The pulled template stays in the store, so the next up of any lab that names it starts without a download. To remove it, find its version with `vmlab template list` and remove that reference.

sh
vmlab template rm x86_64/alpine-3.23@<version>

Next steps