From time to time, as a developer you need to do something on a full-fledged virtual machine (VM). Maybe you need to reproduce an issue or perform integration tests in an environment configured as close as possible to the production environment, including kernel, system services, firewall settings, authentication policies, and anything not easily reproduced in a container. In this article, I demonstrate a quick and easy way to create such a VM using the standard libvirt found in any Linux distribution, and Kickstart scripts supported by Red Hat Enterprise Linux (RHEL).
Most developers target production servers, which are very different from their work systems. Whether you run macOS, Windows, or a desktop-oriented Linux distribution (such as Fedora Linux), your target production system runs a server-oriented distribution, such as RHEL. Possibly, you must target multiple distributions, or at least multiple releases of the same distribution, across different production systems.
Automating the provisioning of fully-configured test VMs
Developers need a way to quickly provision and configure different VMs with settings that emulate many different production environments. You could use a number of automation technologies, such as Vagrant and Ansible, to set up a VM, but you don't need them to get started. The standard RHEL installer, named Anaconda, already provides a powerful automation feature called Kickstart.
By using Kickstart together with features of the standard Linux virtualization stack (libvirt + Qemu + KVM) you can build a simple set of scripts, which you can store in a Git repository, to provision highly customized test VMs.
These scripts are useful for even developers not running Linux desktops. Windows users can take advantage of those scripts on any distribution that supports Windows Subsystem for Linux (WSL2) by using nested virtualization, creating a RHEL VM inside the WSL VM. You can also use these scripts on cloud instances that support nested virtualization.
MacOS users can take advantage of Kickstart scripts (without the libvirt scripts). Thanks to Podman Desktop, which relies on Podman Machine and macadam, you have an easy way to create test VMs preconfigured with RHEL or any other Linux distribution, and then use nested virtualization on those VMs.
This article first describes a common network-based workflow for automatic provisioning of RHEL machines using Kickstart. Then I demonstrate how libvirt can simplify the workflow so that no network services are required, meaning you can use rootless VMs without administrator access to local machines or networks.
From network to local automatic provisioning
To people who haven't had the pleasure of using it, Kickstart technology from the RHEL installer can be surprisingly powerful.
Many years ago, before the availability of cloud-based Red Hat Training and Certification virtual classrooms, I was a Red Hat Certified Instructor. All certified instructors carried a USB drive that enabled them to provision classrooms for 12 students, already customized to a specific course, in about 30 minutes. When I arrived in the classroom, I would boot the instructor machine using this USB drive, and it automatically configured the machine with DHCP, DNS, BOOTP, and HTTP services for the classroom. Then I would turn each student machine on, select network boot in BIOS, and those machines would be configured as hypervisors, running the number of VMs required by the specific course. I didn't have to wait for one student machine to finish before moving on the next one, so multiple machines were automatically configured in parallel.
A process for automated, network-based installation
You can add the ks argument to the kernel command line to prompt a computer to fetch a Kickstart file from a web server. The Kickstart file directs the installer to fetch RPM packages from a specified server.
The kernel command line is entered manually, by interactive editing the GRUB menu from the standard RHEL installation media, or it can be preconfigured on the network boot server. You can use the reposync command to populate a web server with RPM packages, or just copy the files from the installation media.
The full process, as illustrated in Figure 1, on a computer booting to a RHEL installer is:
- BIOS sends DHCP request for network configuration. This step requires a valid DHCP server.
- BIOS loads kernel and initial RAM disk (initrd) using PXE, BOOTP, and UEFI. This step requires a network boot server.
- BIOS invokes the kernel passing a
ksargument. - Kernel starts Anaconda, which gets the Kickstart script location from the kernel.
- Anaconda downloads the Kickstart script using HTTP. This step requires a valid web server with the Kickstart in an accessible location.
- Anaconda downloads RPM packages specified in the Kickstart script using HTTP. This step requires an RPM package server.

It's easy to find instructions on the internet to perform similar network-based processes because they make sense for IT operations teams, who would configure all required network infrastructure only once, and then use it to provision a large number of machines. The same infrastructure could support multiple RHEL releases, and also provide RHEL updates.
Red Hat Satellite makes this kind of infrastructure especially easy. Large IT operations teams are used to this infrastructure, but it can be intimidating for a developer.
A process for network-based installation, without networking infrastructure
For developers, preparing the required network infrastructure may be perceived as too much work. And besides, it wastes memory and CPU cycles on their work machines. A developer might reasonably want a non-networked process.
It's not difficult to configure a local web server to provide the required files (the Kickstart script and RPM packages) and then use libvirt's direct kernel loading feature to skip both the virtual BIOS and the network boot server to install a VM directly from the local web server. This saves you the trouble of configuring network infrastructure, and avoids potential conflicts with DHCP services already running on your local network.
The local-only version of the workflow, illustrated in Figure 2, looks like this:
- Libvirt loads the kernel and initrd from a drive on localhost.
- Libvirt invokes the kernel passing a
ksargument, which exists on a drive on localhost. - Within the RHEL virtual machine that's booted, the kernel starts Anaconda, which receives the Kickstart script location from the kernel.
- Anaconda downloads the Kickstart script using HTTP from a local web server running on localhost.
- Anaconda downloads RPM packages using HTTP from the local web server.

If you have a good internet connection, you could stop here, and just use the Red Hat Content Delivery Network (CDN) instead of providing your own web server with RHEL packages.
A process for local installation, without any network requirement
The good news for developers who don't want to rely on an external network connection is that you don't really even need a web server. You can use libvirt's features to also load the Kickstart script and RPM packages from a local directory, instead. You would end up with a longer virt-install command, but you don't need to deal with a web server setup, SELinux file contexts, port redirections, firewalls, and container volumes that a web server setup could require.
The only modification to the workflow, as illustrated in Figure 3, involves a change to the initrd and the inclusion of virtiofs to access a virtual filesystem.
- Libvirt loads the kernel and a modified initrd.
- Libvirt invokes the kernel passing a
ksargument. - Within the RHEL virtual machine that's booted, the kernel starts Anaconda, which receives the Kickstart script location from the kernel.
- Anaconda reads the Kickstart script from initrd.
- Anaconda reads RPM packages from a virtiofs device.

Now that you understand why there are a simpler process than what you may have found elsewhere on the internet, and how they work, here are two examples of these processes you can try on your own Linux machine.
Disconnected installation from the RHEL DVD media
The first example performs a disconnected installation, which requires only a copy of the RHEL installation media and no internet access.
The resulting VM can't install additional packages or updates on Day 2, unless you either register it to Red Hat with an internet connection, or configure it with access to a copy of the installation media, but this is the fastest way of provisioning a short-lived test VM.
You can get RHEL and other Red Hat products at no cost by registering to the Red Hat Developer program and agreeing to the terms of the Red Hat Developer Subscription for Individuals. With that, you gain access to the Red Hat Developer product downloads for RHEL. You can download ISO images for the installation media for the latest stable releases of RHEL.
Important: Do NOT click the first download links you see! Scroll down to the All Downloads heading, and download the DVD ISO image.
All scripts from this article are available in a public Git repository, which you can clone to follow these instructions and later adapt to your needs.
$ git clone https://github.com/flozanorht/kickstart.gitCreate a working directory and copy the contents of the dvd-iso directory.
$ mkdir temp-rhel-vm
$ cd temp-rhel-vm
$ cp -r ../kickstart/dvd-iso/* .Now review the kickstart script dvd.ks. It starts with a very basic set of Kickstart commands, which prepare a system for an unattended installation by setting the locale, keyboard layout, automatic partitioning, and dynamic network configuration. It then sets a minimal set of packages, which includes the Apache web server.
lang en_US.UTF-8
keyboard us
timezone Etc/UTC --utc
zerombr
clearpart --all --initlabel
reqpart --add-boot
part / --grow --fstype xfs
network --bootproto=dhcp
rootpw --lock
cdrom
text
reboot
%packages
@^Minimal Install
httpd
%endThe Kickstart script finishes with a %post section, which does the really interesting stuff. Some of these settings, such as creating an initial user and setting the hostname, could be performed by Kickstart commands, but I found that doing it in a %post section is more portable with image mode for RHEL.
%post --log=/var/log/anaconda/post-install.log --erroronfail
useradd -g wheel core
echo "core:redhat123" | chpasswd
mkdir /home/core/.ssh
cat > /home/core/.ssh/authorized_keys << EOFSSH
REPLACE_WITH_SSH_PUB_KEY
EOFSSH
echo "rhel-dvd" > /etc/hostname
chmod 644 /etc/hostname
systemctl enable httpd.socket
firewall-offline-cmd --zone=public --add-service=http
mkdir -p /mnt/host-files
mount -t virtiofs -o ro host-files /mnt/host-files
cp /mnt/host-files/index.html /var/www/html
%endThe commands to enable the httpd socket with systemd, and to enable the http service with firewalld, make the system ready to function as a web server immediately after installation. The final command copies HTML files from the host-files virtiofs device to the default Apache document root.
There is a REPLACE_WITH_SSH_PUB_KEY placeholder in the example file, which you must replace with the contents of a valid public SSH key that has access to the VM, especially if you don't like the idea of having a default user with a well-known password!
To generate an SSH key pair, and then insert the public part of it in the Kickstart file:
$ ssh-keygen -N '' -f vm-key -C 'initial key for test VMs'
$ SSH_PUB_KEY=$( cat vm-key.pub )
$ sed -i "s|REPLACE_WITH_SSH_PUB_KEY|$SSH_PUB_KEY|" dvd.ksNow review the virt-install.sh script, which creates the test VM. It's just a long virt-install command, which starts by setting the VM size and user mode networking, with a couple of forwarded ports.
#!/bin/bash
virt-install --name rhel-dvd --os-variant rhel9.5 \
--vcpus 2 --ram 4096 --disk size=20 \
--network passt,portForward0=8022:22,portForward1=8080:80 \
--location ~/Downloads/rhel-9.6-x86_64-dvd.iso \
--initrd-inject ./dvd.ks \
--memorybacking source.type=memfd,access.mode=shared \
--filesystem $PWD/html,host-files,driver.type=virtiofs \
--graphics none \
--extra-arg console=ttyS0 \
--extra-args inst.ks=file:/dvd.ksThe --location option specifies direct kernel loading, which bypasses the virtual BIOS and bootloader and enables the virt-install command to control the kernel command line. It also enables the script to use the --initrd-inject option to dynamically modify the initrd to insert the Kickstart script so it's available to Anaconda before it configures networking or additional storage devices.
The --memorybacking and --filesystem directives create a virtual device (using the virtiofs driver) to make a host directory available to the VM. Notice the name of the virtual device is set to host-files, which is referred to in the Kickstart script.
Finally, the --graphics and --extra-arg options configure a real text mode console (instead of a virtual VGA graphics card) using a virtual serial port, which enables you to capture all kernel boot messages, Anaconda messages, and systemd messages by redirecting standard output to a file. This is especially useful for troubleshooting, should something go wrong while installing your VM. It also enables you to create your VMs over SSH on a remote system.
Run the script and wait a few moments while your terminal is flooded with kernel and installation messages:
$ bash virt-install.shThe end result is a RHEL login prompt. Log in with the user name core and the password redhat123. Or press Ctrl+] to return to your host shell.
You can use the first forwarded port to open a SSH session to the VM:
$ ssh -i vm-key -p 8022 core@127.0.0.1You can verify that the VM is not registered with Red Hat, which prevents it from downloading additional packages or updates:
$ sudo subscription-manager status
+-------------------------------------------+
System Status Details
+-------------------------------------------+
Overall Status: Unknown
System Purpose Status: Unknown
$ sudo dnf search podman
Updating Subscription Management repositories.
Unable to read consumer identity
This system is not registered with an entitlement server. You can use "rhc" or "subscription-manager" to register.
No matches found.You can also use the second forwarded port to access the web server running inside the VM:
$ curl 127.0.0.1:8080
RHEL VM installed offlineYou can use your virtiofs volume to provide any kind of files to the VM, including additional RPM packages, container image layers, or SQL scripts.
Connected installation from the RHEL boot media
This second example uses a network-based installation, which can use the smaller Boot ISO instead of the full DVD ISO. This process takes longer to install because of the time spent downloading RPM packages from Red Hat over the internet, but it creates a system using the latest packages, instead of whatever versions are in the latest DVD ISO.
This process also creates a VM that's subscribed to Red Hat, so it can easily download additional packages and updates on Day 2. It's a better method for provisioning long-lived VMs.
To use this method, you need an activation key, which is used to authenticate to the Red Hat Customer Portal and register your VM. It works with your free Developer subscription.
On the Downloads page from Red Hat Developer (or any other page on the site, once you're authenticated), click the user icon in the top right corner of your web browser window. This displays your user name and email, and a series of links to configure your user, community, and certification profiles. Click Subscriptions to leave the Red Hat Developer site and enter the Red Hat Subscription Management page of the Red Hat Customer Portal.
Look for the Systems panel and click Activation keys. This opens the Activation Keys page of the Red Hat Hybrid Cloud Console, and probably shows no activation keys for your account. Take note of the Organization ID displayed just below the Activation Keys heading, because you'll need that value later.
Click the Create activation key button to start the assistant. On the first page, you can leave the autogenerated name as is, and optionally provide a description. Click Next three times, until you reach the Review tab, and then click Create. Then dismiss the information pop-up, which states the activation key was created.
Use the values from the Red Hat Hybrid Cloud Console to set two shell variables on your developer machine (the values shown here are examples only):
$ RHSM_ORG=1234567
$ RHSM_KEY=12345678-90ab-cdef-1234-567890abcdefYou can keep using the same work directory you created for the previous example, and just copy the new scripts over it. You can reuse the same SSH key pair that you already generated.
$ cp -r ../kickstart/boot-iso/* .Review the boot.ks Kickstart script. It contains the same commands as the previous script, except that the cdrom command is replaced by an rhsm command, which uses your activation key to authenticate to Red Hat and download RPM packages:
rhsm --organization REPLACE_WITH_ORG_ID --activation-key REPLACE_WITH_ACTIVATION_KEYAlter the working copy of the Kickstart script to include the values of the shell variables you set after creating your activation key:
$ sed -i "s/REPLACE_WITH_ORG_ID/$RHSM_ORG/" boot.ks
$ sed -i "s/REPLACE_WITH_ACTIVATION_KEY/$RHSM_KEY/" boot.ksThe new virt-install.sh script is also very similar to the script in the previous example. It changes the name of the VM and references the rhel*boot.iso instead of the rhel*dvd.iso installation image. It also forwards different local ports to the SSH and HTTP ports of the VM, so you can run both VMs in parallel.
Run the installation script:
$ bash virt-install.shLog in using the user name core and the password redhat123, as you did with the previous example VM. Alternately, detach from the VM console by pressing Ctrl+].
You can verify that the VM got a different host name, making it easy to differentiate between shells on each one, and that its web server contains a different HTML file. If you're running this from the host and not the VM, add port 8180 to the curl command:
$ curl 127.0.0.1
RHEL VM installed and registeredYou can also verify that this VM is able to connect to Red Hat to retrieve additional packages and updates:
$ sudo subscription-manager status
+-------------------------------------------+
System Status Details
+-------------------------------------------+
Overall Status: Disabled
Content Access Mode is set to Simple Content Access. This host has access to content, regardless of subscription status.
System Purpose Status: Disabled
$ sudo dnf search podman
Updating Subscription Management repositories.
Red Hat Enterprise Linux 9 for x86_64 - BaseOS 14 MB/s | 95 MB 00:06
Red Hat Enterprise Linux 9 for x86_64 - AppStre 22 MB/s | 79 MB 00:03
Last metadata expiration check: 0:00:07 ago on Mon 29 Dec 2025 08:49:17 PM UTC.
========================= Name Exactly Matched: podman =========================
podman.x86_64 : Manage Pods, Containers and Container Images
[...]Now that you have your local VMs ready, you can manage them using the Cockpit web interface, the virsh command, the virt-manager or Boxes GNOME applications, and other tools from the libvirt suite. Or just destroy them, knowing that you can quickly recreate them, when you need them.
Moving forward
The two examples I've provided in this article install RHEL using package mode, but they can be adapted to install RHEL using image mode. You can see an example of using virtiofs in a %pre section of a Kickstart script to make a bootc container image available to Anaconda, without requiring that the image is pushed to a container registry. Or you could use direct kernel loading to give Anaconda a Kickstart file that refers to a bootc container image in a container registry.
The examples here are intentionally simple, but you don't need to embed all your customization scripts on your Kickstart files. They can just invoke external scripts, which are made available to Anaconda using virtiofs. Keep the scripts in a Git repository to make them easy to test and troubleshoot in isolation. Of course, you can also use Red Hat Ansible Automation Platform, with or without RHEL system roles, to interact with or to manage your VMs.
The examples here install a VM running RHEL 9.6, but they also work with older and newer releases of RHEL. You don't need to declare the exact RHEL release with the --os-variant option of the virt-install command.
You can find reference documentation about Kickstart scripts on the RHEL product docs and on the upstream community docs. Other Linux distributions, such as Fedora Linux and CentOS Stream, also use the Anaconda installation program and support Kickstart scripts.
I know some developers use scripts similar to the examples here in their CI/CD workflows, because creating a nested VM in a cloud instance (not necessarily a bare metal instance) is faster and easier than dealing with cloud provider proprietary APIs (and sometimes also cheaper). This gives them the advantage of using the same scripts for local development.
Special thanks to Clemens Lang, Marc-André Lureau, Peter Larsen, and Sergio Correia for reviewing drafts of this article.