Architecture

Two principles shape ZTD: one interface across every backend, and all orchestration tooling in Docker.

One interface, many backends

  ztd CLI (Go / Cobra)          ← the single binary; assets embedded
        │
        ├── host lane ─────────────────────────► vz  (macOS)
        │   (bash, on the host)                  lima / Virtualization.framework
        │
        └── container lane
              │ Taskfile → Terraform root  ← runs inside the toolbox container
                            │ selects ONE backend module by TARGET
              ┌─────────────┼───────────────────────┐
          kvm-local      proxmox                    ec2
        (libvirt)      (bpg/proxmox)          (aws provider)
              └─────────────┴───────────────────────┘
                            │ all four consume the SAME
                       cloud-init  (Docker + Node + agent + creds)

The CLI routes each verb down one of two lanes. Most dispatch into the toolbox container, which runs Task and Terraform; the host lane runs bash directly on the host for the verbs that inspect or mutate host state (check, setup, the agent workflow) and for the whole vz backend, which has no usable Terraform provider and drives Apple’s hypervisor from the host.

Terraform’s provider swap is hidden inside per-backend modules, selected by a single target variable (count = var.target == … ? 1 : 0). You never juggle two tools or two configs — the same ./ztd up works everywhere, and the same cloud-init provisions every guest. This was a deliberate choice over, say, Vagrant-for-local plus Terraform-for-remote.

Tooling in Docker

Everything except the hypervisor runs in a toolbox container (Terraform, providers, Task — and eventually the ztd CLI image). The host stays clean:

BackendHost needs
kvm (local Linux)Docker + libvirt/qemu
vz (local macOS)Docker + Virtualization.framework
proxmox / ec2 (remote)Docker only

The toolbox reaches the local hypervisor over the libvirt socket using host networking. A fully self-contained variant — libvirtd + qemu inside a privileged container, using only the host’s /dev/kvm — is possible later; v1 keeps the hypervisor host-side for reliability.

Dual Docker

There are two independent Docker daemons, and that’s the point:

HOST ── Docker (toolbox)  +  libvirt/qemu (hypervisor)
  └─ VM ── its OWN dockerd  (the agent's sandboxed dev systems)

The guest’s daemon is where the agent builds and runs things. Because the host socket is never shared in, the two never touch — so a compromised guest can’t reach host containers. See The zero-trust model.