Run lightweight QEMU VMs inside a Docker container. Pick a Linux distribution via an environment variable; the image is downloaded and launched automatically. Uses KVM when available.
Prefer plain docker commands for one-shot runs. The CI publishes an image to GHCR; the helper script pulls ghcr.io/munenick/docker-qemu:latest
by default. Compose is for persistent usage. Design principle: 1 VM = 1 container.
# Run (one-shot, ephemeral; enable KVM if available). Uses GHCR image: ghcr.io/munenick/docker-qemu:latest
docker run --rm -it \
--name vm1 \
-p 2222:2222 \
--device /dev/kvm:/dev/kvm \
ghcr.io/munenick/docker-qemu:latest
# Run a different distro with custom resources
docker run --rm -it \
--name vm1 \
-p 2201:2201 \
--device /dev/kvm:/dev/kvm \
-e DISTRO=debian-12 -e VM_MEMORY=2048 -e VM_CPUS=4 -e VM_SSH_PORT=2201 \
ghcr.io/munenick/docker-qemu:latest
# Persist images across runs (cache under ./images)
docker run --rm -it \
--name vm1 \
-p 2222:2222 \
--device /dev/kvm:/dev/kvm \
-v "$PWD/images:/images" \
ghcr.io/munenick/docker-qemu:latest
# Use local distros.yaml instead of the baked-in one
docker run --rm -it \
--name vm1 \
-p 2222:2222 \
--device /dev/kvm:/dev/kvm \
-v "$PWD/distros.yaml:/config/distros.yaml:ro" \
ghcr.io/munenick/docker-qemu:latest
- Interactive console in the same terminal. To quit QEMU: press Ctrl+A then X.
- Alternative start + attach (docker): add
-d
todocker run
->docker attach vm1
- Show logs (docker):
docker logs -f vm1
- Debug inside the container (docker):
docker exec -it vm1 /bin/bash
- Alternative start + attach (compose):
docker compose up -d
->docker attach $(docker compose ps -q vm1)
- Show logs (compose):
docker compose logs vm1
- Debug inside the container (compose):
docker compose exec vm1 /bin/bash
- Run via curl (default image):
curl -fsSL https://raw.github.com/munenick/docker-qemu/main/scripts/run-vm.sh | bash
- Run with CPU/Memory:
curl -fsSL https://raw.github.com/munenick/docker-qemu/main/scripts/run-vm.sh | bash -s -- --memory 2g --cpus 4
Preflight checks performed by the script:
- Docker CLI present and Docker daemon reachable
- KVM availability and basic permission hinting (
/dev/kvm
readable) - Clear errors with next-step guidance if checks fail
Notes:
- If
/dev/kvm
exists, KVM is enabled automatically; otherwise it falls back to TCG (slower). - The one-shot flow is ephemeral by default (no volumes). Use
--persist
to cache images in./images
. - If the GHCR image is private, run
echo $GITHUB_TOKEN | docker login ghcr.io -u munenick --password-stdin
or set the image public in GitHub Packages.
ubuntu-2404
,ubuntu-2204
,ubuntu-2004
debian-12
,debian-11
centos-stream-9
fedora-41
opensuse-leap-155
rocky-linux-9
,rocky-linux-8
almalinux-9
,almalinux-8
archlinux
DISTRO
: Defaultubuntu-2404
- distribution key fromdistros.yaml
.VM_MEMORY
: Default4096
- memory in MB.VM_CPUS
: Default2
- number of vCPUs.VM_DISK_SIZE
: Default20G
- resize target for the work image.VM_DISPLAY
: Defaultnone
- headless mode.VM_ARCH
: Defaultx86_64
- QEMU system architecture.QEMU_CPU
: Defaulthost
- CPU model.VM_PASSWORD
: Defaultpassword
- console password set via cloud-init.VM_SSH_PORT
: Default2222
- container TCP port forwarded to guest:22
(QEMU user-mode NAT). Useful when running multiple VMs concurrently.VM_NAME
: Optional - per-VM name used to create working artifacts (<name>-work.qcow2
,<name>-seed.iso
). Defaults to container hostname orDISTRO
.NET_MODE
: Defaultuser
- currently onlyuser
(NAT with hostfwd :2222) is supported inside the container.VM_SSH_PUBKEY
: Optional - SSH public key injected via cloud-init.EXTRA_ARGS
: Additional QEMU CLI flags.
Cloud-init is always enabled with a minimal NoCloud seed to set the default user's password for the chosen distribution. Log in on the console using:
- user: distro default (e.g.,
ubuntu
,debian
,centos
, ...) - pass: value of
VM_PASSWORD
docker-qemu/
Dockerfile # QEMU container image
docker-compose.yml # Compose configuration
distros.yaml # Distribution map (mounted into the container)
entrypoint.sh # Startup script
scripts/run-vm.sh # One-shot runner for plain docker
images/ # Cached VM images
README.md
Note: docker-compose.yml
mounts only distros.yaml
into
/config/distros.yaml
to avoid masking the image's /config
directory.
- KVM not available: check
/dev/kvm
withls -la /dev/kvm
. The VM will fall back to TCG (slower) if KVM is unavailable. - Unknown distribution: ensure
DISTRO
matches a key indistros.yaml
and that the file is mounted into the container at/config/distros.yaml
. - If input is not accepted, ensure the container has
stdin_open: true
andtty: true
(compose already sets these), then re-rundocker attach vm1
(for plain docker) ordocker attach $(docker compose ps -q vm1)
(for compose).
- Default (NAT):
- Start:
docker compose up -d
- SSH:
ssh -p $VM_SSH_PORT <user>@localhost
(default 2222)
- Start:
- Docker
- KVM-capable host (optional, for acceleration)
- Docker Compose (only for persistent usage)
For long-running or managed lifecycle use cases, use Compose which pulls the published image, mounts ./images
, and sets restart policy. To run multiple VMs, run multiple containers (1 VM = 1 container) and assign different VM_SSH_PORT
and VM_NAME
values.
docker compose up -d
# Attach interactive console later
docker attach vm1
To pre-pull or update the image:
docker compose pull
- GitHub Actions builds and publishes
ghcr.io/munenick/docker-qemu:latest
on pushes to the default branch and publishes tagged variants on tags. - Pull on clients via the helper script or use
IMAGE_NAME=ghcr.io/munenick/docker-qemu:latest
to override.
MIT License