Run lightweight QEMU VMs from the published container image (ghcr.io/munenick/docker-vm-runner:latest). Each container hosts a single VM orchestrated by libvirt and sushy for Redfish management.
docker run --rm -it \
--name vm1 \
--hostname vm1 \
-p 2222:2222 \
--device /dev/kvm:/dev/kvm \
ghcr.io/munenick/docker-vm-runner:latest- SSH:
ssh -p 2222 user@localhost(password:password). - Redfish (when enabled):
https://localhost:8443(default credentialsadmin/password).
To launch a different distro or adjust resources:
docker run --rm -it \
--name vm1 \
-p 2201:2201 \
--device /dev/kvm:/dev/kvm \
-e DISTRO=debian-12 \
-e MEMORY=2048 \
-e CPUS=4 \
-e SSH_PORT=2201 \
ghcr.io/munenick/docker-vm-runner:latest
Mount a Docker volume (or host directory) at /data and persistence is enabled automatically:
docker run --rm -it \
--name vm1 \
-p 2222:2222 \
--device /dev/kvm:/dev/kvm \
-v myvm:/data \
ghcr.io/munenick/docker-vm-runner:latestWhen /data is mounted, Docker-VM-Runner automatically sets DATA_DIR=/data and PERSIST=1. Override with -e PERSIST=0 if you want ephemeral behavior on a mounted volume. You can also set DATA_DIR explicitly with -v ./data:/data -e DATA_DIR=/data for the same effect.
Storage layout under DATA_DIR:
base/<distro>.qcow2— cached cloud images per distro.vms/<name>/disk.qcow2— working disk (retained whenPERSIST=1).vms/<name>/seed.iso— regenerated cloud-init seed (only when cloud-init is enabled).vms/<name>/.installed— marker written after the first successful run (used for smart ISO skip).state/— management state (Redfish certificates, boot ISO cache, etc.).
Note: When
PERSIST=1, cloud-init only runs on the first boot (keyed by the VM name asinstance-id). ChangingGUEST_PASSWORDor other cloud-init settings on subsequent runs will not take effect unless you also changeGUEST_NAMEor delete the persistent disk.
The container always injects a vendor cloud-config that creates the default login user and password. Supply a second, user-controlled stage by mounting a file and pointing CLOUD_INIT_USER_DATA at it:
cat <<'EOF' > ./cloud-init/user-data.yaml
#cloud-config
packages:
- htop
runcmd:
- ['bash', '-lc', 'echo hello from user-data']
EOF
docker run --rm -it \
--name vm1 \
-p 2222:2222 \
--device /dev/kvm:/dev/kvm \
-v "$PWD/cloud-init:/cloud-init:ro" \
-e CLOUD_INIT_USER_DATA=/cloud-init/user-data.yaml \
ghcr.io/munenick/docker-vm-runner:latestThe file can contain any cloud-init payload (#cloud-config, shell script, boothook, etc.). It is attached as the second part of a multipart NoCloud seed so it runs after the built-in configuration, mirroring the “vendor data + user data” flow seen on EC2.
- Attach to the serial console:
virsh console <vm_name>(inside container) or rely on the container entrypoint (unlessNO_CONSOLE=1). - Detach from console:
Ctrl+]. - Logs:
docker logs -f vm1(compose:docker compose logs vm1). - Exec shell inside the container:
docker exec -it vm1 /bin/bash.
The built-in guest-exec command lets you run commands inside the VM non-interactively via the QEMU Guest Agent — no SSH required:
docker exec vm1 guest-exec "uname -a"
docker exec vm1 guest-exec "cat /etc/os-release"
docker exec vm1 guest-exec "systemctl status nginx"The command captures stdout/stderr and propagates the guest exit code. Cloud images automatically install and start qemu-guest-agent via cloud-init, so guest-exec is available once cloud-init completes (typically 1-2 minutes after boot).
If the guest agent is not yet running, guest-exec prints a clear error message and exits with code 127.
Boot with UEFI firmware (OVMF):
docker run --rm -it \
--device /dev/kvm:/dev/kvm \
-p 2222:2222 \
-e BOOT_MODE=uefi \
ghcr.io/munenick/docker-vm-runner:latestSecure Boot with TPM (TPM is auto-enabled):
docker run --rm -it \
--device /dev/kvm:/dev/kvm \
-p 2222:2222 \
-e BOOT_MODE=secure \
ghcr.io/munenick/docker-vm-runner:latestFor Windows guests, enable Hyper-V enlightenments and use UEFI:
docker run --rm -it \
--device /dev/kvm:/dev/kvm \
-p 6080:6080 \
-e BOOT_FROM=https://example.com/windows.iso \
-e BOOT_MODE=uefi \
-e HYPERV=1 \
-e DISK_SIZE=64G \
-e MEMORY=8192 \
-e CPUS=4 \
-e GRAPHICS=novnc \
ghcr.io/munenick/docker-vm-runner:latestAttach extra disks and let the VM use all available host resources:
docker run --rm -it \
--device /dev/kvm:/dev/kvm \
-p 2222:2222 \
-e MEMORY=max \
-e CPUS=half \
-e DISK_SIZE=50G \
-e DISK2_SIZE=100G \
-e DISK3_SIZE=20G \
ghcr.io/munenick/docker-vm-runner:latestOverride the built-in mapping by bind-mounting your own file:
docker run --rm -it \
--name vm1 \
--device /dev/kvm:/dev/kvm \
-p 2222:2222 \
-v "$PWD/distros.yaml:/config/distros.yaml:ro" \
ghcr.io/munenick/docker-vm-runner:latest