This guide will get you up and running with Image Mode on RHEL on Azure to help demonstrate how the technology works and how it can save you time and effort in managing your RHEL estate on Azure.
To get started, you’ll need the following:
- Container Registry
- Linux server for building containers
- Azure Subscription for deploying a RHEL VM in Image Mode
On an existing Linux system with “az” installed (and logged into an Azure account), let’s go ahead and begin by creating a resource group:
az group create -l eastus -n linux-vms
Now on that same system, let’s generate a ssh key for logging into vms:
ssh-keygen -f ~/.ssh/linuxkey -t rsa
And hit enter at both passphrase prompts. This will generate an ssh key for the purpose of this quickstart. In production, you may wish to actually use a passphrase to better protect your ssh keys.
Now, we need to create a container registry. To do this on Azure, let’s go to https://portal.azure.com/#create/Microsoft.ContainerRegistry and on the first screen, specify an active subscription and choose “linux-vms” as our resource group.
For the name of the registry, it must be unique across the Azure namespace, so try something with your name. In my case, my name is Karl and so I’m going to use “rhel10demo” for the purpose of this quick start. Anywhere you see that name, please replace it with your own registry name.
Your first page should look similar to this when completed:
Once you’ve filled those values in, click “Review + Create” and then on the next screen, click “Create”. Once the deployment finishes, you will have a container registry ready to go.
Back over on our Linux system, let’s now deploy to Azure a Linux server for building containers:
az vm create \
--resource-group linux-vms \
--name container-builder \
--image RedHat:rhel-raw:9_5:9.5.2024120516 \
--admin-username core \
--assign-identity \
--ssh-key-values ~/.ssh/linuxkey.pub\
--public-ip-sku Standard
Once this command has completed, you’ll have a public ip address specified as the value for “publicAddress” in the output from the above command. This document will use ip.ip.ip.ip in place of an actual ip address to show you where you’d place the value.
We also need to copy over our linuxkey.pub file to the container building machine so that we can inject this key into the container images we build:
scp -i ~/.ssh/linuxkey ~/.ssh/linuxkey.pub core@ip.ip.ip.ip:.ssh/
Now let’s get started building our image mode container image!
ssh core@ip.ip.ip.ip -i ~/.ssh/linuxkey
First, we want to install podman and git:
sudo dnf install -y podman git
Now we need to have the Azure CLI tooling so that we can interact with our Azure Container Registry, to do that, let’s run:
sudo rpm --import https://packages.microsoft.com/keys/microsoft.asc
We now need to edit /etc/yum.repos.d/azure-cli.repo and make sure that it has the following contents:
[azure-cli]
name=Azure CLI
baseurl=https://packages.microsoft.com/yumrepos/azure-cli
enabled=1
gpgcheck=1
gpgkey=https://packages.microsoft.com/keys/microsoft.asc
We can now run:
sudo dnf install azure-cli -y
Once this package has installed, we need to run:
az login
And follow the instructions.
Let’s now generate a token credential granting us registry access by doing the following: (Remember to replace “rhel10demo” with your own registry name.)
az acr token create --name mytoken --resource-group linux-vms --registry rhel10demo --scope-map _repositories_push
"creationDate": "2025-04-23T14:02:59.654151+00:00",
"credentials": {
"certificates": null,
"passwords": [
{
"creationTime": "2025-04-23T14:03:10.774839+00:00",
"expiry": null,
"name": "password1",
"value": "roF8leBAMlfeTS2Jb75WmaK/Z0WATcRK/sNowu6Str+ACRAZruef"
},
{
"creationTime": "2025-04-23T14:03:10.774858+00:00",
"expiry": null,
"name": "password2",
"value": "vzRJsZwsp4PX/ToXTmAFQitz2yYye+dqHZXbbUXZrD+ACRBPzPXi"
}
],
"username": "mytoken"
},
…
In the output, we will see two passwords. Let’s grab password1, which in our example looks like: (You’ll want to save this information as it’s the only time you’ll get to see it!)
"passwords": [
{
"creationTime": "2025-04-23T14:03:10.774839+00:00",
"expiry": null,
"name": "password1",
"value": "roF8leBAMlfeTS2Jb75WmaK/Z0WATcRK/sNowu6Str+ACRAZruef"
},
Here the password is the value of the “value” parameter, and we can now log into the registry with the following command: (Remember to replace “rhel10demo” with your registry name.)
podman login rhel10demo.azurecr.io -u mytoken -p roF8leBAMlfeTS2Jb75WmaK/Z0WATcRK/sNowu6Str+ACRAZruef
Next, we want to clone the git repository that has our MSSQL Image Mode example:
git clone https://github.com/mrguitar/rhel-mssql-bootc
As we are using Azure PAYG (Pay as you Go) images, we need to enable RHUI access inside of containers by running:
sudo sh -c "echo -e '/usr/share/rhel/secrets:/run/secrets\n/etc/pki/rhui:/etc/pki/rhui\n/etc/yum.repos.d/rh-cloud-base.repo:/etc/yum.repos.d/rh-cloud-base.repo' >> /etc/containers/mounts.conf"
sudo chmod 644 /etc/pki/rhui/{private,product}/*
Now, let’s change into the directory of our git repository and copy our auth.json for our container:
cd rhel-mssql-bootc
mkdir -p etc/ostree
cp /run/user/1000/containers/auth.json ./etc/ostree/
We can now examine the Containerfile.azure and see that it has instructions to build and bring up a system. In the from container, we already have MIcrosoft SQL Server installed, so we do not have to add that to our container:
FROM quay.io/mrguitar/rhel-mssql-bootc:latest
COPY etc/ /etc/
COPY 05-cloud-kargs.toml /usr/lib/bootc/kargs.d/
ARG sshpubkey
RUN if test -z "$sshpubkey"; then echo "must provide sshpubkey"; exit 1; fi; \
useradd -G wheel core && \
mkdir -m 0700 -p /home/core/.ssh && \
echo $sshpubkey > /home/core/.ssh/authorized_keys && \
chmod 0600 /home/core/.ssh/authorized_keys && \
chown -R core: /home/core
# install required packages and enable services
RUN dnf -y install \
WALinuxAgent \
cloud-init \
cloud-utils-growpart \
gdisk \
hyperv-daemons && \
dnf clean all && \
systemctl enable NetworkManager.service && \
systemctl enable waagent.service && \
systemctl enable cloud-init.service && \
echo 'ClientAliveInterval 180' >> /etc/ssh/sshd_config
# configure waagent for cloud-init to handle provisioning
RUN sed -i 's/Provisioning.Agent=auto/Provisioning.Agent=cloud-init/g' /etc/waagent.conf && \
sed -i 's/ResourceDisk.Format=y/ResourceDisk.Format=n/g' /etc/waagent.conf && \
sed -i 's/ResourceDisk.EnableSwap=y/ResourceDisk.EnableSwap=n/g' /etc/waagent.conf
We can now build the container image by running:
podman build --build-arg "sshpubkey=$(cat ~/.ssh/linuxkey.pub)" --no-cache -f Containerfile.azure
Now that we have our container registry configured and our image built, we can push the image to the registry. Let’s start by finding the image ID for our newly built image:
[core@container-builder rhel-mssql-bootc]$ podman images
REPOSITORY TAG IMAGE ID CREATED SIZE
<none> <none> ba4f948305db 2 minutes ago 3.64 GB
quay.io/mrguitar/rhel-mssql-bootc latest 534ac925516a 4 weeks ago 3.47 GB
We can now tag this image with the following command (Remember to replace “rhel10demo” with your registry name.):
podman tag ba4f948305db rhel10demo.azurecr.io/mssql-image-mode:azure
Now we can push our image to our registry:
podman push rhel10demo.azurecr.io/mssql-image-mode:azure
Now, let’s exit from the container building server and go create a new RHEL 9.5 VM that we will switch from package mode to image mode. This needs to be run on the system that you created the container building image on.
az vm create \
--resource-group linux-vms \
--name mssql \
--image RedHat:rhel-raw:9_5:9.5.2024120516 \
--admin-username core \
--assign-identity \
--ssh-key-values ~/.ssh/linuxkey.pub \
--public-ip-sku Standard
Once this machine starts, let’s ssh to the system:
ssh -i ~/.ssh/linuxkey core@ip.ip.ip.ip
Let’s start by installing podman:
sudo dnf install podman -y
Now we must login to our container registry using sudo so that podman running as root can download our container image: (We will use the credentials that were generated earlier in this quick start.)
sudo podman login rhel10demo.azurecr.io -u mytoken -p roF8leBAMlfeTS2Jb75WmaK/Z0WATcRK/sNowu6Str+ACRAZruef
and then we can run the following command to pull down our image mode image and deploy it to this system:
sudo podman run --privileged --pid=host -v /var/lib/containers:/var/lib/containers -v /:/target -v /home/core/.ssh/authorized_keys:/bootc_authorized_ssh_keys/root rhel10demo.azurecr.io/mssql-image-mode:azure bootc install to-existing-root --acknowledge-destructive --root-ssh-authorized-keys /bootc_authorized_ssh_keys/root
At this point, when the above command completes successfully, you can run:
sudo systemctl reboot
And when you reboot, your RHEL system will now be running in Image Mode! Let’s go ahead and log back in and see what’s different:
ssh -i ~/.ssh/linuxkey core@ip.ip.ip.ip
Let’s start by trying to create a file in /opt as root – something you could normally do on package mode RHEL:
sudo touch /opt/hello.txt
If you are on an image mode system, you’ll see this output:
touch: cannot touch 'hello.txt': Read-only file system
This is because the files in the container image are immutable and the only way to change them is to change the image and reboot into the image! This provides a significant new layer of security in that operating system files cannot be easily modified. You are still able to edit files in /etc and in home directories.
This image has Microsoft SQL Server installed and we can verify this by running:
sudo systemctl status mssql-server
You should see output similar to:
○ mssql-server.service - Microsoft SQL Server Database Engine
Loaded: loaded (/usr/lib/systemd/system/mssql-server.service; disabled; preset: disabled)
Active: inactive (dead)
Docs: https://docs.microsoft.com/en-us/sql/linux
Given that we want all virtual machines based on this image to have SQL Server running by default, let’s make that change to the container image. To do this, we’ll need to exit this shell and ssh into the container builder machine from earlier.
On that machine, run:
cd rhel-mssql-bootc
And then use this command to add “RUN systemctl enable mssql-server.service” to the end of Containerfile.azure:
echo >> Containerfile.azure && echo "RUN systemctl enable mssql-server.service" >> Containerfile.azure
Once this is done, we need to rebuild the image, tag the rebuilt image, and push the new image to our container registry: (Remember to replace “rhel10demo” with your registry name.)
podman build --build-arg "sshpubkey=$(cat ~/.ssh/linuxkey.pub)" --no-cache -f Containerfile.azure
podman images
REPOSITORY TAG IMAGE ID CREATED SIZE
<none> <none> b7322184d64e 9 seconds ago 3.67 GB
rhel10demo.azurecr.io/mssql-image-mode azure 237947455bd9 53 minutes ago 3.67 GB
quay.io/mrguitar/rhel-mssql-bootc latest e36c92d89714 4 days ago 3.5 GB
podman tag b7322184d64e rhel10demo.azurecr.io/mssql-image-mode:azure
cp etc/ostree/auth.json /run/user/1000/containers
podman push rhel10demo.azurecr.io/mssql-image-mode:azure
Once this is done, we can exit this machine and log back on to our image mode machine and run the following:
sudo bootc upgrade
sudo systemctl reboot
And watch the magic as the box reboots!
Now when you ssh in again, run:
sudo systemctl status mssql-server
This time, you should get output showing that SQL Server is running. Upon updating and rebooting all VMs based on this image, they’ll now be running SQL Server.
At this point, we can run the SQL Server demo by running:
sudo PATH=$PATH:/opt/mssql/bin/ /opt/mssql_demo.sh
This demo shows that the SQL Server is working on our RHEL machine running in Image Mode.
Any further changes that you want to make to this box can be pushed by changing the container image in the registry and calling a bootc upgrade.
Image Mode RHEL also ships with a timer that allows these boxes to check for updates on the weekend. This can be configured so that not all of your image mode RHEL boxes update at the same time.
With Image Mode RHEL, you don’t have to worry about system drift as you are always running off of a known image. Also, you’ll probably find yourself saving a lot of time by not having to log on to lots of systems. If you need to spin up 1,000 VMs with the same image, you can easily do that with the tooling we’ve shown you today on top of Azure!
Published May 19, 2025
Version 1.0abbottkarl
Microsoft
Joined March 10, 2025
Linux and Open Source Blog
Follow this blog board to get notified when there's new activity