A pre-configured operating system can be saved as a template for faster virtual machines creation as day 2 operation. In this tutorial, we will create a Rocky Linux 9 and Rocky Linux 8 template from cloud images of the said OS. This applies only to the server edition releases of Rocky Linux, as the desktop variant will require installation from a DVD ISO image, which is beyond the scope of this article.
Follow the following steps to create a customized OS template from Rocky Linux cloud images.
1 – Download Rocky Linux Cloud Images
Begin by downloading the Rocky Linux QCOW2 cloud images:
Use wget to directly download the latest generic Qcow2 images from source:
- Rocky Linux 9:
wget https://dl.rockylinux.org/pub/rocky/9/images/x86_64/Rocky-9-GenericCloud.latest.x86_64.qcow2
- Rocky Linux 8:
wget https://dl.rockylinux.org/pub/rocky/8/images/x86_64/Rocky-8-GenericCloud.latest.x86_64.qcow2
This should done on Proxmox server, or a KVM host where libguestfs-tools
can be installed.
2 – Define template variables
Login to your Proxmox host, and list available configured storage pools:
# pvesm status
Name Type Status Total Used Available %
local dir active 1884995968 10524288 1874471680 0.56%
local-zfs zfspool active 1875328912 857128 1874471784 0.05%
In this guide, we will be using local
storage pool. Next list configured networks:
# brctl show
bridge name bridge id STP enabled interfaces
vmbr0 8000.5ced8cedb886 no eno1
vmbr1 8000.5ced8cedb889 no eno4
- Rocky Linux 9:
# Image path
IMAGE=./Rocky-9-GenericCloud.latest.x86_64.qcow2
# VM ID / Template ID
TID=800
# Template name
TNAME=Rocky-9-Template
# Disk size
SIZE=20G
# Network bridge
BRIDGE=vmbr0
# RAM
RAM=2048
# CPU cores
CORES=1
# Storage pool
STORAGE=local
# Cloud Init user
CUSER=rocky
# Cloud Init user password
CPASS="Str0ngUserPassW0rd"
- Rocky Linux 8:
# Image path
IMAGE=./Rocky-8-GenericCloud.latest.x86_64.qcow2
# VM ID / Template ID
TID=801
# Template name
TNAME=Rocky-8-Template
# Disk size
SIZE=20G
# Network bridge
BRIDGE=vmbr0
# RAM
RAM=2048
# CPU cores
CORES=1
# Storage pool
STORAGE=local
# Cloud Init user
CUSER=rocky
# Cloud Init user password
CPASS="Str0ngUserPassW0rd"
NATed network creation guide: Creating Private Network Bridge on Proxmox VE with NAT
3 – Image customization using virt-customize
We will need the virt-customize
command which is provided by libguestfs-tools package. Let’s install it on Proxmox server or on a KVM node if you have one.
sudo apt update && sudo apt install libguestfs-tools
If installed properly, you should be able to check version.
virt-customize --version
We’ll use virt-customize to install basic packages, such as Vim, and qemu-guest-agent into the base cloud image.
# virt-customize -a $IMAGE --install vim,unzip,bash-completion,wget,curl,qemu-guest-agent
[ 0.0] Examining the guest ...
[ 8.2] Setting a random seed
[ 8.3] Setting the machine ID in /etc/machine-id
[ 8.3] Installing packages: vim unzip bash-completion wget curl qemu-guest-agent
[ 163.2] Finishing off
Use the same command to enable qemu-guest-agent service to start at boot.
# virt-customize -a $IMAGE --run-command 'systemctl enable qemu-guest-agent'
[ 0.0] Examining the guest ...
[ 7.6] Setting a random seed
[ 7.6] Running: systemctl enable qemu-guest-agent
[ 8.2] Finishing off
Set the default timezone for the OS. Replace UTC with your timezone, e.g `America/New_York”
# virt-customize -a $IMAGE --timezone "UTC"
[ 0.0] Examining the guest ...
[ 7.5] Setting a random seed
[ 7.6] Setting the timezone: UTC
[ 8.0] Finishing off
For better security, we recommend disabling SSH password authentication. To enable it, run the commands below:
# virt-customize -a $IMAGE --run-command 'sed -i "s/.*PasswordAuthentication.*/PasswordAuthentication yes/g" /etc/ssh/sshd_config'
[ 0.0] Examining the guest ...
[ 7.4] Setting a random seed
[ 7.5] Running: sed -i "s/.*PasswordAuthentication.*/PasswordAuthentication yes/g" /etc/ssh/sshd_config
[ 8.1] Finishing off
It is recommended to disable root user ssh login and use standard user set for cloud init.
# virt-customize -a $IMAGE --run-command 'sed -i "s/.*PermitRootLogin.*/PermitRootLogin no/g" /etc/ssh/sshd_config'
[ 0.0] Examining the guest ...
[ 7.2] Setting a random seed
[ 7.3] Running: sed -i "s/.*PermitRootLogin.*/PermitRootLogin no/g" /etc/ssh/sshd_config
[ 7.7] Finishing off
Relabel SELinux, as it is enforced by default.
# virt-customize -a $IMAGE --selinux-relabel
[ 0.0] Examining the guest ...
[ 7.4] Setting a random seed
[ 7.4] SELinux relabelling
[ 7.9] Finishing off
To disable SELinux completely, run the commands below.
# virt-customize -a $IMAGE --run-command ' sed -i "s/^SELINUX=.*/SELINUX=disabled/g" /etc/selinux/config'
[ 0.0] Examining the guest ...
[ 4.7] Setting a random seed
guest
[ 4.8] Running: sed -i "s/^SELINUX=.*/SELINUX=disabled/g" /etc/selinux/config
[ 4.9] Finishing off
4 – Create Rocky Linux OS template on Proxmox
Expand the image disk to the size defined in the SIZE variable.
# qemu-img resize $IMAGE $SIZE
Image resized.
Create a VM with set ID, RAM, CPU and Network bridge.
qm create $TID \
--name $TNAME \
--memory $RAM \
--cores $CORES \
--net0 virtio,bridge=$BRIDGE \
--scsihw virtio-scsi-pci \
--cpu host
We created a VM without disk attached. Import the base image we customized into the actual VM storage disk.
# qm importdisk $TID $IMAGE $STORAGE
importing disk './Rocky-8-GenericCloud.latest.x86_64.qcow2' to VM 801 ...
Formatting '/var/lib/vz/images/801/vm-801-disk-0.raw', fmt=raw size=107374182400 preallocation=off
...
transferred 89.6 GiB of 100.0 GiB (89.59%)
transferred 90.6 GiB of 100.0 GiB (90.59%)
transferred 91.6 GiB of 100.0 GiB (91.62%)
transferred 92.6 GiB of 100.0 GiB (92.62%)
transferred 93.6 GiB of 100.0 GiB (93.62%)
transferred 94.6 GiB of 100.0 GiB (94.62%)
transferred 95.6 GiB of 100.0 GiB (95.65%)
transferred 96.6 GiB of 100.0 GiB (96.65%)
transferred 97.6 GiB of 100.0 GiB (97.65%)
transferred 98.6 GiB of 100.0 GiB (98.65%)
transferred 99.6 GiB of 100.0 GiB (99.65%)
transferred 100.0 GiB of 100.0 GiB (100.00%)
transferred 100.0 GiB of 100.0 GiB (100.00%)
unused0: successfully imported disk 'local:801/vm-801-disk-0.raw'
After disk is imported, attach it to the VM created.
- Using local dir storage volume
# qm set $TID --scsihw virtio-scsi-pci --virtio0 $STORAGE:$TID/vm-$TID-disk-0.raw
update VM 801: -scsihw virtio-scsi-pci -virtio0 local:801/vm-801-disk-0.raw
- Using LVM or ZFS Pool:
# qm set $TID --scsihw virtio-scsi-pci --virtio0 $STORAGE:vm-$TID-disk-0
update VM 800: -scsihw virtio-scsi-pci -virtio0 local-zfs:vm-800-disk-0
Enable QEMU guest agent for the instance.
# qm set $TID --agent 1
update VM 801: -agent 1
Change boot order to start with VirtIO block device.
# qm set $TID --boot c --bootdisk virtio0
update VM 801: -boot c -bootdisk virtio0
Attach cloud init image:
# qm set $TID --ide2 $STORAGE:cloudinit
update VM 801: -ide2 local:cloudinit
Formatting '/var/lib/vz/images/801/vm-801-cloudinit.qcow2', fmt=qcow2 cluster_size=65536 extended_l2=off preallocation=metadata compression_type=zlib size=4194304 lazy_refcounts=off refcount_bits=16
ide2: successfully created disk 'local:801/vm-801-cloudinit.qcow2,media=cdrom'
Use of uninitialized value in split at /usr/share/perl5/PVE/QemuServer/Cloudinit.pm line 106.
generating cloud-init ISO
Create a custom SSH public key file and paste your SSH key into it. Alternatively, you can generate an SSH key pair using ssh-keygen
:
vim sshpub.key
Inject SSH key for the specified default user. Path to SSH public key can be changed accordinly.
# qm set $TID --sshkey ./sshpub.key
update VM 700: -sshkeys ssh-rsaxxxyyyzzz
Set default IP assignment to DHCP, and update username and password.
# qm set $TID --ciuser=$CUSER --cipassword="$CPASS" --ipconfig0 ip=dhcp
update VM 700: -cipassword <hidden> -ciuser rocky -ipconfig0 ip=dhcp
Assign a name to the VM using the following commands.
# qm set $TID --name $TNAME
update VM 700: -name Rocky-8-Template
Finally, convert the virtual machine into a template.
qm template $TID
5 – Creating a Virtual Machine from the Template
List available templates
# qm list
VMID NAME STATUS MEM(MB) BOOTDISK(GB) PID
800 Rocky-9-Template stopped 8192 10.00 0
801 Rocky-8-Template stopped 2048 100.00 0
Define all the variables required to create a VM from template.
TID=800 # Template ID
VMID=$(pvesh get /cluster/nextid) #ID of the VM to be created. This is auto-assigned by Proxmox VE
VMNAME=AppServer # Name of the VM
RAM=4096 # VM RAM size in MB
CORES=2 # CPU cores for the VM
It is also possible to define custom network configurations such as static IP address, gateway and netmask.
BRIDGE=vmbr0 # Name of the bridge attached to the VM
IP=192.168.10.11/24 # IP address (If using static IP addressing)
GW=192.168.10.1 # Default gateway IP Address
DNS=8.8.8.8 # Default DNS server
SDOMAINS="example.com" # Search domains, separate with , for multiple.
There are two standard options for cloning a template into virtual machine:
- Linked Clone – A VM created from this type uses less disk space but depends on the base VM template and cannot run without access to it.
- Full Clone – A VM created from a full clone is a complete copy and operates independently from the original VM template, but it requires the same amount of disk space as the original.
An example on full cloning of the template:
qm clone $TID $VMID --full --name $VMNAME --format qcow2
Linked clone example:
qm clone $TID $VMID --name $VMNAME
Modify CPU and RAM allocations:
# qm set $VMID --vcpus $CORES --cores $CORES
update VM 100: -vcpus 1
# qm set $VMID --memory $RAM
update VM 100: -memory 4096
Finally, update changes for the IP, DNS and Search domain(s) if you need static IP address:
# qm set $VMID --ipconfig0 ip=$IP,gw=$GW
update VM 100: -ipconfig0 ip=192.168.10.11/29,gw=192.168.10.1
# qm set $VMID --searchdomain $SDOMAINS
update VM 100: -searchdomain example.net
# qm set $VMID --nameserver $DNS
update VM 100: -nameserver 8.8.8.8
You can also provide a custom ssh public key to override the default.
qm set $VMID --sshkey $SSHKEY
Make sure the virtual machine is set to start on boot
qm set $VMID --onboot 1
The virtual machine can be started from CLI or web console
qm start $VMID