Introduction

Depuis plusieurs années, je cherche à gérer ma configuration système “As-code” pour garantir la reproductibilité et la cohérence de mes environnements. L’objectif premier était d’installer mon laptop de travail, mais j’ai également le besoin d’installer des serveurs Linux (sans Kubernetes, donc Talos n’est pas une option).

Initialement, j’utilisais Packer pour générer une image de machine virtuelle, que je clonais ensuite sur le disque de la machine que je voulais configurer. Ça marchait très bien pour des templates serveurs, mais pour une machine de dev, c’était un peu un empilement de bout de scotchs. En plus de ça, j’ai décidé de chercher une alternative à Packer à cause des changements de licence des produit Hashicorp (décision que j’ai encore du mal à digérer !).

NixOS fut un excellent remplaçant pendant quelque temps, mais je me suis heurté à plusieurs problèmes sans avoir le niveau nécessaire pour les résoudre. J’ai migré sur une Fedora Silverblue, une autre distribution immuable sur laquelle j’ai plus de confiance. J’y ai retrouvé le confort d’une installation Linux classique en bénéficiant des avantages d’un système immuable.

Remarque

Petite side-note: je vais beaucoup parler d’OSTree dans le cadre de CoreOS et Fedora Silverblue, mais cette techno n’est pas exclusive à ces distributions, on peut aussi citer Fedora CoreOS, Endless OS, Flatcar Linux, et même la machine virtuelle de Podman lorsqu’on est sur MacOS ou Windows.

Mais je sens que je vais un peu vite en besogne, alors débutons par le commencement.

OSTree : Git pour les filesystems

OSTree est souvent décrit comme un “Git pour les filesystems”. Il permet de versionner, distribuer et déployer des systèmes Linux de manière atomique. Plutôt que de gérer les paquets individuellement, OSTree stocke des snapshots complets du système, facilitant les mises à jour et les retours en arrière.

Il se base sur ComposeFS dont le focus premier est de garantir l’intégrité des données, il se base sur EROFS (Enhanced Read-Only File System) pour garantir que les données ne seront pas corrompues et stocké les métadonnées des fichiers. Il fait nativement de la déduplication de données et de la compression en LZ4.

Pour l’utilisateur, c’est totalement transparent.

❯ mount | grep -e "overlay" -e "erofs"
composefs on / type overlay (ro,relatime,seclabel,lowerdir+=/run/ostree/.private/cfsroot-lower,datadir+=/sysroot/ostree/repo/objects,redirect_dir=on,metacopy=on)

lowerdir est le répertoire en lecture seule (le composefs) contenant les métadonnées des fichiers, et datadir est le répertoire contenant les données (le erofs).

La data (dans /sysroot/ostree/repo/objects) est stockée dans un répertoire dédié, qui contient toutes les versions du système. Chaque version est identifiée par un hash, similaire à Git.

❯ ls /ostree/repo/
config  extensions  objects  refs  state  tmp
❯ ls /ostree/repo/objects/05/
02404180f5f1f775c1dc71850f19339064d281b433ec340730f96be2aa9c4f.file  5e77706a79f15f1c6b13713c0a05e8aca051ebb40bce01832b5ce0145ee3cc.file  c01726f16a1c315014f946ee821e27b646536dae6aa70f39ed96eee0bea6c7.file
0fff529c3a948b1f56d0973eca5f840a1459c5ef43806f3c451f2fd835ebe2.file # ...

La comparaison avec Git ne s’arrête pas là, OSTree permet de créer des commits, de nouvelles versions de notre système et de basculer entre ces versions comme on le ferait avec des commits Git (git commit et git checkout).

Regardons par exemple un laptop Silverblue que j’ai installé récemment:

  ostree-image-signed:docker://ghcr.io/ublue-os/bluefin-dx:latest
                   Digest: sha256:7aabc7db3c3212ab3efd4a93dd895ee09c82a7af8d7c2e952bfb80f0058110a6
                  Version: latest-42.20251010.1 (2025-10-10T04:58:09Z)
                     Diff: 36 upgraded, 3 added
          LayeredPackages: brightnessctl btop emacs erofs-utils gammastep gh ghostty kubectl matugen niri pavucontrol pcsc-tools quickshell-git trayscale vimiv wl-mirror zoxide

● ostree-image-signed:docker://ghcr.io/ublue-os/bluefin-dx:latest
                   Digest: sha256:55481755d5d9ae678150b86962d75d15dd8ba2709664a64427197d5cdd083140
                  Version: latest-42.20251008 (2025-10-08T02:20:33Z)
          LayeredPackages: brightnessctl btop emacs gammastep gh ghostty kubectl matugen niri pavucontrol pcsc-tools quickshell-git trayscale vimiv wl-mirror zoxide

Je dispose de 2 commits identifiés par leur hash (Digest). Je suis sur le plus ancien (en bas avec l’encoche ), mais je peux facilement basculer sur le plus récent en redémarrant mon système (méthode atomique) ou en utilisant la commande rpm-ostree apply-live pour appliquer la mise à jour sans redémarrer.

En plus de voir les différences entre les versions (36 paquets mis à jour, 3 nouveaux), on peut aussi voir les paquets additionnels que j’ai installés par-dessus l’image de base (LayeredPackages). Je peux également demander à ostree de m’afficher le contenu des commits, comme on le ferait avec git show.

$ ostree admin status
  default 5ef5958f30a0ba985961f1a9e1e8a672287ad5b67e8092bce724cf9c72147484.0 (staged)
    origin: <unknown origin type>
* default e595112738655e363e10ecbdb9378adcd6ebaebc23c1113c4d980e6b71e30b17.0
    origin: <unknown origin type>
$ ostree ls --repo=/sysroot/ostree/repo e595112738655e363e10ecbdb9378adcd6ebaebc23c1113c4d980e6b71e30b17 /
d00555 0 0      0 /
l00777 0 0      0 /bin -> usr/bin
l00777 0 0      0 /home -> var/home
l00777 0 0      0 /lib -> usr/lib
l00777 0 0      0 /lib64 -> usr/lib64
l00777 0 0      0 /media -> run/media
l00777 0 0      0 /mnt -> var/mnt
l00777 0 0      0 /opt -> var/opt
l00777 0 0      0 /ostree -> sysroot/ostree
l00777 0 0      0 /root -> var/roothome
l00777 0 0      0 /sbin -> usr/sbin
l00777 0 0      0 /srv -> var/srv
d00755 0 0      0 /boot
d00755 0 0      0 /dev
d00755 0 0      0 /proc
d00755 0 0      0 /run
d00755 0 0      0 /sys
d00755 0 0      0 /sysroot
d01777 0 0      0 /tmp
d00755 0 0      0 /usr
d00755 0 0      0 /var

Oui, les hash étant calculés différemment entre ostree et rpm-ostree, ils ne correspondent pas donc je dois récupérer le hash équivalent avec ostree

Les possibilités sont assez folles à partir de là. Je peux faire des ostree checkout, ostree commit, ostree diff, etc. L’inspiration Git est vraiment omniprésente (et c’est tant mieux). Bref, les avantages d’OSTree sont nombreux, comme :

  • Atomicité : Les mises à jour sont appliquées en une seule opération au reboot, réduisant les risques d’incohérence.
  • Rollback facile : En cas de problème, il est possible de revenir à une version précédente du système (il va créer une entrée dans le menu de boot à chaque mise à jour).
  • Gestion des versions : Chaque état du système est versionné, facilitant le suivi
  • Distribution efficace : On se base sur une méthode de distribution connue et éprouvée, on en parle après !

Mais forcément comme dans tout système immuable, il y a des parties mutables (sinon, on ne pourrait pas créer le moindre fichier de configuration). OSTree gère ça avec des “overlays” (en fait, on utilise OverlayFS) qui permettent de superposer un système de fichiers en lecture-écriture par-dessus le système immuable. Par exemple, les répertoires /etc et /var sont en écriture, tandis que le reste du système est en lecture seule.

Pour donner un peu de détail, c’est le /var qui est en écriture et les répertoires mutables vont avoir des liens symboliques pointant vers des sous-répertoires de /var:

  • /home/var/home
  • /opt/var/opt
  • /srv/var/srv
  • /root/var/roothome
  • /usr/local/var/usrlocal
  • /mnt/var/mnt
  • /tmp/sysroot/tmp

/etc est également en écriture, mais il est géré un peu différemment. OSTree utilise une technique appelée “etc overlay” pour gérer les modifications dans /etc. Lorsqu’une mise à jour est appliquée, OSTree compare les fichiers dans la nouvelle version avec ceux dans /etc et applique les changements de manière intelligente, en préservant les modifications locales autant que possible.

Ainsi, il y a le dossier /usr/etc contenant la version immuable des fichiers de configuration, et /etc qui est le répertoire mutable. Si je modifie un fichier dans /etc et que je demande à OSTree de comparer avec la nouvelle version, il va me montrer les différences et conserver mes modifications locales.

❯  ls /usr/etc/motd
/usr/etc/motd

❯ sudo ostree admin config-diff | grep motd # Aucune diff

❯ echo "Hello, World!" > /etc/motd

❯ sudo ostree admin config-diff | grep motd
M    motd

J’aime beaucoup cette approche et je la trouve rassurante sur la capacité d’OSTree à gérer les configurations des services sans nous contraindre à ne jamais les modifier.

Un paquet-manager pour OSTree : rpm-ostree

OSTree gère les fichiers qui composent le système, mais ça veut dire quoi pour les paquets qui veulent écrire dans /usr ou /lib ? C’est pour cette raison qu’il faut une intégration avec le gestionnaire de paquets, dans le cas des OS Redhat, c’est rpm-ostree qui remplace dnf et yum.

❯ dnf install neovim
Error: this bootc system is configured to be read-only. For more information, run `bootc --help`.

Dès qu’on va essayer d’installer un paquet avec dnf, on va se heurter à une erreur. Il faut utiliser rpm-ostree pour gérer les paquets.

❯ rpm-ostree install vim
❯ rpm-ostree install neovim
Checking out tree 39fd9fc... done
Resolving dependencies... done
Will download: 17 packages (55.1 MB)
Downloading from 'updates'... done
Downloading from 'fedora'... done
Importing packages... done
Checking out packages... done
Running systemd-sysusers... done
Running pre scripts... done
Running post scripts... done
Running posttrans scripts... done
Writing rpmdb... done
Writing OSTree commit... done
Staging deployment... done
Freed: 126.9 MB (pkgcache branches: 0)
Added:
  compat-lua-libs-5.1.5-28.fc42.x86_64
  erofs-utils-1.8.10-1.fc42.x86_64
  inotify-tools-4.23.9.0-4.fc42.x86_64
  intel-qpl-1.7.0-1.fc42.x86_64
  libluv-1.51.0.0-2.fc42.x86_64
  libtsan-15.2.1-1.fc42.x86_64
  libvterm-0.3.3-5.fc42.x86_64
  lua5.1-lpeg-1.1.0-5.fc42.x86_64
  luajit-2.1.1748459687-2.fc42.x86_64
  luajit2.1-luv-1.51.0.0-2.fc42.x86_64
  neovim-0.11.4-1.fc42.x86_64
  nodejs-1:22.19.0-2.fc42.x86_64
  nodejs-docs-1:22.19.0-2.fc42.noarch
  nodejs-full-i18n-1:22.19.0-2.fc42.x86_64
  nodejs-libs-1:22.19.0-2.fc42.x86_64
  nodejs-npm-1:10.9.3-1.22.19.0.2.fc42.x86_64
  ripgrep-14.1.1-2.fc42.x86_64
  tree-sitter-cli-0.25.10-1.fc42.x86_64
  unibilium-2.1.2-3.fc42.x86_64
  xsel-1.2.1-8.fc42.x86_64
Changes queued for next boot. Run "systemctl reboot" to start a reboot

Après que mon paquet ait été installé, rpm-ostree m’indique que les changements seront appliqués au prochain redémarrage. En effet, rpm-ostree crée un nouveau commit OSTree avec le paquet ajouté, mais ne modifie pas le système en cours d’exécution. C’est une étape importante pour garantir l’atomicité des mises à jour.

Si je souhaite néanmoins appliquer les changements immédiatement, je peux utiliser la commande rpm-ostree apply-live qui va appliquer les changements sans redémarrer le système (mais certaines modifications peuvent nécessiter un redémarrage pour être pleinement effectives).

Et à l’inverse, si je veux revert les modifications, c’est aussi possible :

❯ rpm-ostree rollback

Bootc : Linux en mode conteneur

Et oui, car si je dois passer par le workflow standard de Fedora avec des kickstarts pour installer mes machines, ça ne m’avance pas beaucoup plus qu’avant. C’est là qu’intervient Bootc.

Si on utilise des images OCI pour automatiser le déploiement d’applications, pourquoi ne pas utiliser la même approche pour déployer des systèmes d’exploitation ? C’est exactement ce que propose Bootc. Il s’agit d’un projet qui permet de démarrer un système Linux directement à partir d’une image de conteneur. L’idée est de traiter le système d’exploitation comme une image immuable, facilitant la gestion, la reproductibilité et la sécurité.

alt text

La différence majeure avec une image de conteneur classique est que l’image utilisée par Bootc contient un système complet, incluant le noyau Linux, les bibliothèques, les outils système et les applications.

Une fois l’image générée, on peut en faire un peu ce qu’on veut : l’utiliser pour générer une image .qcow2 pour une VM, coupler un anaconda pour faire une installation sur du bare-metal, la stocker dans un registre d’images pour la déployer sur des serveurs cloud, etc.

Je précise quand même que l’OCI est une méthode de distribution, le système déployé ne va pas tourner dans un conteneur, on démarre un vrai Linux.

Relation entre Bootc et OSTree

Les deux sont des projets distincts, mais ils peuvent être utilisés ensemble pour créer un workflow puissant de gestion des systèmes Linux. Ostree gère les fichiers et les paquets (avec rpm-ostree par exemple), tandis que Bootc s’occupe de la création, du déploiement et va orchestrer les processus de mise à jour.

Ils se complètent parfaitement et permettent d’avoir un processus moderne et efficace pour gérer des serveurs Linux.

Je ne vous parle presque que de Fedora parce que Redhat mise beaucoup sur ces projets et les intègre profondément dans leur écosystème (Fedora Silverblue, CoreOS, Openshift…) mais il y a quand même quelques portages vers d’autres distributions.

Si ça vous intéresse, j’ai découvert cette issue qui liste des portages d’OSTree pour d’autres distributions (Linux Mint, Arch, Ubuntu, Gentoo).

Maintenant que la théorie est posée, je vais vous montrer comment utiliser ces outils pour déployer une Fedora Silverblue.

Déployer une image Bootc

Prenons un cas assez simple et concret : je veux installer une Fedora Silverblue sur l’un de mes serveurs en utilisant un système préparé avec Bootc. Comme expliqué précédemment, Bootc utilise des images OCI, donc nous allons commencer par créer un Containerfile (équivalent d’un Dockerfile mais pour podman).

FROM quay.io/fedora/fedora-bootc:latest

# -- Package installation --
## Enable RPM Fusion repositories
## https://rpmfusion.org/
RUN dnf install -y https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm

ENV BASE_PKG="tmux unzip vim htop qemu-guest-agent @container-management @hardware-support zsh rsync"
RUN dnf install -y ${BASE_PKG} && \
    dnf clean all

# -- User setup --
ARG USERNAME
ARG PASSWORD

RUN groupadd -g 1000 ${USERNAME} \
    && useradd -m -u 1000 -g 1000 -G wheel -s /bin/zsh -K MAIL_DIR=/dev/null ${USERNAME} \
    && echo "${USERNAME}:${PASSWORD}" | chpasswd \
    && mkdir -p /home/${USERNAME} \
    && chown ${USERNAME}:${USERNAME} /home/${USERNAME} \
    && chmod 700 /home/${USERNAME}

# -- Finalize container setup --
RUN bootc container lint

Concrètement, on part d’une image de base Fedora avec Bootc préparé. Ensuite, on installe les paquets nécessaires (dont qemu-guest-agent pour une VM) et on crée un utilisateur avec un mot de passe. Enfin, on valide que l’image est correcte avec bootc container lint.

Mais tu n’avais pas expliqué qu’il fallait passer par rpm-ostree pour installer des paquets et pas dnf ?

Oui… et non ! En fait durant le build de l’image, on doit utiliser dnf (car on n’est pas dans un système en cours d’exécution) et on peut modifier n’importe quel répertoire comme on le ferait dans une distro classique. C’est seulement une fois l’image déployée qu’on doit utiliser rpm-ostree pour gérer les paquets.

La construction de l’image se fait avec podman (ou docker si vous préférez).

sudo podman build -t my-silverblue:latest --build-arg USERNAME=qjoly --build-arg PASSWORD=supersecret .

Le sudo est nécessaire en fonction de votre choix à la prochaine étape.

Ça donne des images assez lourdes (2.2Go pour cette image de base Fedora Silverblue), mais c’est le prix à payer pour avoir un système complet et prêt à l’emploi.

❯ sudo podman image ls
REPOSITORY                                TAG         IMAGE ID      CREATED             SIZE
localhost/my-silverblue                   latest      7784e0c9cde1  About a minute ago  2.26 GB

Maintenant qu’on a notre image, on a deux options :

  1. Construire une iso (ou autre) avec l’image OCI pour une installation complète.
  2. Prendre une Fedora Silverblue standard et lui demander de basculer sur notre image OCI avec bootc switch.

La première option est la plus simple. Il existe un projet officiel Redhat nommé bootc-image-builder permettant de générer des images d’installation (ISO, QCOW2, AWS AMI, etc.) à partir d’une image OCI.

sudo podman run \
               --rm \
               -it \
               --privileged \
               --pull=newer \
               --security-opt label=type:unconfined_t \
               -v /var/lib/containers/storage:/var/lib/containers/storage \
                -v ./output:/output \
               quay.io/centos-bootc/bootc-image-builder:latest \
               --type anaconda-iso \
               --use-librepo=True \
               localhost/my-silverblue:latest --rootfs xfs
Trying to pull quay.io/centos-bootc/bootc-image-builder:latest...
Getting image source signatures
Copying blob 093e6ed8faf1 done   | 
Copying blob 926690fefe60 done   | 
Copying config f1c302e11f done   | 
Writing manifest to image destination
[/] Disk image building step
[9 / 9] Pipeline bootiso [---------------------------------------------------------------------------------------------------->] 100.00%
[3 / 3] Stage org.osbuild.implantisomd5 [------------------------------------------------------------------------------------->] 100.00%
Message: Results saved in .

le fait qu’on doive lancer le conteneur avec un sudo s’explique par le fait qu’il doit être privilégié et avoir accès à notre répertoire d’images dans /var/lib/containers/storage.

Le résultat se trouve dans le répertoire output que j’ai mappé en volume.

❯ tree
.
├── Containerfile
└── output
    ├── bootiso
    │   └── install.iso
    └── manifest-anaconda-iso.json

3 directories, 3 files

L’ISO générée peut être utilisée pour installer Fedora Silverblue sur n’importe quel serveur ou VM, l’Anaconda (installeur de Fedora) va formater le premier disque trouvé et installer le système.

Pour tester ça, on peut démarrer une VM avec qemu :

qemu-img create -f qcow2 vm_disk.qcow2 20G
qemu-system-x86_64 -m 8G -cpu host -smp 4 -boot d -cdrom ./output/bootiso/install.iso -hda vm_disk.qcow2 -netdev user,id=mynet0 -device e1000,netdev=mynet0 -serial stdio -enable-kvm

Immédiatement après le démarrage, on peut voir qu’anaconda démarre sans nous poser la moindre question.

alt text

Après l’installation, on peut démarrer la VM et vérifier que le système est bien une Fedora Silverblue.

qemu-system-x86_64 -m 8G -cpu host -smp 4 -boot d -hda vm_disk.qcow2 -netdev user,id=mynet0 -device e1000,netdev=mynet0 -serial stdio -enable-kvm

alt text

Nous avons bien notre VM Fedora Silverblue avec l’utilisateur qjoly que nous avions défini dans le Containerfile. Celle-ci montre bien que nous l’avons installée à partir d’une image Bootc pointant localhost/my-silverblue:latest (cette information aura son importance plus tard 😉).

On ne va pas voir la deuxième option (basculer une Silverblue existante vers une image OCI) dans cet article, mais c’est aussi possible avec la commande bootc switch.

Maintenant, ce qu’il m’est possible de faire, c’est d’envoyer cette image dans un registre d’images que je selfhost (avec Harbor par exemple) et de rendre cette image accessible. Il y a 2 raisons pour lesquelles je veux faire ça :

  • Pour générer mes images d’installation depuis n’importe où et les versionner sur Harbor.
  • Pour déployer cette image sur des serveurs distants en utilisant bootc switch (sur les images Silverblue officielles) ou bootc upgrade (sur nos serveurs déployés avec Bootc).

C’est d’ailleurs cette deuxième raison qui m’intéresse le plus. En effet, le déployer, c’est bien, penser aux mises à jour, c’est mieux. Avec Bootc, on peut imaginer un workflow où l’on construit une nouvelle image OCI avec les mises à jour et on demande aux serveurs distants de basculer sur cette nouvelle image.

Les mises à jour avec Bootc et OSTree

Je vais rajouter un pipeline de CI/CD (Github Actions) qui va construire une nouvelle image OCI à chaque fois que je pousse un changement dans mon Containerfile. Ensuite, je vais pousser cette image dans mon registre Harbor.

En installant une VM à partir de cette nouvelle image, on peut vérifier que tout fonctionne comme prévu.

[qjoly@fedora]~% rpm-ostree status
State: idle
Deployments:
●  ostree-unverified-registry:harbor.cortado.thoughtless.eu/bootc/server:main
                   Digest: sha256:5638b6581830be13c9ae418c5d1587f36c7f99b3860326fa7b163bef70236438
                  Version: 42.20250921.0 (2025-09-21T19:04:38Z)

Je peux installer un paquet supplémentaire (par exemple cowsay) en utilisant rpm-ostree et redémarrer la machine virtuelle pour appliquer les changements.

[qjoly@fedora]~% rpm-ostree status
State: idle
Deployments:
● ostree-unverified-registry:harbor.cortado.thoughtless.eu/bootc/server:main
                   Digest: sha256:5638b6581830be13c9ae418c5d1587f36c7f99b3860326fa7b163bef70236438
                  Version: 42.20250921.0 (2025-09-21T19:04:38Z)
          LayeredPackages: cowsay

  ostree-unverified-registry:harbor.cortado.thoughtless.eu/bootc/server:main
                   Digest: sha256:5638b6581830be13c9ae418c5d1587f36c7f99b3860326fa7b163bef70236438
                  Version: 42.20250921.0 (2025-09-21T19:04:38Z)

On est bon ! Maintenant, imaginons que je veux maintenant pointer vers une nouvelle image, par exemple harbor.cortado.thoughtless.eu/bootc/server:add-nginx qui contient nginx.

[qjoly@fedora]~% sudo bootc switch harbor.cortado.thoughtless.eu/bootc/server:add-nginx
Fetched layers: 0 B in 0 seconds (0 B/s)
Deploying: done (8 seconds) Pruned images: 0 (layers: 0, objsize: 36.9 MB)
Pruned images: 0 (layers: 0, objsize: 36.9 MB)
Queued for next boot: harbor.cortado.thoughtless.eu/bootc/server:add-nginx
  Version: 42.20250920.0
  Digest: sha256:bb6169309e6c0d728f7a726330360b505bd2e60931f86c38b81c773291f4daf0

Après redémarrage, on constate que nginx est bien installé… mais plus cowsay !

[qjoly@fedora]~% which nginx 
/usr/bin/nginx
[qjoly@fedora]~% cowsay
zsh: command not found: cowsay

Si je veux le re-installer, je peux le faire avec rpm-ostree install cowsay et il sera ajouté à la nouvelle image… mais si je fais ça, je vais avoir un drift entre mon image OCI générée par CI/CD et l’état de ma machine virtuelle. Ce n’est pas quelque chose de souhaitable parce que bootc délivre par défaut un service bootc-fetch-apply-updates.service qui va vérifier périodiquement si une nouvelle image est disponible et basculer dessus automatiquement pour garder le système à jour (il s’agit d’un timer systemd qui tourne toutes les 4 heures par défaut et va lancer la commande bootc upgrade --apply --quiet).

Mais lorsqu’on est dans un état de drift, on ne peut pas appliquer les mises à jour au risque de perdre les paquets installés manuellement. C’est ce que bootc va nous indiquer à notre login :

qjoly@192.168.1.35's password: 
Last login: Sat Oct 11 15:55:27 2025 from 192.168.1.181
[systemd]
Failed Units: 1
  bootc-fetch-apply-updates.service
[qjoly@fedora ~]$ journalctl -u bootc-fetch-apply-updates.service
Oct 11 15:56:05 fedora systemd[1]: Starting bootc-fetch-apply-updates.service - Apply bootc updates...
Oct 11 15:56:05 fedora bootc[1252]: error: Upgrading: Deployment contains local rpm-ostree modifications; cannot upgrade via bootc. You can run `rpm-ostree reset` to undo the modifications.
Oct 11 15:56:05 fedora systemd[1]: bootc-fetch-apply-updates.service: Main process exited, code=exited, status=1/FAILURE
Oct 11 15:56:05 fedora systemd[1]: bootc-fetch-apply-updates.service: Failed with result 'exit-code'.
Oct 11 15:56:05 fedora systemd[1]: Failed to start bootc-fetch-apply-updates.service - Apply bootc updates.

La solution est de ne pas installer de paquets manuellement avec rpm-ostree mais de toujours passer par la génération d’une nouvelle image OCI avec les paquets souhaités. Ainsi, on garde un état cohérent entre l’image et le système déployé. Générons une nouvelle image avec cowsay et pushons là dans Harbor pour que le service de mise à jour puisse la récupérer.

FROM quay.io/fedora/fedora-bootc:latest

# -- Package installation --
## Enable RPM Fusion repositories
## https://rpmfusion.org/
RUN dnf install -y https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm

ENV BASE_PKG="tmux unzip vim htop qemu-guest-agent @container-management @hardware-support zsh rsync nginx cowsay"
# On ajoute cowsay
RUN dnf install -y ${BASE_PKG} && \
    dnf clean all

# -- User setup --
ARG USERNAME
ARG PASSWORD

RUN groupadd -g 1000 ${USERNAME} \
    && useradd -m -u 1000 -g 1000 -G wheel -s /bin/zsh -K MAIL_DIR=/dev/null ${USERNAME} \
    && echo "${USERNAME}:${PASSWORD}" | chpasswd \
    && mkdir -p /home/${USERNAME} \
    && chown ${USERNAME}:${USERNAME} /home/${USERNAME} \
    && chmod 700 /home/${USERNAME}

RUN echo VARIANT="QJoly server" && echo VARIANT_ID=com.github.qjoly.bootc-server >> /usr/lib/os-release

# -- Finalize container setup --
RUN bootc container lint

Après un rpm-ostree reset pour annuler les modifications locales, le service de mise à jour va pouvoir appliquer la nouvelle image automatiquement au prochain check (toutes les 4 heures par défaut).

[qjoly@fedora ~]$ rpm-ostree status
State: idle
Deployments:
● ostree-unverified-registry:harbor.cortado.thoughtless.eu/bootc/server:add-nginx
                   Digest: sha256:5638b6581830be13c9ae418c5d1587f36c7f99b3860326fa7b163bef70236438
                  Version: 42.20250921.0 (2025-09-21T19:04:38Z)

  ostree-unverified-registry:harbor.cortado.thoughtless.eu/bootc/server:add-nginx
                   Digest: sha256:5638b6581830be13c9ae418c5d1587f36c7f99b3860326fa7b163bef70236438
                  Version: 42.20250921.0 (2025-09-21T19:04:38Z)
          LayeredPackages: cowsay
[qjoly@fedora ~]$ journalctl -u bootc-fetch-apply-updates.service
Oct 11 16:06:31 fedora bootc[1326]: Fetching ostree-unverified-registry:harbor.cortado.thoughtless.eu/bootc/server:add-nginx
Oct 11 16:06:32 fedora bootc[1326]: layers already present: 25; layers needed: 45 (1.0 GB)
Oct 11 16:06:32 fedora bootc[1326]: Pulling new image: ostree-unverified-registry:harbor.cortado.thoughtless.eu/bootc/server:add-nginx
Broadcast message from root@fedora (Sat 2025-10-11 16:07:35 UTC):

Initiated by bootc
The system will reboot now!

Automatiquement, le système redémarre sur la nouvelle image contenant nginx et cowsay sans que je n’aie eu à intervenir.

Afin d’en rassurer quelques-uns :

  • Il est possible de désactiver le service de mise à jour automatique si on préfère gérer les mises à jour manuellement.
  • Il est aussi possible de configurer à quelle heure le service se déclenche ou désactiver le reboot automatique.

Amis du GitOps, vous voilà servis !

Conclusion

Bootc et OSTree représentent une nouvelle façon de penser le déploiement et la gestion des systèmes Linux. En s’appuyant sur les concepts des conteneurs et du versioning, ils offrent des solutions robustes et modernes pour répondre aux besoins actuels des administrateurs et des développeurs.

Ça fait quelque temps que je cherche à envoyer un serveur dans un datacenter pour être libre d’annoncer mes propres IPs via du BGP, le choix de l’OS qui tourne sur ce serveur est important et je pense qu’avec Bootc + OSTree, je tiens une solution qui me convient parfaitement (car si jamais je bloque la machine durant une mise à jour, un simple reboot la remettra dans un état cohérent).

Je suis encore débutant avec ces outils, mais je suis très enthousiaste à l’idée de les explorer davantage et de les intégrer dans mes projets futurs (il me reste encore à automatiser le chiffrement en surchargeant le Kickstart d’Anaconda et à tester le déploiement sur du bare-metal).

J’espère que cet article vous aura donné envie d’en savoir plus sur Bootc et OSTree. N’hésitez pas à me poser des questions ou à partager vos expériences dans les commentaires !

Bon kawa ! ☕