Хабр, привет!
С мая 2025 года стала доступна новая версия Red Hat Enterprise Linux — RHEL 10. Одним из её главных новинок стала поддержка bootc-контейнеров — загружаемых контейнеров, которые можно запускать не только в привычном виде qcow, vmdk и raw-образов виртуальных машин, но и прямо на любимом Bare Metal.
Некоторые аналитики считают технологию крайне перспективной. В нашей практике мы с ней не сталкивались, но это только пока. Для нас это отличный повод посмотреть на нее поближе.
Спойлер: bootc чем-то напоминает CoreOS и то, как Machine Config из OpenShift взаимодействует с ним. Но bootc применяется уже в отрыве от «родительского» окружения в «реальном мире».

План такой:
Предисловие – несколько слов о bootc
Собираем лабу для теста (Что нам потребуется?)
Тестируем
Пища для размышлений
Предисловие
Сам проект bootс существует два года, тем не менее мы уверены, что он заставит нас взглянуть на старые добрые контейнеры и их использование по-другому, о чем расскажем ниже. На github-страничке проекта описание подразумевает, что у вас уже есть бэкграунд в этой области:
The original Docker container model of using "layers" to model applications has been extremely successful. This project aims to apply the same technique for bootable host systems - using standard OCI/Docker containers as a transport and delivery format for base operating system updates.
The container image includes a Linux kernel (in e.g. /usr/lib/modules), which is used to boot. At runtime on a target system, the base userspace is not itself running in a "container" by default. For example, assuming systemd is in use, systemd acts as pid1 as usual - there's no "outer" process
Если же перевести это все на простой язык, то bootc позволяет создавать загружаемые контейнеры, помещая ядро Linux внутрь:

При этом получившийся образ может быть сконвертирован как диск для виртуальной машины или образ для установки на BareMatel. Также bootc управляет жизненным циклом развернутого из нашего образа сервера и может обновлять его или откатывать назад используемую версию. В данном обзоре мы посмотрим на то, как выполняется установка и обновление.
Схема работы с bootc-контейнерами выглядит следующим образом:

Лирическое отступление: За идеей bootc, на наш взгляд, стоит желание управлять сервером (доставка обновлений и установленные пакеты) как Infrastructure as Code (IaC). Еще немного приготовлений с настройкой Matchbox — и мы получим среду, где BareMetal-серверы развертываются в большей степени автоматически, доcтавка обновлений происходит через обновление самого контейнера, не затрагивая данные приложения. Тем не менее, при таком сценарии у нас остаются приятные дополнения в виде слоеной (layered) файловой системы, однородности устанавливаемых серверов и так далее.
Похожие идеи применяются в Flatcar (CoreOS) и Talos – в них не используются контейнеры для доставки, но есть попытка максимально абстрагироваться от администрирования ОС и заточить ее под выполнение одной функции. Такой подход помогает переосмыслить роль ОС и способы взаимодействия с ней.
И мы попробуем воссоздать данную инфраструктуру из говна и веток с минимальными затратами.
Что нам потребуется
Инсталляционный (Kickstart) сервер. Мы не будем настраивать PXE и полную автоматизацию, ограничимся раздачей профилей через Apache плюс установим podman для сборки образа. Разумеется, можно настроить Matchbox, но это усложнит обзор.
Registry для хранения нашего собранного образа. В нашем случае он будет установлен как docker/podman-контейнер на инсталляционном сервере.
Образ (image) для создания bootc-контейнера.
Профиль для Kickstart.
Клиентский сервер (в моем случае это просто виртуальная машина VMWare).
Инсталляционный сервер
В качестве инсталляционного сервера используем RHEL10. Начнем с подготовки.
a) Устанавливаем на него Apache:
# dnf install apache -y
# systemctl enable httpd
# systemctl start httpd
b) Разрешаем в firewall доступ к httpd:
# firewall-cmd --permanent --add-service=http --add-service=https
# firewall-cmd --reload
c) Устанавливаем podman:
# dnf install podman -y
Registry
Теперь подготовим контейнер с registry.
Логинимся в docker.io и получаем образ registry:
# podman login docker.io
# podman pull registry:latest
# podman image ls | grep registry

Теперь запускаем контейнер с registry:
# podman run -p 5000:5000 -d docker.io/library/registry
# firewall-cmd --permanent --add-port=5000/tcp
# firewall-cmd --reload
Проверяем, что registry доступен с других хостов:

Сборка образа (image) контейнера
Теперь нам нужно собрать образ контейнера, который будет использоваться при инсталляции. Делается это старым добрым методом через docker-файл. Например:
FROM registry.redhat.io/rhel9/rhel-bootc:9.6-1747275992
#Устанавливаем компоненты нашего Web-сервера
RUN dnf module enable -y php:8.2 nginx:1.22 && dnf install -y httpd mariadb mariadb-server php-fpm php-mysqlnd && dnf clean all
#Запускаем сервисы автоматически при загрузке
RUN systemctl enable httpd mariadb php-fpm
#Создаем домашнюю страницу
RUN echo 'Welcome to RHEL' >> /var/www/html/index.html
У нас есть выбор контейнеров для использования. К примеру, по запросу rhel-bootc в catalog.redhat.com находятся образы как с RHEL9, так и с RHEL10.

Проверяем образы из командной строки:

Вывод получился какой-то неполный. Пробуем по-другому:

Так-то лучше!
Я для своего docker-файла выбрал RHEL9.6. О чем говорит строка:
FROM registry.redhat.io/rhel9/rhel-bootc:9.6-1747275992
Внимание! Для данной сборки вам потребуется аккаунт в RedHat для получения данного образа:
Проверяем, что образ появился в списке. Обратите внимание, какой тяжелый получился образ:
# podman image ls

Отправляем образ в registry, чтобы в будущем выполнить из него инсталляцию.
# podman push 10.31.135.242:5000/lamp-bootc:9.6-1747275992
Kickstart-профиль
В рамках маленькой обзорной статьи приведем свой конфигурационный файл с небольшим комментарием. Помните, что всегда есть ассистент на странице RedHat: https://access.redhat.com/labs/kickstartconfig/
Скрытый текст
# cat rhel9-image.cfg
text
network --bootproto=dhcp --device=link --activate
# Basic partitioning
clearpart --all --initlabel --disklabel=gpt
reqpart --add-boot
part / --grow --fstype xfs
# Here's where we reference the container image to install - notice the kickstart
# Здесь нет секции %packages
ostreecontainer --url 10.31.135.242:5000/lamp-bootc:9.6-1747275992
firewall --disabled
services --enabled=sshd
%pre
cat <<EOF > /etc/containers/registries.conf.d/registry-lab.conf
[[registry]]
location = "10.31.135.242"
insecure = true
blocked = false
EOF
%end
# optionally add a user
user --name=mike --groups=wheel --plaintext --password=q1q1q1
# if desired, inject a SSH key for root
rootpw --iscrypted locked
rebootЗдесь нас интересует строка, где мы указываем расположение имиджа:
ostreecontainer --url 10.31.135.242:5000/lamp-bootc:9.6-1747275992
и секция:
%pre
cat <<EOF > /etc/containers/registries.conf.d/registry-lab.conf
[[registry]]
location = "10.31.135.242"
insecure = true
blocked = false
EOF
%end
Установка
Установка выполняется классическим способом.
Загружаемся с образа RHEL10.
При появлении меню выбираем установку и нажимаем «e».
Редактируем строку linux, добавляя inst.ks=http://<ваш сервер>/<профиль>

Процесс пошел:

Установка завершена. Теперь мы можем проверить, какой образ был для нее использован:
[root@localhost ~]# bootc status
● Booted image: 10.31.135.242:5000/lamp-bootc:9.6-1747275992
Digest: sha256:4d676117deee8cecef57f93ff7005e89d5e56a408b9fffdf95f359809734e2ae
Version: 9.6 (2025-08-03 13:41:59.522050324 UTC)
Файл-приветcтвие на месте:
[root@localhost ~]# curl http://localhost:80
Welcome to RHEL
Перейдем к обновлению нашей системы. Подготовим docker-файл для образа: берем за основу наш старый файл и вносим в него изменения:
FROM registry.redhat.io/rhel9/rhel-bootc:9.6-1747275992
#install the lamp components
RUN dnf module enable -y php:8.2 nginx:1.22 && dnf install -y httpd mariadb mariadb-server php-fpm php-mysqlnd telnet&& dnf clean all
#start the services automatically on boot
RUN systemctl enable httpd mariadb php-fpm
#create an awe inspiring home page!
RUN echo 'Welcome to RHEL' >> /var/www/html/index.html
Вывод следующей команды очень длинный, поэтому пропустим его для простоты:
# podman build -f /root/docker/rhel-bootc -t 10.31.135.242:5000/lamp-bootc: 9.6-1747275992
Копируем его в registry:
# podman push 10.31.135.242:5000/lamp-bootc:9.6-1747275992
Внутри нашей виртуальной машины создаем конфигурационный файл:
# cat 1-my.conf
[[registry]]
location = "10.31.135.242"
insecure = true
blocked = false
Перед обновлением создаем тестовый файл и проверяем, что данные в контейнере сохраняются:
[root@localhost ~]# touch /root/TEST
Выполняем проверку наличия обновления:
# bootc upgrade --check
Update available for: docker://10.31.135.242:5000/lamp-bootc:9.6-1747275992
Version: 9.6
Digest: sha256:0a3cf8823bac6be7e8fd82ebf63d4c03061390a88d657b6cb602d7180e2f09e5
Total new layers: 70 Size: 1.1 GB
Removed layers: 1 Size: 278 bytes
Added layers: 2 Size: 448 bytes Deploying: done (5 seconds) Queued for next boot: 10.31.135.242:5000/lamp-bootc:9.6-1747275992
Version: 9.6
Digest: sha256:c79144af6c55371c2a8daebf14d0acf0c6263e61175fca6f815fba9fbbfff4b6
Total new layers: 70 Size: 1.1 GB
Removed layers: 1 Size: 278 bytes
Added layers: 2 Size: 445 bytes
Обновляем:
# bootc upgrade
layers already present: 69; layers needed: 1 (232 bytes)
Fetched layers: 232 B in 1 second (308 B/s)
Deploying: done (5 seconds) Queued for next boot: 10.31.135.242:5000/lamp-bootc:9.6-1747275992
Version: 9.6
Digest: sha256:0a3cf8823bac6be7e8fd82ebf63d4c03061390a88d657b6cb602d7180e2f09e5
Total new layers: 70 Size: 1.1 GB
Removed layers: 1 Size: 278 bytes
Added layers: 2 Size: 448 bytes
И теперь перезагружаемся. После завершения загрузки проверяем, что используется новый образ:
# bootc status
● Booted image: 10.31.135.242:5000/lamp-bootc:9.6-1747275992
Digest: sha256:0a3cf8823bac6be7e8fd82ebf63d4c03061390a88d657b6cb602d7180e2f09e5
Version: 9.6 (2025-08-03 15:00:15.652220473 UTC)
Rollback image: 10.31.135.242:5000/lamp-bootc:9.6-1747275992
Digest: sha256:4d676117deee8cecef57f93ff7005e89d5e56a408b9fffdf95f359809734e2ae
Version: 9.6 (2025-08-03 13:41:59.522050324 UTC)
А на месте ли наш тестовый файл?
# ls -la /root/
total 20
drwx------. 3 root root 125 Aug 3 10:50 .
drwxr-xr-x. 25 root root 4096 Aug 4 05:16 ..
-rw-------. 1 root root 853 Aug 4 05:16 .bash_history
-rw-------. 1 root root 20 Aug 3 10:12 .lesshst
drwx------. 2 root root 6 Aug 3 10:05 .ssh
-rw-r--r--. 1 root root 0 Aug 3 10:23 TEST
-rw-------. 1 root root 1214 Aug 3 10:05 anaconda-ks.cfg
-rw-------. 1 root root 749 Aug 3 10:05 original-ks.cfg
Все ОК.
Выводы:
С bootc контейнеры заметно «потяжелели»
Метод управления обновлениям сервера (даже bare metal) изменился. Нужно время чтобы сказать стало ли лучше и насколько
Composefs на физическом сервере приносит свои плюсы
Граница между контейнерами и физическими серверами размывается
Решение проблем, возникающих при работе с аппаратной частью, станет еще интереснее
Получив общее представление о bootc, дальше можно углубиться в документацию:
https://bootc-dev.github.io/bootc/bootc-images.html
https://docs.fedoraproject.org/en-US/bootc/community/
Скажем по секрету: углубляться есть куда — внутри интереснее, чем снаружи:
[root@localhost ~]# df -h
Filesystem Size Used Avail Use% Mounted on
devtmpfs 4.0M 0 4.0M 0% /dev
tmpfs 7.7G 0 7.7G 0% /dev/shm
tmpfs 3.1G 8.7M 3.1G 1% /run
/dev/sda3 19G 2.6G 17G 14% /sysroot
composefs 6.9M 6.9M 0 100% /
tmpfs 7.7G 0 7.7G 0% /tmp
/dev/sda2 960M 192M 769M 20% /boot
tmpfs 1.6G 0 1.6G 0% /run/user/1000
Но об этом — в следующих сериях?
dekanovich
Ммммм, пятый пункт радует. У меня всегда будет работа -- сношания с ОС будет еще больше :)))
MikeGavrilov
Процесс становится более изощренным, но суть процесса не меняется :)