Building your own Atomic (bootc) Desktop

Photo by Jason Dela Cueva on Unsplash (cropped)

Bootc and associated tools provide the basis for building a personalised desktop. This article will describe the process to build your own custom installation.

Disclaimer

Building and using a custom installation is “at your own risk”. Your installation may be harder to find support for when compared with a mainstream solution.

Motivation

There has been an increasing interest in atomic distros, which offer significant benefits in terms of stability and security.

These distros apply updates as a single transaction, known as atomic upgrades, which means if an update doesn’t work as expected, the system can instantly roll back to its last stable state, saving users from potential issues. The immutable nature of the filesystem components reduces the risk of system corruption and unauthorised modifications as the core system files are read-only, making them impossible to modify.

If you are planning to spin off various instances from the same image (e.g. setting up computers for members of your family or work), atomic distros provide a reliable desktop experience where every instance of the desktop is consistent with each other, reducing discrepancies in software versions and behaviour.

Mainstream sources like Fedora and Universal Blue offer various atomic desktops with curated configurations and package selections for the average user. But what if you’re ready to take control of your desktop and customise it entirely, from packages and configurations to firewall, DNS, and update schedules?

Thanks to bootc and the associated tools, building a personalised desktop experience is no longer difficult.

What is bootc?

Using existing container building techniques, bootc allows you to build your own OS. Container images adhere to the OCI specification and utilise container tools for building and transporting your containers. Once installed on a node, the container functions as a regular OS.

The filesystem structure follows ostree specifications:

  • The /usr directory is read-only, with all changes managed by the container image.
  • The /etc directory is editable, but any changes applied in the container image will be transferred to the node unless the file was modified locally.
  • Changes to /var (including /var/home) are made during the first boot. Afterwards, /var remains untouched.

You can find the full documentation for bootc here: https://bootc-dev.github.io/bootc/

Creating your own bootc desktop

The approach described in this article uses quay.io/fedora/fedora-bootc as a base image to create a customizable container for building your personalised Fedora KDE atomic desktop.

Although tailored to KDE Plasma, most of the concepts and methodologies described here also apply to other desktop environments.

The kde-bootc repository

I published kde-bootc as a repository available in GitHub, and I will use it as a reference. It will help this explanation providing additional details, and a source to clone and experiment. You may wish to clone kde-bootc to following along.

Folder structure:

  • scripts/
  • system/
  • systemd/
  • Containerfile

scripts: Scripts to be ran from the Containerfile during building
system: Files to be copied to /usr and /etc
systemd: Systemd unit files to be copied to /usr/lib/systemd

Each file follows a specific naming convention. For instance a file /usr/lib/credstore/home.create.admin is named as usr__lib__credstore__home.create.admin

Explaining the Containerfile

The following will describe and show, step by step, the contents of the example Containerfile created.

Image base

The fedora-bootc project is part of the Cloud Native Computing Foundation (CNCF) Sandbox projects and  generates reference “base images” of bootable containers designed for use with the bootc project.

In this example, I’m using quay.io/fedora/fedora-bootc as the base image. The containerfile starts off with:

FROM quay.io/fedora/fedora-bootc

Setup filesystem

If you plan to install software on day 2, i.e. after the kde-bootc installation is complete, you may need to link /opt to /var/opt. Otherwise, /opt will remain an immutable directory that you can only populate from the container build.

RUN rmdir /opt
RUN ln -s -T /var/opt /opt

In some cases, for successful package installation, the /var/roothome directory must exist. If this folder is missing, the container build may fail. It is advisable to create this directory before installing the packages.

RUN mkdir /var/roothome

Prepare packages

To simplify the installation, and to have a record of installed and removed packages for future reference, I found it useful to keep them as a resource under /usr/share.

  • All additional packages to be installed on top of fedora-bootc and the KDE environment are documented in packages-added.

COPY --chmod=0644 ./system/usr__local__share__kde-bootc__packages-added /usr/local/share/kde-bootc/packages-added

  • Packages to be removed from fedora-bootc and the KDE environment are documented in packages-removed.

COPY --chmod=0644 ./system/usr__local__share__kde-bootc__packages-removed /usr/local/share/kde-bootc/packages-removed

  • For convenience, the packages included in the base fedora-bootc are documented in packages-fedora-bootc.

RUN jq -r .packages[] /usr/share/rpm-ostree/treefile.json > /usr/local/share/kde-bootc/packages-fedora-bootc

Install repositories

This section handles adding extra repositories needed before installing packages.

In this example, I’m adding Tailscale, but the same principle applies to any other source you may add to your repositories.

Adding repositories uses the config-manager verb, available as a DNF5 plugin. This plugin is not pre-installed by default in fedora-bootc, so it will need to be installed beforehand.

RUN dnf -y install dnf5-plugins
RUN dnf config-manager addrepo --from-repofile=https://pkgs.tailscale.com/stable/fedora/tailscale.repo

Install packages

For clarity and task separation, I divided the installation into two steps:

Installation of environment and groups.

RUN dnf -y install @kde-desktop-environment

And the installation of all other individual packages. The script will select all lines not starting with # passing them as arguments to dnf -y install. The --allowerasing option is necessary for cases like installing vim-default-editor, which would conflict with nano-default-editor, removing the latter first.

RUN grep -vE '^#' /usr/local/share/kde-bootc/packages-added | xargs dnf -y install –-allowerasing

PACKAGES-ADDED
# LibreOffice
libreoffice
libreoffice-help-en
# Utilities
vim-default-editor
git
....

Remove packages

Some of the standard packages included in @kde-desktop-environment don’t behave well and sometimes conflict with an immutable desktop, so we will remove them.

This is also an opportunity to remove software you may never use, saving resources and storage.

RUN grep -vE '^#' /usr/local/share/kde-bootc/packages-removed | xargs dnf -y remove
RUN dnf -y autoremove
RUN dnf clean all

The criteria used to remove some packages is listed below:

Conflict with bootc and its immutable nature.
plasma-discover-offline-updates
plasma-discover-packagekit
PackageKit-command-not-found

Bring unwanted dependencies.
tracker
tracker-miners
mariadb-server-utils
abrt
at
dnf-data

Deprecated services.
iptables-services
iptables-utils

Packages that are resource-heavy, or bring unnecessary services.
rsyslog
dracut-config-rescue

Configuration

This section will copy all necessary configuration files to /usr and /etc. As recommended by the bootc project, prioritise using /usr and use /etc as a fallback if needed.

Bash scripts that will be used by systemd services are stored in /usr/local/bin:

COPY --chmod=0755 ./system/usr__local__bin/* /usr/local/bin/

Custom configuration for new users’ home directories will be added to /etc/skel/. As an example you can customise bash.

COPY --chmod=0644 ./system/etc__skel__kde-bootc /etc/skel/.bashrc.d/kde-bootc

If you’re building your container image on GitHub and keeping it private, you’ll need to create a GITHUB_TOKEN to download the image. Further information is available at GitHub container registry.

COPY --chmod=0600 ./system/usr__lib__ostree__auth.json /usr/lib/ostree/auth.json

Users

I opted for systemd-homed users because they are better suited than regular users for immutable desktops, preventing potential drift in case of local modifications in /etc/passwd. Additionally, each user home benefits from LUKS encrypted volume.

The process begins when firstboot-setup runs, triggered by firstboot-setup.service during boot. It executes homectl firstboot, which checks if any regular home areas exist. If none are found, it searches for service credentials starting with home.create. to create users at boot.

The parameter below imports service credentials into the systemd service:

FIRSTBOOT-SETUP.SERVICE
...
ImportCredential=home.create.*

For more details, refer to the homectl and systemd.exec manual pages.

The homed identity file (usr__lib__credstore__home.create.admin) sets the user’s parameters, including username, real name, storage type, etc.

Common systemd-homed parameters:

  • userName: A single word for your username and home directory. In this example, it is admin.
  • realName: Full name for the user
  • diskSize: The size of the LUKS storage volume, calculated in bytes. For instance, 1 GB equals 1024x1024x1024 bytes, which is 1073741824 bytes.
  • rebalanceWeight: Relevant only when multiple user accounts share the available storage. If diskSize is defined, this parameter can be set to false.
  • uid/gid: User and Group ID. The default range for regular users is 1000-6000, and for systemd-homed users, it is 60001-60513. However, you can assign uid/gid for systemd-homed users from both ranges.
  • memberOf: The groups the user belongs to. As a power user, it should be part of the wheel group.
  • hashedPassword: This is the hashed version of the password stored under secret. Setting up an initial password allows homectl firstboot to create the user without prompting. This password should be changed afterwards (homectl passwd admin). The hash password can be created using the mkpasswd utility.

We are storing the identity file in one of the directories where systemd-homed expects to find credentials.

COPY --chmod=0644 ./system/usr__lib__credstore__home.create.admin /usr/lib/credstore/home.create.admin

For more information on user records, visit: https://systemd.io/USER_RECORD/

This section also creates a temporary password for the root user. As I will explain later, having a root user as an alternative login is important.

echo "Temp#SsaP" | passwd root -s

Subuid and Subgid:

Another key parameter to set up is the range for /etc/subuid and /etc/subgid for the admin user. This range is necessary for running rootless containers since each uid inside the container will be mapped to a uid outside the container within this range. Systemd-homed predefines ranges for uid/gid.

The available range is 524288…1879048191. Choosing 1000001 makes it easy to identify the service running in the container. For instance, if the container is running Apache with uid=48, the volume or folder bound to it will have uid=1000048.

echo "admin:1000001:65536">/etc/subuid
echo "admin:1000001:65536">/etc/subgid

For more information on available ranges, visit: https://systemd.io/UIDS-GIDS/

The next step will set up authselect to enable authenticating the admin user on the login page. To achieve this, we need to enable the features with-systemd-homed and with-fingerprint (if your computer has a fingerprint reader) for the local profile.

authselect enable-feature with-systemd-homed
authselect enable-feature with-fingerprint

Systemd services

I decided to install at least two services; One to complete the configuration during machine boot, to run commands that require systemd (firstboot-setup.service), and the other one to automate updates (bootc-fetch.service).

We are enabling, by default, the first systemd service firstboot-setup:

COPY --chmod=0644 ./systemd/usr__lib__systemd__system__firstboot-setup.service /usr/lib/systemd/system/firstboot-setup.service
RUN systemctl enable firstboot-setup.service

USR__LIB__SYSTEMD__SYSTEM__FIRTBOOT-SETUP.SERVICE
[Unit]
Description=Setup USERS and /VAR at boot
After=multi-user.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/bin/firstboot-setup
ImportCredential=home.create.*
[Install]
WantedBy=multi-user.target

And it runs the script below:

FIRSTBOOT-SETUP
# Setup hostname
HOST_NAME=kde-bootc
hostnamectl hostname $HOST_NAME
# Create user(s)
homectl firstboot
# Setup firewall to allow kdeconnect to functions
firewall-cmd --set-default-zone=public
firewall-cmd --add-service=kdeconnect --permanent

We are triggering bootc-fetch daily by a timer as a second systemd service:

COPY --chmod=0644 ./systemd/usr__lib__systemd__system__ bootc-fetch.service /usr/lib/systemd/system/bootc-fetch.service
COPY --chmod=0644 ./systemd/usr__lib__systemd__system__bootc-fetch.timer /usr/lib/systemd/system/bootc-fetch.timer


USR__LIB__SYSTEMD__SYSTEM__BOOTC-FETCH.TIMER
[Unit]
Description=Fetch bootc image daily
[Timer]
OnCalendar=*-*-* 12:30:00
Persistent=true
[Install]
WantedBy=timers.target

USR__LIB__SYSTEMD__SYSTEM__BOOTC-FETCH.SERVICE
[Unit]
Description=Fetch bootc image
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/bin/bootc update --quiet

This service replaces bootc-fetch-apply-updates, which would download and apply updates as soon as they are available. This approach is problematic because it causes your computer to shut down without warning, so it is better to disable by masking the timer:

RUN systemctl mask bootc-fetch-apply-updates.timer

How to create an ISO?

The instructions that follow will build the container locally. You need to do it as root so bootc-image-builder can use the image to make the ISO.

cd /path-to-your-repo
sudo podman build -t kde-bootc .

Then, outside the repository on a different directory, create a folder named output for the ISO image. And also you need to create the configuration file config.toml to feed the installer.

CONFIG.TOML
[customizations.installer.kickstart]
contents = "graphical"

[customizations.installer.modules]
disable = [
"org.fedoraproject.Anaconda.Modules.Users"
]

It instructs the installer to use the graphical interface and disable the module for user creation. We do not need to set up a user during installation, as this is already being taken care of.

Within the directory where ./output/ and ./config.toml exists, run bootc-image-builder utility which is available as a container. It must be run as root.

sudo podman run --rm -it --privileged --pull=newer \
--security-opt label=type:unconfined_t \
-v ./output:/output \
-v /var/lib/containers/storage:/var/lib/containers/storage \
-v ./config.toml:/config.toml:ro \
quay.io/centos-bootc/bootc-image-builder:latest \
--type iso \
--chown 1000:1000 \
localhost/kde-bootc

If everything goes well, the ISO image will be available in the ./output directory. You can use Fedora Media Writer to create a USB and put your images on a portable drive such as flash disk.

At the time of writing, the installer uses Anaconda and functions like any other Fedora flavor installation.

For more information on bootc-image-builder, visit: https://github.com/osbuild/bootc-image-builder

Post installation

The first step is to restore the SELinux context for the systemd-homed home directory. Without this, you may not be able to log in as admin. To complete this task, log in as root, activate admin home area, and then run restorecon to restore the SELinux context.

homectl activate admin
<< enter password for admin
restorecon -R /home/admin
homectl deactivate admin

At this point, you can change the passwords for root and admin:

passwd root
homectl passwd admin

After completing these steps, you can log out from root and log in to admin.

If your computer has a fingerprint reader, setting it up is not possible from Plasma’s user settings, as systemd-homed is not yet recognised by the desktop. However, you can manually enroll your fingerprint by running fprintd-enroll and placing your finger on the reader as you normally would.

sudo fprintd-enroll admin

Same as above, you cannot set up the avatar from Plasma’s user settings, but you can copy an available avatar (PNG file) from Plasma’s avatar directory to the account service’s directory. The file name needs to be the same as the username:

/usr/share/plasma/avatars/<avatar.png> -> /var/lib/AccountsService/icons/admin

Finally, enable the service to keep your system updated and any other desired services:

systemctl enable --now bootc-fetch.timer
systemctl enable --now tailscaled

Troubleshooting

Drifts on /etc

Please note that a configuration file in /etc drifts when it is modified locally. Consequently, bootc will no longer manage this file, and new releases won’t be transferred to your installation. While this might be desired in some cases, it can also lead to issues.

For instance, if /etc/passwd is locally modified, uid or gid allocations for services may not get updated, resulting in service failures.

Use ostree admin config-diff to list the files in your local /etc that are no longer managed by bootc, because they are modified or added.

If a particular configuration file needs to be managed by bootc, you can revert it by copying the version created by the container build from /usr/etc to /etc.

Adding packages after first installation

The /var directory is populated in the container image and transferred to your OS during initial installation. Subsequent updates to the container image will not affect /var. This is the expected behavior of bootc and generally works fine. However, some RPM packages execute scriptlets after installation, resulting in changes to /var that will not be transferred to your OS.

Instead of trying to identify and update the missing bits in /var, I found it easier to overlay /usr (bootc usr-overlay) and reinstall the packages (dnf reinstall ..) after updating and rebooting bootc.

References

GitHub – kde-bootc: https://github.com/sigulete/kde-bootc
GitHub – bootc: https://github.com/bootc-dev/bootc
GitLab – fedora-bootc: https://gitlab.com/fedora/bootc

Fedora Project community

9 Comments

  1. Nice guide, thank you! I’m looking forward to when image-based Fedora is the standard. 🙂

    The official Fedora docs on bootc (https://docs.fedoraproject.org/en-US/bootc/) includes some interesting information in the “bootc and rpm-ostree” section. Apparently rpm-ostree can act as a client for bootc updates. I wonder if that means there’s automatic integration with something like GNOME Software, which supports updates via rpm-ostree.

  2. Great and very detailed article. Thanks for all the information!

  3. Jason

    I’m currently using and maintaining a modified version of the Universal Blue build system from 2-3 years ago to build my own Atomic-based images. I’ve taken a passing glance at bootc, and while I need to read this post again to get a better understanding of how it works compared to my existing scripts, I appreciate this post as it will be a great help mapping out a migration strategy. Thank you so much I’m looking forward to actually making the full migration.

  4. Mike

    So systemd home REALLY doesn’t work for this well. It doesn’t integrate with plasma, and firstboot can’t support the necessary initial SELinux configuration so you have to manually run the setup from a CLI as root to get it to even boot successfully.
    I’d be interested to see this done without using systemd home. Do you just script a full

    useradd

    call into the first boot script? Presumably you’re not using sysusr, which is intended for daemon/system users.

    Also I see bootc still hasnt solved user/group modifications. That seems to be the Achilles heel of the immutable distros so far, everyone has a different way of trying to work around the problem that the frequently changed

    /etc/passwd

    and

    /etc/group

    need to be partially included in the immutable portion but aren’t, and any user changes cause the updates to them to break.

    I also don’t see you mention anything about packages added to the running system being lost on update rather than rebased. Did bootc finally fix that, or is it still just not ready for Desktop User use?

    • Daniel

      Hi Mike,

      If you prefer using regular users, you can. My suggestion would be creating the user in the container build, adding an instruction to config-users’ script. This way /etc/passwd won’t drift, as it is managed by bootc.
      A instruction similar to the one below would do the trick:

      useradd -G wheel -u 1000 -K SUB_GID_MIN=1000001 -K SUB_UID_MIN=1000001 $USER_NAME
      echo "Password" | passwd $USER_NAME -s
  5. Pramod

    Thanks for the detailed article.

    BTW, unless you are distributing your custom distribution, can’t you just layer packages via rpm-ostree?

    In a bootc-only fedora of the future, I assume this must be the only way of such layering, for things which sysexts can’t ship… like custom udev rules for a controller, an alternate Wi-Fi daemon like iwd, or just systemd-crontab-generator.

    • Daniel

      Yes, you can use rpm-ostree to layer packages on top of the installation created by bootc.
      However, once packages are layered, you won’t be able to use bootc to update. I assume rpm-ostree can download and update changes from the container image similarly. But I didn’t try that scenario.

      • Pramod

        I agree.
        But I am skeptical of the advantages of

        bootc

        when [rpm-]ostree already lets you have the best of container images and packages…

  6. Mbarak

    Hi! Just wanted to add some details for whoever comes later

    Regarding using systemd-homed users:

    You need to have the plaintext

    password

    field under the

    secret

    section – otherwise you will need to type your password in at boot.
    You still need to have the

    hashedPassword

    field under

    privileged

    section – otherwise systemd-homed will refuse to manage the user.
    systemd-homed users do not work with SSH.

    Don’t use this for your servers.

    Regarding SSH: if the home directory is activated

    homectl activate user

    and the user’s password is entered, then SSH will work. Otherwise, on boot, SSH for that user does not work.

    This is regardless of the

    storage

    type of the record i.e.

    directory

    storage – which is not encrypted – still doesn’t work.

    I thought I could use this for bootc to put my user in the container image but it seems that the only way to create a user is with “machine local state” from the installer.

Leave a Reply


The interval between posting a comment and its appearance will be irregular so please DO NOT resend the same post repeatedly. All comments are moderated but this site is not monitored continuously so comments will not appear as soon as posted.

This site uses Akismet to reduce spam. Learn how your comment data is processed.

The opinions expressed on this website are those of each author, not of the author's employer or of Red Hat. Fedora Magazine aspires to publish all content under a Creative Commons license but may not be able to do so in all cases. You are responsible for ensuring that you have the necessary permission to reuse any work on this site. The Fedora logo is a trademark of Red Hat, Inc. Terms and Conditions