Build an installation process using QEMU. Depending on your use case, this can do anything from testing the install process to automating real mass-installations. For other installation topics, see DebianInstaller.
Basic usage
To install the amd64 version of Debian in a VM, install qemu-system-x86, download the stable amd64 netinst CD image, then do:
# Create a disk image:
qemu-img create -f qcow2 hd_image.qcow2 10G
# run with minimal options:
qemu-system-x86_64 \
-m 1G \
-drive file=hd_image.qcow2 \
-cdrom <your-iso-image>
The installer should start in a window. You can install a Debian system in hd_image.qcow2 now, but it will be very slow. Close the window and try this instead:
# Delete and recreate your disk image:
rm hd_image.qcow2
qemu-img create -f qcow2 hd_image.qcow2 10G
# run with some new options:
qemu-system-x86_64 \
-m 1G -cpu max -enable-kvm \
-no-reboot \
-device virtio-net-pci,netdev=net0 -netdev user,id=net0 \
-drive file=hd_image.qcow2,cache=unsafe \
-cdrom <your-iso-image>
Here's what all the options mean:
- -m 1G
give the VM one gigabyte of memory (slightly more than the minimum memory requirement)
- -cpu max
- emulate the fastest available CPU
- -enable-kvm
- use KVM virtualisation (you may need you to enable virtualization in your BIOS)
- -no-reboot
- exit when the installer tries to reboot
- -device virtio-net-pci,netdev=net0 -netdev user,id=net0
- use a fast network device
- -drive file=hd_image.qcow2,cache=unsafe
- use fast cache settings (risks corrupting the disk on unclean exit, but you can just restart the installer)
- -cdrom <your-iso-image>
- use your ISO image
Finally, try the following advanced technique:
# Extract the Linux kernel and the initrd for the text-based installer:
sudo mount -o loop,ro <your-iso-image> /mnt
cp /mnt/install.amd/{initrd.gz,vmlinuz} ./
sudo umount /mnt
# Delete and recreate your disk image:
rm hd_image.qcow2
qemu-img create -f qcow2 hd_image.qcow2 10G
# run with more new options:
qemu-system-x86_64 \
-m 1G -cpu max -enable-kvm \
-no-reboot \
-device virtio-net-pci,netdev=net0 -netdev user,id=net0 \
-nographic -serial mon:stdio -echr 8 \
-kernel vmlinuz -initrd initrd.gz \
-append "console=ttyS0,115200n8" \
-drive file=hd_image.qcow2,cache=unsafe \
-cdrom <your-iso-image>
The installer should run in a terminal without showing a boot menu. It will start a screen session, so you can e.g. type ctrl-a <space> to cycle between screens. You can also toggle between normal mode and the QEMU monitor with ctrl-h c. Here's what the new options mean:
- -nographic
- do not display QEMU's graphical interface
- -serial mon:stdio
- redirect the VM's serial port to the QEMU monitor, and display the monitor on standard input/output
- -echr 8
change QEMU's escape character from ctrl-a to ctrl-h (h is the 8th letter of the alphabet - change the number to your preferred letter)
- -kernel vmlinuz -initrd initrd.gz
use the previously-extracted kernel and initrd files
- -append "console=ttyS0,115200n8"
- run the installer on the VM's serial port, with the fastest settings
To exit, type ctrl-h c then quit <enter>. If your terminal thinks it's only 25 lines tall after exiting qemu, run reset to fix it
Running the installer in a terminal means you can copy and paste between the VM and the host. Adding -kernel, -initrd and -append lets you change the initial root filesystem and kernel command-line without having to edit the ISO image. For more initrd and command-line options, see boot/grub/grub.cfg in your ISO image.
Basic usage for i386
i386 support was removed in in trixie, so this is only available in bookworm and earlier.
Just change qemu-system-x86_64 to qemu-system-i386; and get an i386 image, kernel and initrd. Everything else should work as normal.
Basic usage for ppc64el
First, change qemu-system-x86_64 to qemu-system-ppc64el; and get a ppc64el image, kernel and initrd.
Then find this line:
-m 1G -cpu max -enable-kvm \
... and remove -enable-kvm:
-m 1G -cpu max
Basic usage for mips64el, mipsel, arm64, armhf and riscv64
riscv64 support was added in trixie, so was only available in testing at the time of writing.
As of trixie, the CD images for these architectures don't work with qemu - use netboot instead.
First, change qemu-system-x86_64 to qemu-system-<architecture>; and get an appropriate kernel and initrd.
Next, find this line:
-m 1G -cpu max -enable-kvm \
... for mips64el, change -cpu max to -cpu 5KEc and replace -enable-kvm with e.g. -M malta:
-m 1G -cpu 5KEc -M malta \
... or for mipsel, remove -cpu and replace -enable-kvm with e.g. -M malta:
-m 1G -M malta \
... or for arm64, armhf and riscv64, just replace -enable-kvm with -M virt:
-m 1G -cpu max -M virt \
Finally, unless you're using riscv64, remove this line:
-append "console=ttyS0,115200n8" \
Install to a physical disk
QEMU can install Debian on a physical disk by direct install within the VM, disk-level copy with qemu-img dd, partition-level copy with qemu-nbd and filesystem-specific tools, or device-mapper copy with Linux snapshots.
Typing the wrong device name can destroy all information on your hard disk, so follow these instructions to select the right device name:
# Make sure your device is disconnected or turned off, then do:
ls /dev/disk/by-id/* | grep -v -- '-part[0-9]*$' | tee /tmp/disks.txt
# Make sure your device is connected and powered up, then do:
ls /dev/disk/by-id/* | grep -v -- '-part[0-9]*$' | diff /tmp/disks.txt -
# You should see something like:
# 10a11
# > /dev/disk/by-id/<identifier-of-your-disk>
# Make a variable with the device from the previous command:
TARGET_DEVICE=/dev/disk/by-id/<identifier-of-your-disk>
# Check you typed it correctly by reading one byte from the disk:
sudo head -c1 "$TARGET_DEVICE" > /dev/null
# Disconnect your device, then try again:
sudo head -c1 "$TARGET_DEVICE" > /dev/null
If you did everything right, the head command should only work when the device is plugged in. You can now use $TARGET_DEVICE instead of the name of your device in future commands.
The instructions below often refer to $TARGET_DEVICE. If you create a variable as described above, you can paste those exact instructions. Otherwise, change $TARGET_DEVICE to your actual device name.
Direct install
The simplest method is just to make your target device available in the VM. Find this line:
-drive file=hd_image.qcow2,cache=unsafe \
... change hd_image.qcow2 to "$TARGET_DEVICE" and append ,format=raw:
-drive file="$TARGET_DEVICE",cache=unsafe,format=raw \
You may also need to run qemu with sudo so it can access the disk.
Disk-level copy
You can install Debian on a disk image, then burn that image to your target disk.
First, create a new hd_image.qcow2 that's exactly the same size as your target disk:
# Get the directory in /sys with information about your device:
TARGET_INFO="/sys/block/$(basename $(readlink -f "$TARGET_DEVICE"))"
# Get the size in bytes of the device:
TARGET_SIZE="$(( $(<"$TARGET_INFO"/size) * $(<"$TARGET_INFO"/queue/logical_block_size) ))"
# Create a disk image exactly the same size as the real one:
rm hd_image.qcow2
qemu-img create -f qcow2 hd_image.qcow2 "$TARGET_SIZE"
Next, install Debian on hd_image.qcow2 in the usual way.
Then copy the installed disk image to the real disk:
sudo qemu-img convert -f qcow2 -O raw -W -S 0 hd_image.qcow2 "$TARGET_DEVICE"
Your physical disk should now have the Debian system you created. Here's what all the options mean:
- -f qcow2
assume hd_image.qcow2 is in qcow2 format
- -O raw
assume $TARGET_DEVICE is a normal hard disk
- -W
- allow out-of-order writes (faster)
- -S 0
- reset the entire disk (see below)
-S 0 overwrites the entire disk, including areas the installer didn't touch - this takes longer and makes it harder to undelete files. If you instead specify e.g. -S 1M, qemu-img will skip writing any part of the disk with more than the specified number of consecutive zeroes. This is faster, but could lead to corruption in the unlikely event the installer created an actual file with that many zeroes in it.
Partition-level copy
You can install Debian on a disk image, then clone each partition to your target disk. This requires filesystem-specific tools which can leave unused parts of the disk alone.
First, create a new hd_image.qcow2 that's at most the same size as your target disk:
sudo lsblk
# ... check the target disk is at least 10GB in size ...
rm hd_image.qcow2
qemu-img create -f qcow2 hd_image.qcow2 10G
Next, install Debian on hd_image.qcow2 in the usual way.
Then make the partitions on hd_image.qcow2 available to the host:
# Connect to the disk image:
sudo modprobe nbd
sudo qemu-nbd -c /dev/nbd0 hd_image.qcow2
Then partiton your target device however you like. For example:
# Dump the disk image's partitions:
sudo sfdisk --dump /dev/nbd0 | tee hd_image.sfdisk
# ... edit hd_image.sfdisk however you like (e.g. resize the partitions) ...
# Repartition the disk:
sudo sfdisk "$TARGET_DEVICE" < hd_image.sfdisk
Then copy (or recreate) each partition in a filesystem-specific way. For example:
# Clone the used sectors in an ext{2,3,4} partition and resize to fit:
sudo e2image -r -a -p /dev/nbd0p1 "$TARGET_DEVICE"-part1
sudo e2fsck -f "$TARGET_DEVICE"-part1
sudo resize2fs -p "$TARGET_DEVICE"-part1
# Recreate a swap partition instead of copying it:
sudo blkid /dev/nbd0p5
sudo mkswap --uuid <uuid> "$TARGET_DEVICE"-part5
# Copy an arbitrary partition:
sudo dd conv=fsync status=progress bs=4M if="/dev/nbd0p6" of="$TARGET_DEVICE"-part6
Finally, disconnect the disk image:
sudo qemu-nbd -d /dev/nbd0
Device-mapper copy
Linux's device-mapper lets you manage snapshots for any device. Among other things, that lets you queue up writes for a device, then choose whether to flush or discard the queue.
First, create a device to contain the queue, and a frontend device to actually work with (note: /tmp has been a tmpfs since trixie):
QUEUE_FILE=/tmp/installer/queue.img
FRONTEND_FILENAME=installer-frontend
mkdir -p /tmp/installer
# not required in trixie and later:
sudo mount -t tmpfs tmpfs /tmp/installer
truncate -s 10G "$QUEUE_FILE"
QUEUE_DEVICE="$( sudo losetup --find --show "$QUEUE_FILE" )"
sudo dmsetup create "$FRONTEND_FILENAME" --table \
"0 $( sudo blockdev --getsz "$TARGET_DEVICE" ) snapshot $TARGET_DEVICE $QUEUE_DEVICE P 4096"
This will create a file called /dev/mapper/$FRONTEND_FILENAME. It appears to be a normal device, but changes written there will be queued to $QUEUE_FILE.
Next, find this line:
-drive file=hd_image.qcow2,cache=unsafe \
... change hd_image.qcow2 to "/dev/mapper/$FRONTEND_FILENAME" and append ,format=raw:
-drive file="/dev/mapper/$FRONTEND_FILENAME",cache=unsafe,format=raw \
Then install Debian on /dev/mapper/$FRONTEND_FILENAME in the usual way.
Then if the install went well, flush the queue to the physical device:
# Flush the queue:
sudo dmsetup reload "$FRONTEND_FILENAME" --table \
"0 $( sudo blockdev --getsz "$TARGET_DEVICE" ) snapshot-merge $TARGET_DEVICE $QUEUE_DEVICE P 4096"
sudo dmsetup resume "$FRONTEND_FILENAME"
# Wait for flushing to finish:
while ! sudo dmsetup status "$FRONTEND_FILENAME" | \
{ read _ _ _ ALLOCATED_TOTAL METADATA && echo "$ALLOCATED_TOTAL $METADATA" && [ ${ALLOCATED_TOTAL%/*} = $METADATA ] ; }
do
sleep 5
done
# Flush any remaining caches to disk:
sudo blockdev --flushbufs "$TARGET_DEVICE"
Finally, clear everything up:
sudo dmsetup remove "$FRONTEND_FILENAME"
sudo losetup -d "$QUEUE_DEVICE"
sudo rm "$QUEUE_FILE"
If you flushed the queue, Debian should now be installed on $TARGET_DEVICE. Or if you skipped that step, the device should be unchanged.
Advanced techniques
The basic usage lets you install a system using a VM, advanced techniques can be more efficient and more widely useful.
Netboot without a CD image
The normal installation method ("netinst") requires a CD image, the netboot method downloads everything instead. This might be more convenient if you're testing the installer against recently-updated packages, or you're installing from a system without much disk space.
First, download the netboot kernel and initrd:
go to the installation page
- click the "other images" section for your architecture
if there is a netboot/ directory (most architectures)...
if you want a graphical installer, click gtk/
if there is a debian-installer/ directory (most architectures)...
click debian-installer/
- click your architecture
download initrd.gz and either linux, vmlinuz or vmlinux (whichever is present)
otherwise, if there are directories with different machine types (e.g. mips64el)...
- click your machine type
click netboot/
download vmlinuz-* and initrd.gz
Then find the following lines in your qemu command:
-kernel vmlinuz -initrd initrd.gz \
-cdrom <your-iso-image>
... change vmlinuz to the name of the kernel file you downloaded, and remove the -cdrom line altogether:
-kernel linux -initrd initrd.gz \
Running your qemu command should now start the netboot installer.
Cache downloads
The installer can download packages through a proxy server. This can save time and bandwidth if you run the installer frequently.
First, install apt-cacher-ng on your host (or some other device on your network).
Next, find the following line in your qemu command:
-append "console=ttyS0,115200n8" \
... and add mirror/http/proxy=<url>
-append "console=ttyS0,115200n8 mirror/http/proxy=<url>" \
qemu assigns the address 10.0.2.2 to the host machine by default, and apt-cacher-ng uses port 3142 by default, so you would usually use mirror/http/proxy=http://10.0.2.2:3142/.
If you use preseeding, you can add mirror/http/proxy=<url> there instead.
Preseed your answers
You can use preseeding to define default answers to questions.
First, install cpio and create a preseed file called preseed.cfg.
Next, merge it into a new initrd:
# Add your preseed file to the initrd:
{ cat initrd.gz; echo preseed.cfg | cpio --format=newc --create | gzip -c; } > initrd-custom.gz
You can add other files the same way, such as udeb files to use during installation, scripts to call from preseed/early_command and preseed/late_command, or installer hooks.
To use your preseed file, find these lines:
-kernel vmlinuz -initrd initrd.gz \
-append "console=ttyS0,115200n8" \
... and change them to something like:
-kernel vmlinuz -initrd initrd-custom \
-append "console=ttyS0,115200n8 auto" \
This will use your modified initrd-custom file and run the installer in automated mode.
Install to an in-memory disk
If you have enough memory on your computer and want the installer to run faster, you can install to a device that only exists in memory.
First, create your device (note: /tmp has been a tmpfs since trixie):
mkdir -p /tmp/installer
# not required in trixie and later:
sudo mount -t tmpfs tmpfs /tmp/installer
TARGET_DEVICE=/tmp/installer/installer.img
truncate -s 10G "$TARGET_DEVICE"
Then install using the Direct install instructions, above.
truncate -s creates a sparse file. This should be slightly faster than a .qcow2 file, but doesn't support snapshots.
Use the qemu monitor
You can toggle between normal mode and the QEMU monitor with ctrl-h c. Some commands are particularly useful when testing the installer:
To debug the installer kernel, run gdbserver in the monitor, then call gdb --eval-command='target remote localhost:1234' on the host.
To rerun a problematic section of the installer, run savevm, loadvm and delvm in the monitor to store and roll back to specific snaphots.
Snapshots require all attached devices to use .qcow2. If you want to install to an in-memory disk, you'll have to use an in-memory .qcow2 disk.
Communicate between host and VM
To make host files available in the installer's filesystem (e.g. udebs), copy them the same way as preseed.cfg.
You can just copy and paste data in the terminal. The installer includes base64, which makes it easier to copy and paste binary files.
How do I copy d-i logfiles to a remote host? provides general information about communicating with the installer, but here are some qemu-specific notes:
switch to a shell with ctrl-a <space> instead of ctrl-alt-F2
by default, the VM thinks your host has IP address 10.0.2.2
- by default, the host cannot connect to the VM (see below)
Connect from the host to the VM
The host can only connect to the VM on ports that you explicitly forwarded.
Find the following line in your qemu command:
-device virtio-net-pci,netdev=net0 -netdev user,id=net0 \
... and append e.g. ,hostfwd=tcp::5555-:80 to forward port 5555 on the host to port 80 on the VM:
-device virtio-net-pci,netdev=net0 -netdev user,id=net0,hostfwd=tcp::5555-:80 \
Now anything in the VM that listens on 10.0.2.2:80 will be accessible from the host on 127.0.0.1:5555.
Communicate using a device
QEMU lets you use arbitrary files as disks, but the installer only populates /dev/disk when it detects disks. In practice, that means it's only useful in late_command and installer hooks that run after the directory is created. This section provides some examples for how you might use this.
First, create the file with an initial value, for example:
# Create a small file the VM can modify:
echo -n 0 > shared-device
# Or create a tarball with files you don't want to put in your `initrd`:
tar cf shared-device my-file1 my-file2 ...
Then add an option like this to your qemu command-line:
-drive file=shared-device,format=raw \
Finally, add a line to e.g. your late_command script:
# Modify the small file:
echo -n 1 > /dev/disk/by-id/ata-QEMU_HARDDISK_QM00002
# Or extract your tarball into the installed system:
tar x -C /target -f /dev/disk/by-id/ata-QEMU_HARDDISK_QM00002
The first example stores 1 in shared-device if installation succeeds, or 0 on error. The second example extracts the contents of shared-device into hd_image.qcow2.
Other possible solutions
The following could theoretically be used to communicate between host and guest, but have been found not to be useful.
virtiofs lets you mount a host directory as a filesystem in a VM. But as of bookworm, the installer uses BusyBox's mount command, which doesn't seem to support -t virtiofs.
QEMU lets you bind /dev/ttyS1 in the VM to a socket on the host, but it's limited to 11,5200 bytes per second, data needs to be base64-encoded to avoid sending special characters, buffering causes files to be truncated, and it's hard to mark end-of-file. If you want to try it, add something like this to your qemu command-line:
-chardev socket,path=installer.sock,server=on,wait=off,id=installer -serial chardev:installer
You can then communicate with the guest using e.g.:
# In the host:
socat - unix:installer.sock
# Read data into the guest:
cat /dev/ttyS1
# Write data from the guest:
cat > /dev/ttyS1
Configure partman
Preseeding uses partman to configure partitions, which can be hard to test because it runs in the middle of the installation process. Here is an easy way to test it:
use the partman-auto/expert_recipe_file setting to store your partman recipe in /partman-recipe.txt
- comment out the setting for the last question before partitioning
usually either confirming the ordinary user's password or setting up the clock
- go through the installation process until you reach that question
On the host, do: socat tcp-listen:12345 - < /path/to/your/partman-recipe.txt
- back in the VM...
use ctrl-a <space> to cycle to a shell
do: nc 10.0.2.2:12345 > /partman-recipe.txt
- this should overwrite the previous recipe
cycle back to the installer with ctrl-a <space>
- continue the installer
- when partman finishes (or fails), reload your snapshot and return to step 5
See Also
QEMU on this wiki
IRC channel: qemu
- information about specific architectures
arm64: Arm64Qemu
ppc64el: DebianInstaller/PowerPC/qemu
riscv64: RISC-V and UEFI Unified Kernel Image for Debian Installer on riscv64
- manuals
