はじめに
DockerでLinux環境を起動するのは簡単です。
docker run --rm -it ubuntu
これだけでUbuntuのユーザーランドをすぐに試すことができます。
ただし、コンテナでは足りない場面があります。
たとえば、systemdを普通に動かしたい場合、カーネルやブート周りの挙動を確認したい場合、OSインストーラを起動したい場合、ディスクレイアウトやファームウェア、cloud-init、ネットワークブートなどを検証したい場合です。
こういう用途では、やはりコンテナではなくVMが必要になります。
一方で、QEMUやlibvirtを直接扱うのはそれなりに面倒です。毎回 qemu-system-x86_64 の長いオプションを書くのも大変ですし、ディスク、ネットワーク、ポートフォワード、VNC、SSH、cloud-initなどを毎回組み立てるのも面倒です。
そこで、Dockerの操作感で本物のVMを起動できるツールとして、docker-vm-runner を作成しました。
リポジトリはこちらです。
https://github.com/MuNeNiCK/docker-vm-runner
Docsはこちらです。
https://munenick.github.io/docker-vm-runner/
この記事では概要と代表的な使い方を紹介します。細かい環境変数、ネットワーク、ストレージ、アクセス方法、Agent Skill、ExamplesについてはDocsにまとめています。
docker-vm-runnerとは
docker-vm-runner は、Dockerコンテナの中からQEMU/libvirtを使ってVMを起動するツールです。
名前の通り、DockerでVMをrunします。
ただし、起動しているのはコンテナではありません。ゲストOSは独自のカーネル、ファームウェア、ディスク、コンソールを持った実際の仮想マシンです。
そのため、通常のコンテナでは検証しづらい以下のような用途に使えます。
- cloud imageの起動確認
- OSインストーラISOの起動
- systemdやカーネルモジュール周りの検証
- cloud-initの検証
- ディスクサイズや追加ディスクの検証
- NAT、Bridge、Directなどのネットワーク検証
- iPXEによるネットワークブート検証
- VNC/noVNCを使ったGUIインストーラの操作
- Redfish/IPMIを使ったBMC風の制御
- QEMU Guest Agent経由でのゲスト内コマンド実行
コンテナで十分なものはコンテナで良いと思います。 ただ、OSそのものの挙動や、VMでないと再現できない部分を扱いたい場合は、VMを使った方が素直です。
docker-vm-runner は、そのVMの起動やアクセスをDockerのワークフローに寄せるためのツールです。
まず起動してみる
一番単純な起動方法は以下です。
docker run --rm -it \
--name docker-vm-runner \
--device /dev/kvm \
ghcr.io/munenick/docker-vm-runner:latest
Linuxホストで /dev/kvm が使える場合は、KVMによるハードウェア支援を使ってVMを起動できます。
/dev/kvm がない環境では、以下のように --device /dev/kvm を外すことでソフトウェアエミュレーションにフォールバックできます。
docker run --rm -it \
--name docker-vm-runner \
ghcr.io/munenick/docker-vm-runner:latest
ただし、性能面ではKVMが使えるLinuxホストで動かすのが基本です。
起動すると、VMのシリアルコンソールに接続されます。
一時的に試すだけならこの形で十分です。コンテナを終了すると、VMのディスクやダウンロード済みイメージキャッシュも削除されます。
ディスクを永続化する
毎回VMを作り直すのではなく、ディスクやイメージキャッシュを保持したい場合は /data をマウントします。
docker run --rm -it \
--name docker-vm-runner \
--device /dev/kvm \
-v docker-vm-runner-data:/data \
ghcr.io/munenick/docker-vm-runner:latest
/data をマウントすると、VMのディスク、状態、イメージキャッシュが保持されます。
ちょっとした検証用VMとして使う場合は、この形が使いやすいと思います。
バックグラウンドで起動したい場合は -dit を使います。
docker run -dit --name docker-vm-runner \
--device /dev/kvm \
-v docker-vm-runner-data:/data \
ghcr.io/munenick/docker-vm-runner:latest
ログを見る場合は以下です。
docker logs -f docker-vm-runner
シリアルコンソールへ再接続する場合は以下です。
docker attach docker-vm-runner
Docker attach中にVMを止めずに抜けたい場合は、Ctrl+P の後に Ctrl+Q を押します。
起動するOSを選ぶ
docker-vm-runner は os-iso-catalog のイメージIDを使って、起動するOSイメージを選択できます。
デフォルトのカタログは以下です。
https://munenick.github.io/os-iso-catalog/v1/all.json
利用可能なイメージ一覧は以下で確認できます。
docker run --rm \
ghcr.io/munenick/docker-vm-runner:latest --list-distros
Ubuntuを検索する場合は以下です。
docker run --rm \
ghcr.io/munenick/docker-vm-runner:latest --list-distros --search ubuntu
イメージタイプで絞ることもできます。
docker run --rm \
ghcr.io/munenick/docker-vm-runner:latest --list-distros --type cloud-image
docker run --rm \
ghcr.io/munenick/docker-vm-runner:latest --list-distros --type iso
docker run --rm \
ghcr.io/munenick/docker-vm-runner:latest --list-distros --type disk-image
普段使いでは、cloud-initが使えて起動も速い cloud-image が扱いやすいと思います。
特定のイメージを指定して起動する場合は、DISTRO を指定します。
docker run --rm -it \
--device /dev/kvm \
-e DISTRO=ubuntu-24.04-cloud-amd64 \
-v docker-vm-runner-data:/data \
ghcr.io/munenick/docker-vm-runner:latest
デフォルトのゲストユーザーは user、パスワードは password です。
SSH鍵を入れたい場合は、SSH_PUBKEY を指定します。
docker run --rm -it \
--device /dev/kvm \
-e DISTRO=ubuntu-24.04-cloud-amd64 \
-e SSH_PUBKEY="$(cat ~/.ssh/id_ed25519.pub)" \
-v docker-vm-runner-data:/data \
ghcr.io/munenick/docker-vm-runner:latest
起動前に設定を確認する
複雑な設定を入れる場合、いきなりVMを起動する前に --show-config や --dry-run を使うと便利です。
解決後の設定を見る場合は以下です。
docker run --rm \
-e DISTRO=ubuntu-24.04-cloud-amd64 \
-e CPUS=4 \
-e MEMORY=8192 \
-e NETWORK_MODE=nat \
-e PORT_FWD=8080:80 \
ghcr.io/munenick/docker-vm-runner:latest --show-config
起動せずに検証だけする場合は以下です。
docker run --rm \
-e DISTRO=ubuntu-24.04-cloud-amd64 \
-e CPUS=4 \
-e MEMORY=8192 \
ghcr.io/munenick/docker-vm-runner:latest --dry-run
VMのXMLを見たい場合は --show-xml も使えます。
このあたりは、環境変数が増えてきた時の確認に使えます。
SSHで接続する
デフォルトのNATネットワークでは、ホスト側の2222番をゲストの22番に転送できます。
docker run --rm -it \
--device /dev/kvm \
-p 2222:2222 \
-v docker-vm-runner-data:/data \
ghcr.io/munenick/docker-vm-runner:latest
起動後は以下で接続できます。
ssh user@localhost -p 2222
パスワードを変更したい場合は、GUEST_PASSWORD を指定します。
docker run --rm -it \
--device /dev/kvm \
-p 2222:2222 \
-e GUEST_PASSWORD='change-me' \
-v docker-vm-runner-data:/data \
ghcr.io/munenick/docker-vm-runner:latest
追加のポートを転送したい場合は PORT_FWD を使います。
たとえば、ゲストの80番をホスト側8080番で見たい場合は以下です。
docker run --rm -it \
--device /dev/kvm \
-p 8080:8080 \
-e PORT_FWD=8080:80 \
-v docker-vm-runner-data:/data \
ghcr.io/munenick/docker-vm-runner:latest
PORT_FWD は container_port:guest_port の形式です。複数指定する場合はカンマ区切りです。
-e PORT_FWD=8080:80,8443:443
Dockerの -p も合わせて指定する必要があります。
ゲスト内でコマンドを実行する
docker-vm-runner では、QEMU Guest Agentを使ってゲスト内でコマンドを実行する guest-exec も用意しています。
まず、VMをバックグラウンドで起動します。
docker run -dit --name ubuntu-vm \
--device /dev/kvm \
-e DISTRO=ubuntu-24.04-cloud-amd64 \
-v ubuntu-vm-data:/data \
ghcr.io/munenick/docker-vm-runner:latest
その後、ホスト側から以下のようにゲスト内コマンドを実行できます。
docker exec ubuntu-vm guest-exec --wait "uname -a"
引数形式でも実行できます。
docker exec ubuntu-vm guest-exec --wait id user
--wait を付けると、VMドメインとQEMU Guest Agentが利用可能になるまで待ってからコマンドを実行します。
SSHでログインせずに、VM内でOSレベルのコマンドを実行できるので、CIや自動テストで便利です。
たとえば、コンテナではなくVMの中で systemctl やパッケージマネージャーを動かしたい場合に使えます。
docker exec ubuntu-vm guest-exec --wait systemctl is-system-running
docker exec ubuntu-vm guest-exec --wait "apt-get update"
注意点として、guest-exec はPTYではなくQEMU Guest Agent経由で実行します。そのため、コマンドによっては標準出力のバッファリングやstdout/stderrの順序が通常のターミナル実行と少し違う場合があります。
ISOインストールもできる
cloud imageだけでなく、ISOからのインストールにも対応しています。
docker run --rm -it \
--device /dev/kvm \
-e BOOT_FROM=https://example.com/installer.iso \
-e GUEST_NAME=installed-vm \
-e CPUS=4 \
-e MEMORY=8192 \
-e DISK_SIZE=80G \
-v installed-vm-data:/data \
ghcr.io/munenick/docker-vm-runner:latest
ISOから起動する場合は、空のディスクを用意して、CD-ROMを優先して起動します。
永続化したVMでは、インストール完了後に次回起動すると、ISOではなくインストール済みディスクから起動するようにしています。
再度ISOを接続したい場合や、レスキューISOとして繰り返し起動したい場合は FORCE_ISO=1 を使います。
docker run --rm -it \
--device /dev/kvm \
-e BOOT_FROM=https://example.com/rescue.iso \
-e FORCE_ISO=1 \
-e GUEST_NAME=rescue-vm \
-v rescue-vm-data:/data \
ghcr.io/munenick/docker-vm-runner:latest
BOOT_FROM にはURL、ローカルパス、OCI参照、ISO、disk image、blank などを指定できます。
空ディスクから始めたい場合は以下のようにします。
docker run --rm -it \
--device /dev/kvm \
-e BOOT_FROM=blank \
-e DISK_SIZE=40G \
-v blank-vm-data:/data \
ghcr.io/munenick/docker-vm-runner:latest
noVNCでブラウザから操作する
GUIインストーラを使いたい場合は、noVNCを有効にできます。
docker run --rm -it \
--device /dev/kvm \
-p 6080:6080 \
-e GRAPHICS=novnc \
-e BOOT_FROM=https://example.com/desktop-installer.iso \
-e GUEST_NAME=desktop-install \
-e MEMORY=8192 \
-e DISK_SIZE=80G \
-v desktop-install-data:/data \
ghcr.io/munenick/docker-vm-runner:latest
起動後、ブラウザで以下を開きます。
https://localhost:6080/
noVNCのエンドポイントは自己署名証明書を使います。
VNCクライアントを別途用意したい場合は、GRAPHICS=vnc も使えます。
docker run --rm -it \
--device /dev/kvm \
-p 5900:5900 \
-e GRAPHICS=vnc \
-v docker-vm-runner-data:/data \
ghcr.io/munenick/docker-vm-runner:latest
この場合は、VNCクライアントで localhost:5900 に接続します。
ネットワーク
デフォルトではNATを使います。
Dockerでコンテナを起動して、必要なポートだけ -p で公開するのに近い使い方です。普段の検証では、まずNATで使うのが簡単です。
一方で、VMをLAN上の普通のマシンのように見せたい場合はBridgeを使えます。
docker run -d --name bridge-vm \
--network host \
--cap-add NET_ADMIN \
--device /dev/kvm \
--device /dev/net/tun \
--device /dev/vhost-net \
-e DISTRO=ubuntu-24.04-cloud-amd64 \
-e NETWORK_MODE=bridge \
-e NETWORK_BRIDGE=br0 \
-e NO_CONSOLE=1 \
-v bridge-vm-data:/data \
ghcr.io/munenick/docker-vm-runner:latest
Bridgeを使う場合は、ホスト上に br0 のようなLinux bridgeが存在している必要があります。
物理NICにVMを直接ぶら下げたい場合はDirectも使えます。
MACVTAP_MAJOR="$(awk '$2 == "macvtap" { print $1 }' /proc/devices)"
docker run -d --name direct-vm \
--network host \
--cap-add NET_ADMIN \
--device /dev/kvm \
--device /dev/vhost-net \
--device-cgroup-rule "c ${MACVTAP_MAJOR}:* rwm" \
-v /dev:/dev:ro \
-e DISTRO=ubuntu-24.04-cloud-amd64 \
-e NETWORK_MODE=direct \
-e NETWORK_DIRECT_DEV=eth0 \
-e NO_CONSOLE=1 \
-v direct-vm-data:/data \
ghcr.io/munenick/docker-vm-runner:latest
BridgeやDirectはホスト側のネットワーク構成に強く依存します。まずはNATで動かして、必要になったらBridgeやDirectを使うのが良いと思います。
また、複数NICにも対応しています。たとえば1枚目をNATの管理用NIC、2枚目をBridgeやDirectの検証用NICにする、といった構成もできます。
docker run --rm \
-e DISTRO=alpine-3.22-cloud-amd64 \
-e NETWORK_MODE=nat \
-e NETWORK2_MODE=bridge \
-e NETWORK2_BRIDGE=br0 \
ghcr.io/munenick/docker-vm-runner:latest --show-config
iPXEによるネットワークブート
docker-vm-runner はiPXEによるネットワークブートにも対応しています。
docker run --rm -it \
--network host \
--cap-add NET_ADMIN \
--device /dev/kvm \
--device /dev/net/tun \
--device /dev/vhost-net \
-e IPXE_ENABLE=1 \
-e BOOT_ORDER=network,hd \
-e NETWORK_MODE=bridge \
-e NETWORK_BRIDGE=br0 \
-e GUEST_NAME=ipxe-vm \
-v ipxe-vm-data:/data \
ghcr.io/munenick/docker-vm-runner:latest
IPXE_ENABLE=1 を指定すると、プライマリNICにiPXE ROMを注入し、boot orderでnetworkを優先するようにできます。
PXE系のインストーラ環境や、netboot.xyzのような仕組みを検証したい場合に使えます。
実際のPXE環境では、ゲストがDHCP/TFTP/HTTPなどの上流サービスに到達できる必要があります。そのため、NATよりBridgeやDirectの方が現実的です。
ストレージ
メインディスクのサイズは DISK_SIZE で指定できます。
docker run --rm -it \
--device /dev/kvm \
-e DISTRO=ubuntu-24.04-cloud-amd64 \
-e DISK_SIZE=40G \
-v storage-vm-data:/data \
ghcr.io/munenick/docker-vm-runner:latest
DISK_SIZE には half や max も指定できます。
追加ディスクは DISK2_SIZE から DISK6_SIZE で指定できます。
docker run --rm -it \
--device /dev/kvm \
-e DISTRO=ubuntu-24.04-cloud-amd64 \
-e DISK_TYPE=scsi \
-e DISK2_SIZE=100G \
-v storage-vm-data:/data \
ghcr.io/munenick/docker-vm-runner:latest
ディスクコントローラは DISK_TYPE で指定できます。対応している値は virtio、scsi、nvme、ide、usb です。
ホストディレクトリをゲストに共有することもできます。
mkdir -p share
docker run --rm -it \
--device /dev/kvm \
-v "$PWD/share:/shared" \
-e DISTRO=ubuntu-24.04-cloud-amd64 \
-e FILESYSTEM_SOURCE=/shared \
-e FILESYSTEM_TARGET=shared \
-e FILESYSTEM_DRIVER=9p \
-e FILESYSTEM_ACCESSMODE=mapped \
-v shared-vm-data:/data \
ghcr.io/munenick/docker-vm-runner:latest
ゲスト側では以下のようにマウントできます。
sudo mkdir -p /mnt/shared
sudo mount -t 9p -o trans=virtio,version=9p2000.L shared /mnt/shared
virtiofs も使えますが、アクセスモードの指定などは9pと違いがあります。細かい設定はDocsのStorageとReferenceを見てください。
Redfish/IPMIでVMを制御する
docker-vm-runner ではRedfishやIPMIも有効にできます。
Redfishを有効にする例です。
docker run --rm -it \
--device /dev/kvm \
-p 8443:8443 \
-e DISTRO=ubuntu-24.04-cloud-amd64 \
-e REDFISH_ENABLE=1 \
-e REDFISH_PASSWORD='change-me' \
-v redfish-vm-data:/data \
ghcr.io/munenick/docker-vm-runner:latest
確認は以下です。
curl -k -u admin:change-me https://localhost:8443/redfish/v1/
Redfishを有効にする場合、デフォルトパスワードのままではなく REDFISH_PASSWORD を変更してください。
IPMIも同様に有効化できます。
docker run --rm -it \
--device /dev/kvm \
-p 623:623/udp \
-e IPMI_ENABLE=1 \
-e IPMI_PASSWORD='change-me' \
-v ipmi-vm-data:/data \
ghcr.io/munenick/docker-vm-runner:latest
ipmitool -I lanplus -U admin -P change-me -H localhost -p 623 power status
IPMIはVirtualBMC経由でlibvirtのVMを制御します。
このあたりは、普通のローカルVMランナーとしては少し珍しい機能だと思います。物理サーバ向けの制御ツールや、bare metal provisioning系のツールをVM相手に試したい場合に使えます。
Ironic連携のExamples
DocsにはExamplesも用意しています。
https://munenick.github.io/docker-vm-runner/examples/
たとえば、Docker Composeで複数VMを起動する例があります。
examples/multiple-vms/
これはUbuntuとAlpineのVMを1つのComposeプロジェクトとして起動し、それぞれにSSHやnoVNCでアクセスできるようにする例です。
また、RedfishやIPMIでIronicからVMを操作する例もあります。
examples/redfish-ironic/
examples/ipmi-ironic/
これらは完全なOpenStack環境を再現するものではありませんが、IronicがRedfish/IPMI経由でノードを検出し、validateし、boot deviceを操作する流れをローカルで確認できます。
iPXEとnetboot.xyzを使う例もあります。
examples/ipxe-netbootxyz/
単発のコマンドで説明しづらいものは、Examplesの方にDocker Compose形式でまとめています。
AI Agent向けのSkill
最近はCodexやClaude Code、OpenCodeのようなAI Coding Agentにコマンドを実行させる場面が増えています。
ただ、Agentにホスト環境を直接触らせるのは避けたい場合があります。
一方で、通常のコンテナではsystemd、カーネル、OSインストーラ、ネットワークブートのような検証ができません。
そこで、docker-vm-runner にはAI Coding Agent向けのSkillも用意しています。
https://munenick.github.io/docker-vm-runner/agent-skills/
このSkillを使うと、Agentに対して以下のようなVM操作をさせやすくなります。
- 一時VMや永続VMの起動
- guest-execによるゲスト内コマンド実行
- SSH接続
- noVNC/VNCによるGUIインストーラ操作
- RedfishによるBMC風制御
- ISO、cloud image、blank disk、iPXEなどからの起動
- ネットワーク、追加ディスク、ファイル共有、ブロックデバイスの設定
GitHub CLIが使える場合は、以下のようにインストールできます。
Codex向けです。
gh skill install MuNeNICK/docker-vm-runner skills/docker-vm-runner-agent \
--agent codex \
--scope user
Claude Code向けです。
gh skill install MuNeNICK/docker-vm-runner skills/docker-vm-runner-agent \
--agent claude-code \
--scope user
OpenCode向けです。
gh skill install MuNeNICK/docker-vm-runner skills/docker-vm-runner-agent \
--agent opencode \
--scope user
AgentにOSレベルの検証をさせたいが、ホスト環境は直接触らせたくない、という用途に使えると思います。
cleanup
VMを何度も起動していると、前回のコンテナ終了時にランタイムリソースが残る場合があります。
その場合は --cleanup を使えます。
docker run --rm \
-v docker-vm-runner-data:/data \
ghcr.io/munenick/docker-vm-runner:latest --cleanup
/data をマウントしている場合、永続ディスクやキャッシュは残しつつ、残っているランタイムリソースを削除します。
GUEST_NAME や DISTRO を指定していた場合は、cleanup時も同じ値を指定してください。
どういう時に使うか
自分が想定している用途は、主に以下です。
OSイメージの検証
cloud imageやdisk imageが正しく起動するか、cloud-initが通るか、SSHできるか、といった確認に使えます。
Kubernetesやインフラ系スクリプトのテスト
Kubernetesセットアップスクリプトのように、systemd、swap、カーネルモジュール、iptables、firewalld、SELinux/AppArmorなどに依存するものは、コンテナ上では検証しづらいです。
こういうものはVMでテストした方が安全です。
docker-vm-runner を使えば、Dockerのワークフローに寄せたまま、本物のVMで検証できます。
OSインストーラの検証
ISOを起動して、ディスクにインストールし、次回以降はインストール済みディスクから起動する、という流れを扱えます。
GUIインストーラが必要な場合はnoVNCも使えます。
bare metal系ツールの検証
Redfish/IPMIに対応しているため、BMCを前提にしたツールをVM相手に試せます。
Ironicのようなツールを、物理サーバなしである程度確認したい場合に便利です。
Agent用の隔離実行環境
AI Coding Agentに何かを検証させる場合、コンテナでは足りないが、ホストを直接触らせたくないことがあります。
その場合に、VMをサンドボックスとして使えます。
注意点
docker-vm-runner はLinuxホストを主対象にしています。
macOSやWindowsでも、Docker DesktopやWSL2などの環境によって一部は動く可能性がありますが、KVMやホストネットワーク周りはLinuxほど素直には使えません。
Bridge、Direct、ホストブロックデバイス、GPU、USB、TPM、ファイルシステム共有などは、ホスト側のデバイスやDocker実行オプションに依存します。
おわりに
docker-vm-runner は、Dockerのような手軽さで本物のVMを起動するためのツールです。
コンテナで済むものはコンテナで良いと思います。
ただし、systemd、カーネル、ファームウェア、ISO、ディスク、ネットワーク、Redfish/IPMIのような領域に入ると、コンテナではどうしても限界があります。
その境界を超えたい時に、QEMU/libvirtを直接書くのではなく、DockerのワークフローでVMを扱えるようにするのがこのツールの目的です。
興味があれば試してみてください。