Reference · reference

Lab file: container and its children

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

This chapter is the field-by-field reference for the container {} block and the children only a container carries: env, volume, port and healthcheck. A container also takes login {}, nic {}, provision {} and playbook {}, which mean the same thing as on a VM and are documented in vm and its children. It may carry the @dev decorator described there too. What a container is, and how it runs as a micro-VM, is in Lab containers.

container {}

An OCI container run inside a micro-VM. It attaches to segments exactly like a VM, registers in DNS as <name>.<lab>.<suffix>, and joins the same depends_on waves. A container with no NICs is air-gapped but still reachable with exec and cp over the agent channel.

wcl
container "<name>" {
  image      = "nginx:1.27"
  mode       = :workload
  entrypoint = ["/bin/sh", "-c"]
  command    = ["…"]
  workdir    = "/app"
  user       = "1000:1000"
  profile    = "container"
  cpus       = 1
  memory     = 256MiB
  depends_on = ["db"]
  nic         { … }
  env         { … }
  volume      { … }
  port        { … }
  healthcheck { … }
  login "…"   { … }
  provision "…" { … }
  playbook "…"  { … }
}
FieldTypeDefaultMeaning
nameutf8 (label)requiredContainer name, a DNS label, unique per lab. VMs and containers share one namespace; the inline block label.
imageutf8requiredOCI image reference, for example nginx:1.27 or ghcr.io/owner/app@sha256:….
modesymbol:workload:workload starts the OCI process; :idle keeps the micro-VM available for exec without running it.
entrypointlist<utf8>image defaultOverride the image entrypoint, in exec form.
commandlist<utf8>image defaultOverride the image cmd, in exec form.
workdirutf8image defaultWorking directory inside the container.
userutf8image defaultUser to run as: uid[:gid] or a name from the image.
profileutf8noneGuest profile supplying micro-VM hardware defaults, for example container.
cpusi64from profilevCPU count for the micro-VM, greater than 0. One of this field or the profile must supply it.
memoryByteSizefrom profileRAM for the micro-VM, for example 512MiB. One of this field or the profile must supply it.
depends_onlist<utf8>noneVM or container names to wait for before this one. No cycles.
nic {}childrennoneNetwork interfaces. None means air-gapped; exec and copy still work via the agent.
env {}childrennoneEnvironment variables passed to the container process.
volume {}childrennoneHost binds and named volumes mounted into the container.
port {}childrennoneHost-to-container port forwards; sugar for a segment forward to this container.
healthcheck {}childnoneHealth probe gating readiness. Without one the container is ready once its process starts.
login {}childrennoneIdentities a person's commands run as on this container. Without one it falls to the user cinit resolves.
provision {}childrennonewscript scripts run on vmlab up once this container is ready, interleaved with its playbooks in declaration order.
playbook {}childrennoneconfig-weave playbooks applied on vmlab up, interleaved with its provisions in declaration order.

The image reference is parsed like a registry template reference: a first path segment with a dot, a colon or localhost is a registry host, and Docker Hub shorthand normalises to registry-1.docker.io. A digest must be @sha256: followed by 64 hex characters. The digest resolved at first pull is pinned in lab state and never re-pulled implicitly.

Validation enforces these rules:

examples/mixed-lab/vmlab.wclwcl
container "web" {
  image = "nginx:1.27"
  # The micro-VM's size comes from the profile; declare `cpus`/`memory`
  # here to override it. One of the two must supply them.
  profile = "container"
  depends_on = ["nix01"]
  nic {
    segment = "lan"
  }
  port {
    host = 18081
    container = 80
  }  # host → container nginx
  healthcheck {
    command = ["curl", "-fsS", "http://localhost/"]
    interval = 5s
  }
}

A dev container uses :idle mode, since it has no service to be, and a login for the account vmlab shell lands as.

examples/dev-container/vmlab.wclwcl
@dev(default = true, workspace = "./workspace")
container "dev01" {
  image   = "alpine:3.22"
  profile = "container"
  // A dev machine builds things, and a container names its own size when
  // the profile's floor is not the right one.
  cpus    = 2
  memory  = 1GiB
  // `:idle` keeps the micro-VM up without running the image's entrypoint
  // — a dev container has no service to be.
  mode    = :idle
  nic { segment = "lan" }

  // The container identity floor (§19.2): the agent is root and root needs
  // no credential to become an account, so a Linux `login {}` may declare
  // the account alone. `elevated` is a validation error on this side.
  login "dev" { user = "dev" default = true }

  provision "scripts/dev-user.ws" { }
  provision "scripts/home-bits.ws" { }
}

env {}

One environment variable passed to the container process.

wcl
env { name = "NGINX_PORT" value = "8080" }
FieldTypeDefaultMeaning
nameutf8requiredVariable name.
valueutf8requiredVariable value.

A name that is empty or contains = is rejected.

volume {}

A mount into the container: a host directory bound by path, or a named volume kept under the lab directory. Exactly one of host and name is set.

wcl
volume { host = "./site" target = "/usr/share/nginx/html" read_only = true }
volume { name = "pgdata" target = "/var/lib/postgresql/data" }
FieldTypeDefaultMeaning
hostutf8noneHost path to bind-mount, relative to the lab root. One of host or name is required.
nameutf8noneNamed volume kept under the lab dir, shared by name, retained until lab destroy. One of host or name.
targetutf8requiredAbsolute mount path inside the container.
read_onlyboolfalseMount read-only.

Validation rejects a volume with both host and name, or neither. A host path must be a directory under the lab root. A name becomes a directory name, so it cannot be empty, ., .., or contain a slash. target must start with /. Named volumes are lab-scoped: they survive down and a per-container destroy, and only lab destroy removes them. Volume contents are outside snapshot scope.

port {}

A host-to-container port forward. It compiles into the same forward machinery as a segment forward {} and is installed against the container's lease when it becomes ready.

wcl
port { host = 18081 container = 80 proto = "tcp" }
FieldTypeDefaultMeaning
hosti64requiredHost port to listen on, 1 to 65535. Unique across the lab.
containeri64requiredContainer port to forward to, 1 to 65535.
protoutf8tcpProtocol: tcp, udp or both.

The host port must be unused by every other port {} and every segment forward {} in the lab, and the container needs a NIC.

healthcheck {}

A probe run inside the container. Readiness is two-stage: the process starts, then the first probe passes. Only then does the container count as ready for depends_on waves. Consecutive failures past retries fire container.unhealthy.

wcl
healthcheck {
  command      = ["curl", "-fsS", "http://localhost/"]
  interval     = 10s
  timeout      = 5s
  retries      = 3
  start_period = 10s
}
FieldTypeDefaultMeaning
commandlist<utf8>requiredProbe command run inside the container, in exec form. Exit 0 means healthy.
intervalDuration10sTime between probes.
timeoutDuration5sPer-probe timeout.
retriesi643Consecutive failures before unhealthy.
start_periodDuration10sGrace period after start before failures count.

Validation requires a non-empty command, interval and timeout greater than zero, retries at least 1, and a non-negative start_period. An :idle container cannot declare one.