Building Flashable Images For Your Fedora IoT Remix

It’s nice to be able to pick an RPM-OSTree based system like Fedora IoT and build a remix based on it with tools like RPM-OSTree-Engine. But you know what? Maybe it’s not enough to just manage your Remix’ OSTree, host it somewhere publicly and to automate the whole build process. Maybe you want something more, the next step in OS distribution and management? Well I don’t know about you but hell did I want that feature!

With support and funds from my company I hired someone investigating and implementing this into RPM-OSTree-Engine. After some polishing and refactoring with 0.3.3 we can now start building flashable raw images of Fedora 33 IoT for your Raspberry Pi and Compute Modules.

Let me walk you through how that’s done manually with the RPM-OSTree-Engine container before showing you how it’s automated via Gitlab CI. At the end I’ll talk a bit on the implementation details, in case you wonder and consider implementing something similar looking for an inspiration.

The Art Of Creating OS Images

We’ll use so called loopback devices to mount a file as partitioned virtual SD Card where we then install the OSTree system. In order to do this the loop kernel module has to be available, which most modern systems should provide. Another dependency is SELinux if we intend to build Fedora IoT. Overall it’s recommended to use fairly up to date systems to avoid problems with SELinux policies.

Assuming you have a RPM-OSTree repository laying around from previous play through now all you need to do is running the rpm-ostree-engine-image command:

sudo podman run --privileged --rm -it -v $(pwd):/mirror -w /mirror rpm-ostree-engine-image --ref=OSTreeBeard/develop/x86_64 --mirror=/mirror/.deploy-repo

If you want to build an image from a remote RPM-OSTree repository you can additional specify the remote address using the --origin= option:

sudo podman run --privileged --rm -it -v $(pwd):/mirror -w /mirror rpm-ostree-engine-image --ref=OSTreeBeard/develop/x86_64 --origin=

Or even combine both options to get an image from your local repository with a public update channel already configured as an alternative to adding a OSTree remotes.d conf.

With the --output= option you can control how the output image is named. It’s default name is os-iot.raw and it will be placed in the working directory. With something like OSTreeBeard-$(date +%Y%m%d) you can get a filename like OSTreeBeard-20210325.raw.

The command requires sudo because the virtualization requires access to the host system. For access to the loopback devices and in order to use dd on os-iot.raw we also need --privileged.

In order to compress the output raw-image you can use the xz command:

xz -0 -T0 ./os-iot.raw

This will give you a file named os-iot.raw.xz which is compatible with fedora arm-image-installer.

I have yet to test if this works cross-architecture. I’m currently building the images on hosts of the respective architecture simply because I already have them set up.

Let Gitlab CI Do The Repetition

With the RPM-OSTree-Engine templates you can build a .gitlab-ci.yml that automatically builds images from provided RPM-OSTree repositories. It’s intended use is as follow up job to building RPM-OSTree commits, but eventually this is up to you. You just need to get the sources at the designated places. The most basic example would be:

  - project: 'os-forge/rpm-ostree-engine'
      ref: 'v0.3.4'
      file: 'gitlab-ci-template.yml'


  stage: image
  extend: .build-ostree
    - arm64
    - selinux

  stage: image
  extend: .build-image
    - arm64
    - selinux

The template jobs assume that your repository is present under /remote-storage/repo in both jobs. How the repo get’s there is up to you. I use SSHFS in production. In the rpm-ostree-engine-example repository you can find a more complete example .gitlab-ci.yml that makes use of caches to transport the resulting RPM-OSTree repository over to the image build job.

By default the image build job will upload the resulting os-iot.raw.xz as artifact you can then download in gitlab UI. Make sure that the artifacts file size quotas for your gitlab repository are high enough. A typical Fedora IoT 33 image will take up to 750 MB.

Take A Look At The Guts - Eww

The implementation can be found in resources/bin/

We begin with writing some zeros into a file which we’ll later mount as virtual device. Using parted we partition this file before setting up the loopback device and mounting the file. Next we can create filesystems, prepare the boot partition, etc. The interesting part is when we use ostree to initialize a new OSTree based system and deploy our latest commit onto the root partition. After that we need to setup some common folders and SELinux permissions. Finally we need to place pre-compiled fstab, grub.cfg and bootloader for RPi3 and RPi4. Thanks to the coreos-assembler and osbuild projects for pointing that out.

Since we are aiming to run this in a container the container has to be executed with CAP_MAC_ADMIN capability in order to have access on mknod and loopback devices. Apparently dd also needs permission to write to os-iot.raw. For simplicity I’ve resorted to --privileged for now. Apart from that rpm-ostree-engine-image or podman for that matter have to be run with sudo so they are allowed to do their magic. That’s something I have to investigate more.

We found that it might be necessary to “probe” the loopX devices with mknod before trying to allocate them with losetup. I have no idea why though. That’s why you will find that strange for-loop in the Gitlab-CI template probing those devices.

Any thoughts of your own?

Feel free to raise a discussion with me on Mastodon or drop me an email.


The text of this post is licensed under the Attribution 4.0 International License (CC BY 4.0). You may Share or Adapt given the appropriate Credit.

Any source code in this post is licensed under the MIT license.