How to create a custom Debian virtual machine installation using preseed and virt-install


6 min read


Debian preseed is a method for automating the installation of Debian, Ubuntu, and other Debian derivatives. By using a preseed file (preseed.cfg), we can predefine answers to the questions that the installer asks, allowing for an unattended installation process. This is particularly useful for deploying multiple systems with a consistent configuration, saving time and reducing the potential for human error.

We'll start with a simple preseed file to get the basic installation up and running. From there, we'll gradually build upon it, incorporating features like full disk encryption for enhanced security, adding extra packages to tailor the system to our needs, and running a few commands for post-install configuration to ensure everything is set up correctly.

Simple preseed

Let's start with the following preseed:

# Localization and keymap (US)
d-i debian-installer/locale string en_US
d-i keyboard-configuration/xkb-keymap select us

# Automatic network configuration
d-i netcfg/choose_interface select auto
d-i netcfg/get_hostname string debian
d-i netcfg/get_domain string local

# Debian mirror settings
d-i mirror/country string manual
d-i mirror/http/hostname string
d-i mirror/http/directory string /debian
d-i mirror/http/proxy string 

# Account setup (root/toor and user/resu)
d-i passwd/root-password-crypted password $6$I8U7yNFep4gy/oTc$9jqHSPtccHF7jsE4k1astCfy2Ua/UX5fFHXU1mKWaXsU6shipTBmdV5pxa2vcw2xpFkm7BdzZT1E9BBbLG0jY/
d-i passwd/root-password-again-crypted password $6$I8U7yNFep4gy/oTc$9jqHSPtccHF7jsE4k1astCfy2Ua/UX5fFHXU1mKWaXsU6shipTBmdV5pxa2vcw2xpFkm7BdzZT1E9BBbLG0jY/
d-i passwd/user-fullname string User
d-i passwd/username string user
d-i passwd/user-password-crypted password $6$HidIccPq4vpzdXSs$KBviV/Z7ZavtDpoCnOxAqfZfSm6VS8G9n14NL1K39vYOZ.uRuEq996U/C1qO25l.L3wgaXJtigpYnBRlH2/8T/
d-i passwd/user-password-again-crypted password $6$HidIccPq4vpzdXSs$KBviV/Z7ZavtDpoCnOxAqfZfSm6VS8G9n14NL1K39vYOZ.uRuEq996U/C1qO25l.L3wgaXJtigpYnBRlH2/8T/

# Clock and time zone setup: UTC
d-i clock-setup/utc boolean true
d-i time/zone string UTC

# Partitioning: guided partitioning, everything in the same partition
d-i partman-auto/disk string /dev/vda
d-i partman-auto/method string regular
d-i partman-auto/choose_recipe select atomic
d-i partman/choose_partition select finish
d-i partman/confirm write_new_label boolean true
d-i partman/confirm boolean true
d-i partman/confirm_nooverwrite boolean true
d-i partman/confirm_nochanges boolean true

# Package selection: just SSH server
tasksel tasksel/first multiselect standard, ssh-server

# Don't participate in the package usage survey
popularity-contest popularity-contest/participate boolean false

# Install the bootloader on /dev/vda
d-i grub-installer/only_debian boolean true
d-i grub-installer/with_other_os boolean false
d-i grub-installer/bootdev string /dev/vda

# Finishing up
d-i finish-install/reboot_in_progress note

The Debian Installer (d-i) is a set of tools and scripts used to install Debian and its derivatives. In the preseed file, it automates the installation process by providing predefined answers to installation questions.

In order, this preseed file does the following:

  • Setsen_US locale and US keyboard

  • Automatic network configuration and no proxy for apt

  • Setsroot password: toor

  • Create new user called user, with a password of resu

  • Sets the timezone to UTC

  • Automatic partitioning

  • Installs SSH server

  • Doesn't participate in package survery

  • Adds bootloader


  • Hashes for passwords can be generated with openssl passwd -6 (Install openssl package if necessary).

  • When the system is installed, the output from the kernel won't be displayed at boot time, so it will go from "Loading initrd" to the login/password prompt in the console. In most cases that's not an issue, but when something is taking a long time at boot, it will look like it's stuck at "Loading initrd". See below at the end of the Full Disk Encryption section to change that.


Assuming the preseed file is in /tmp/preseed.cfg, run the following for an unattended installation:

sudo virt-install --install debian12 \
    --graphics none \
    --console pty,target_type=serial \
    --noautoconsole \
    --initrd-inject /tmp/preseed.cfg \
    --extra-args="console=ttyS0,115200n8 preseed/file=/preseed.cfg"

Let's skip the first few parameters (as they are explained in a previous blog post) and go over the changes:

  • --initrd-inject /tmp/preseed.cfg: We are injecting a preseed file (into /)

  • preseed/file=/preseed.cfg: in the kernel arguments, we are specifying the preseed is a file and its path.

Full disk encryption

For a FDE, we need to replace the d-i section with partman in the above preseed with the following (we're using a passphrase of password1):

# Partitioning
d-i partman-auto/disk string /dev/vda
d-i partman-auto/method string crypto
d-i partman-lvm/device_remove_lvm boolean true
d-i partman-md/device_remove_md boolean true
d-i partman-lvm/confirm boolean true

# You can choose one of the three predefined partitioning recipes:
# - atomic: all files in one partition
# - home: separate /home partition
# - multi: separate /home, /usr, /var, and /tmp partitions
d-i partman-auto/choose_recipe select atomic

# This makes partman automatically partition without confirmation.
d-i partman-partitioning/confirm_write_new_label boolean true
d-i partman/choose_partition select finish
d-i partman/confirm boolean true
d-i partman/confirm_nooverwrite boolean true

# Encrypted LVM
d-i partman-crypto/passphrase password password1
d-i partman-crypto/passphrase-again password password1
d-i partman-crypto/dmraid/erase_disks boolean false
d-i partman-auto-crypto/erase_disks boolean false

d-i partman-auto-lvm/guided_size string max
d-i partman-auto/choose_recipe select atomic
d-i partman-auto-lvm/new_vg_name string debian-vg
d-i partman-lvm/device_remove_lvm boolean true
d-i partman-md/device_remove_md boolean true
d-i partman-lvm/confirm boolean true

# Filesystems
d-i partman-auto-lvm/new_vg_name string debian-vg
d-i partman-lvm/choose_partition string finish
d-i partman-lvm/confirm boolean true
d-i partman-lvm/confirm_nooverwrite boolean true
d-i partman/confirm_write_new_label boolean true
d-i partman/choose_partition select finish
d-i partman/confirm boolean true
d-i partman/confirm_nooverwrite boolean true

In this automated disk partitioning setup for a BIOS system using /dev/vda, we configure full-disk encryption (FDE) with LVM and LUKS. The configuration creates a small /boot partition with ext2, a root partition that occupies the majority of the disk, and a 2GB swap partition at the end. The entire disk is encrypted with LUKS, and the logical volumes for root and swap are managed within an LVM volume group named crypt.

Additionally, we need to run the following two commands after the installation:

sed -i 's/GRUB_CMDLINE_LINUX=""/GRUB_CMDLINE_LINUX="console=ttyS0,115200n8"/' /etc/default/grub

While text gets displayed on the console, the output requesting to enter the passphrase (as well as the output from the kernel when booting) doesn't get shown unless we force the console in the kernel parameters, and it looks like the boot process is hanging at loading the initrd.

When integrating it into the preseed file, we need to modify preseed/late_command, which now looks like:

d-i preseed/late_command string \
    in-target sed -i 's/GRUB_CMDLINE_LINUX=""/GRUB_CMDLINE_LINUX="console=ttyS0,115200n8"/' /etc/default/grub; \
    in-target update-grub;

Adding packages

If we want to add more packages, we need to add them on a line starting with d-i pkgsel/include. If we add Docker, it will look like:

d-i pkgsel/include string

What else can we do?

Using the preseed/late_command section, we can script it however we want. For multiline or multiple commands, we need to use \, and end each command with ;. Bear in mind that the installation is done in a chroot (in /target). Any command that needs to be run inside the chroot environment must be preceded by in-target.

d-i preseed/late_command string \
    echo "Welcome to your preseeded VM" >> /target/etc/motd; \
    in-target usermod -aG sudo user; \

When adding in-target before the command, it's a command that is run inside the installation.

In this example, the first command appends a message to the MOTD. The installed environment is in /target, and the MOTD is in /etc/motd. If we wanted to use in-target, it would have been in-target echo "Welcome to your preseeded VM" >> /etc/motd. The second command runs inside the chroot environment and adds the user user to the sudo group, to allow it to use sudo.

It can sometimes be tricky to do certain things due to the limitations of the installation process, and it takes a bit of trial and error to figure out how to do it properly or find alternate ways to achieve what we want. The first thing the Debian installer does is validate our preseed file, and if it isn't valid, it will say so and wait. Thus, it's important to have the console (either via virt-manager or via virsh console) as early as possible during these times; otherwise, if we arrive later, nothing will happen on the screen, and it will look like it's doing nothing or hanging.