Your first ZTD VM

By the end of this tutorial you will have booted an isolated Debian VM with its own Docker daemon, opened a shell inside it, reached a service running in it from your host, and destroyed it cleanly — all driven through the single ./ztd interface. Allow about 15 minutes the first time (provider downloads and the base image are cached afterward).

Before you start

You need a Linux host with:

  • Docker installed and running.
  • Hardware virtualization (/dev/kvm present).

You do not need Terraform, Task, or any language runtime on the host — those live in the toolbox container.

Step 1 — Install the hypervisor

The local backend boots the VM with the host’s KVM, so libvirt/qemu must be present. On Arch:

sudo pacman -S --needed qemu-full libvirt dnsmasq
sudo systemctl enable --now libvirtd
sudo usermod -aG libvirt "$USER"

Log out and back in so your shell picks up the libvirt group, then confirm:

systemctl is-active libvirtd     # active
ls /dev/kvm                       # exists

Step 2 — Initialize ZTD

cd ztd
./ztd init

The first run builds the toolbox image (Terraform + Task + libvirt client libraries) and downloads the Terraform providers into .terraform/. You only do this once.

Step 3 — Boot the VM

./ztd up

ZTD fetches the Debian 12 cloud image, creates a copy-on-write disk, renders a cloud-init that installs Docker, Node, and the agent runner, and boots the domain. When it finishes it prints the VM’s IP.

Behind the scenes this is Terraform applying the kvm module; ZTD just wraps it so you don’t think about it.

Step 4 — Look inside

./ztd ssh

You’re now in the guest. Verify it has its own Docker, separate from your host’s:

docker info | grep -i 'name\|root dir'

Start something that listens, for example:

docker run -d --rm -p 8080:80 nginx

Leave it running and exit the shell (exit).

Step 5 — Reach it from the host

./ztd tunnel -- 8080

Open http://localhost:8080 — you’re hitting nginx inside the VM through an SSH tunnel. Stop the tunnel with Ctrl-C when done.

Step 6 — Destroy it

./ztd down

The VM and its disk are gone. Because ZTD treats VMs as cattle, this is the expected end state — anything worth keeping would have been pushed to git first.

What you learned

  • The host only ever needed Docker plus a hypervisor; all tooling was containerized.
  • The guest is fully isolated, with its own Docker daemon.
  • The entire lifecycle is one verb at a time: init, up, ssh, tunnel, down.

Where to go next