Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

bootc

Transactional, in-place operating system updates using OCI/Docker container images. bootc is the key component in a broader mission of bootable containers.

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.

Status

The CLI and API for bootc are now considered stable. Every existing system can be upgraded in place seamlessly across any future changes.

However, the default storage backend uses the ostree project which has been powering stable operating system updates for many years. The stability here generally refers to the surface APIs, not the underlying logic. There is also a composefs backend, which is required for sealed images.


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

Base images

Many users will be more interested in base (container) images.

Fedora/CentOS

Currently, the Fedora/CentOS bootc project is the most closely aligned upstream project.

For pre-built base images; any Fedora derivative already using ostree can be seamlessly converted into using bootc; for example, Fedora CoreOS can be used as a base image; you will want to also rpm-ostree install bootc in your image builds currently. There are some overlaps between bootc and ignition and zincati however; see this pull request for more information.

For other derivatives such as the "Atomic desktops", see discussion of relationships which particularly covers interactions with rpm-ostree.

Other

However, bootc itself is not tied to Fedora derivatives; this issue tracks the main blocker for other distributions.


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

Generic guidance for building images

The bootc project intends to be operating system and distribution independent as possible, similar to its related projects podman and systemd, etc.

The recommendations for creating bootc-compatible images will in general need to be owned by the OS/distribution - in particular the ones who create the default bootc base image(s). However, some guidance is very generic to most Linux systems (and bootc only supports Linux).

Let's however restate a base goal of this project:

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.

Every tool and technique for creating application base images should apply to the host Linux OS as much as possible.

Understanding mutability

When run as a container (particularly as part of a build), bootc-compatible images have all parts of the filesystem (e.g. /usr in particular) as fully mutable state, and writing there is encouraged (see below).

When "deployed" to a physical or virtual machine, the container image files are read-only by default; for more, see filesystem.

Installing software

For package management tools like apt, dnf, zypper etc. (generically, $pkgsystem) it is very much expected that the pattern of

RUN $pkgsystem install somepackage && $pkgsystem clean all

type flow Just Works here - the same way as it does "application" container images. This pattern is really how Docker got started.

There's not much special to this that doesn't also apply to application containers; but see below.

Nesting OCI containers in bootc containers

The OCI format uses "whiteouts" represented in the tar stream as special .wh files, and typically consumed by the Linux kernel overlayfs driver as special 0:0 character devices. Without special work, whiteouts cannot be nested.

Hence, an invocation like

RUN podman pull quay.io/exampleimage/someimage

will create problems, as the podman runtime will create whiteout files inside the container image filesystem itself.

Special care and code changes will need to be made to container runtimes to support such nesting. Some more discussion in this tracker issue.

systemd units

The model that is most popular with the Docker/OCI world is "microservice" style containers with the application as pid 1, isolating the applications from each other and from the host system - as opposed to "system containers" which run an init system like systemd, typically also SSH and often multiple logical "application" components as part of the same container.

The bootc project generally expects systemd as pid 1, and if you embed software in your derived image, the default would then be that that software is initially launched via a systemd unit.

RUN dnf -y install postgresql && dnf clean all

Would typically also carry a systemd unit, and that service will be launched the same way as it would on a package-based system.

Users and groups

Note that the above postgresql today will allocate a user; this leads to the topic of users, groups and SSH keys.

Configuration

A key aspect of choosing a bootc-based operating system model is that code and configuration can be strictly "lifecycle bound" together in exactly the same way.

(Today, that's by including the configuration into the base container image; however a future enhancement for bootc will also support dynamically-injected ConfigMaps, similar to kubelet)

You can add configuration files to the same places they're expected by typical package systems on Debian/Fedora/Arch etc. and others - in /usr (preferred where possible) or /etc. systemd has long advocated and supported a model where /usr (e.g. /usr/lib/systemd/system) contains content owned by the operating system image.

/etc is machine-local state. However, per filesystem.md it's important to note that the underlying OSTree system performs a 3-way merge of /etc, so changes you make in the container image to e.g. /etc/postgresql.conf will be applied on update, assuming it is not modified locally.

Resolver configuration has an additional interaction with the container runtime used to build the image. See DNS and /etc/resolv.conf for the recommended approach.

Prefer using drop-in directories

These "locally modified" files can be a source of state drift. The best pattern to use is "drop-in" directories that are merged dynamically by the relevant software. systemd supports this comprehensively; see drop-ins for example in units.

And instead of modifying /etc/sudoers.conf, it's best practice to add a file into /etc/sudoers.d for example.

Not all software supports this, however; and this is why there is generic support for /etc.

Configuration in /usr vs /etc

Some software supports generic configuration both /usr and /etc - systemd, among others. Because bootc supports derivation (the way OCI containers work) - it is supported and encouraged to put configuration files in /usr (instead of /etc) where possible, because then the state is consistently immutable.

One pattern is to replace a configuration file like /etc/postgresql.conf with a symlink to e.g. /usr/postgres/etc/postgresql.conf for example, although this can run afoul of SELinux labeling.

Secrets

There is a dedicated document for secrets, which is a special case of configuration.

Handling read-only vs writable locations

The high level pattern for bootc systems is summarized again this way:

  • Put read-only data and executables in /usr
  • Put configuration files in /usr (if they're static), or /etc if they need to be machine-local
  • Put "data" (log files, databases, etc.) underneath /var

However, some software installs to /opt/examplepkg or another location outside of /usr, and may include all three types of data undernath its single toplevel directory. For example, it may write log files to /opt/examplepkg/logs. A simple way to handle this is to change the directories that need to be writable to symbolic links to /var:

RUN apt|dnf install examplepkg && \
    mv /opt/examplepkg/logs /var/log/examplepkg && \
    ln -sr /var/log/examplepkg /opt/examplepkg/logs

The Fedora/CentOS bootc puppet example is one instance of this.

Another option is to configure the systemd unit launching the service to do these mounts dynamically via e.g.

BindPaths=/var/log/exampleapp:/opt/exampleapp/logs

The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

Container runtime vs "bootc runtime"

Fundamentally, bootc reuses the OCI image format as a way to transport serialized filesystem trees with included metadata such as a version label, etc.

A bootc container operates in two basic modes. First, when invoked by a container run time such as podman or docker (typically as part of a build process), the bootc container behaves exactly the same as any other container. For example, although there is a kernel embedded in the container image, it is not executed - the host kernel is used. There's no additional mount namespaces, etc. Ultimately, the container runtime is in full control here.

The second, and most important mode of operation is when a bootc container is installed to a physical or virtual machine. Here, bootc is in control; the container runtime used to build is no longer relevant. However, it's very important to understand that bootc's role is quite limited:

  • On boot, there is code in the initramfs to do a "chroot" equivalent into the target filesystem root
  • On upgrade, bootc will fetch new content, but this will not affect the running root

Crucially, besides setting up some mounts, bootc itself does not act as any kind of "container runtime". It does not set up pid or other namespace, does not change cgroups, etc. That remains the role of other code (typically systemd). bootc is not a persistent daemon by default; it does not impose any runtime overhead.

This distinction also applies to DNS configuration. A container runtime may inject /etc/resolv.conf while building or running a container, but it is not present to do so after a bootc image boots. See DNS and /etc/resolv.conf.

Another example of this: While one can add Container configuration metadata, bootc generally ignores that at runtime today.

Labels

A key aspect of OCI is the ability to use standardized (or semi-standardized) labels. The are stored and rendered by bootc; especially the org.opencontainers.image.version label.

Example ignored runtime metadata, and recommendations

ENTRYPOINT and CMD (OCI: Entrypoint/Cmd)

Ignored by bootc.

It's recommended for bootc containers to set CMD /sbin/init; but this is not required.

The booted host system will launch from the bootloader, to the kernel+initramfs and real root however it is "physically" configured inside the image. Typically today this is using systemd in both the initramfs and at runtime; but this is up to how you build the image.

ENV (OCI: Env)

Ignored by bootc; to configure the global system environment you can change the systemd configuration. (Though this is generally not a good idea; instead it's usually better to change the environment of individual services)

EXPOSE (OCI: exposedPorts)

Ignored by bootc; it is agnostic to how the system firewall and network function at runtime.

USER (OCI: User)

Ignored by bootc; typically you should configure individual services inside the bootc container to run as unprivileged users instead.

HEALTHCHECK (OCI: no equivalent)

This is currently a Docker-specific metadata, and did not make it into the OCI standards. (Note podman healthchecks)

It is important to understand again is that there is no "outer container runtime" when a bootc container is deployed on a host. The system must perform health checking on itself (or have an external system do it).

Relevant links:

Kernel

When run as a container, the Linux kernel binary in /usr/lib/modules/$kver/vmlinuz is ignored. It is only used when a bootc container is deployed to a physical or virtual machine.

Security properties

When run as a container, the container runtime will by default apply various Linux kernel features such as namespacing to isolate the container processes from other system processes.

None of these isolation properties apply when a bootc system is deployed.

SELinux

For more on the intersection of SELinux and current bootc (OSTree container) images, see bootc images - SELinux.


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

DNS and /etc/resolv.conf

bootc does not configure networking or select a DNS resolver. The operating system in the container image must provide a coherent networking and DNS policy, just as it must provide a kernel and an init system. Usually that policy is owned by the distribution or base image.

The recommended approach is to configure DNS through the network management and resolver services selected by the base image. Do not treat /etc/resolv.conf as a file that should be copied from the container build environment into the booted system.

Build-time and boot-time resolver configuration

When an image is built, the container runtime normally provides temporary /etc/hostname, /etc/hosts, and /etc/resolv.conf files so that RUN instructions have working networking. These files describe the build environment. They are not the DNS configuration of the machine that will boot the image.

When the image is installed and booted, there is no outer Podman or other container runtime to recreate these files. The network stack included in the image is responsible for generating the booted host's resolver configuration. See Container runtime vs bootc runtime for more about this distinction.

In particular:

  • Do not copy the build host's /etc/resolv.conf into an image.
  • Do not rely on the /etc/resolv.conf visible in a networked RUN instruction. It may be a bind mount owned by Podman or Buildah.
  • Do not use an empty regular /etc/resolv.conf as a placeholder. bootc removes it when importing the image (see below).
  • Do not add files below /run to an image. /run is runtime state and is recreated on every boot.

The one thing bootc does with these files: when importing an image (with the ostree backend today), it removes /etc/resolv.conf and /etc/hostname if they are zero-length regular files, because that is what a container runtime leaves behind after bind-mounting its own copies during the build (see buildah#4242, bootc #1096 and #1167). Apart from that, bootc never reads or writes /etc/resolv.conf.

If a Containerfile step must intentionally replace /etc/resolv.conf, run the step with networking disabled, for example with RUN --network=none. This avoids modifying a container-runtime bind mount. It should only be necessary when deliberately changing the resolver policy inherited from the base image.

Follow the base image's resolver policy

A derived image should normally retain its base image's DNS implementation. Configure DNS servers, search domains, and per-interface behavior through that implementation rather than replacing /etc/resolv.conf.

Common policies include:

  • NetworkManager supplies per-link DNS configuration to systemd-resolved, and /etc/resolv.conf points to the local resolver stub.
  • NetworkManager writes a regular /etc/resolv.conf directly.
  • Another network manager or resolver owns the file under a distribution-specific policy.

NetworkManager and systemd-resolved are not alternatives with the same role. NetworkManager configures network interfaces and may use systemd-resolved as its DNS backend. The base image must ensure that its selected services, the type and target of /etc/resolv.conf, and its service enablement agree. For anything beyond that, follow your distribution's networking documentation and the references in Further reading.

Environment-specific DNS configuration should generally be supplied when a machine is provisioned. If DNS configuration is intentionally lifecycle-bound to an image, include NetworkManager connection profiles or resolver drop-ins in the image rather than including a generated /etc/resolv.conf. This keeps the configuration declarative and makes its owner clear.

Base images using systemd-resolved

A base image using systemd-resolved should ensure that:

  • systemd-resolved is installed and enabled.
  • NetworkManager or the selected network manager is configured to supply DNS information to it.
  • /etc/resolv.conf is a symlink to a resolver file generated below /run/systemd/resolve, normally ../run/systemd/resolve/stub-resolv.conf.

The upstream systemd-resolved package supplies a systemd-tmpfiles rule that creates the symlink at boot, so it does not need to exist in the image:

L! /etc/resolv.conf - - - - ../run/systemd/resolve/stub-resolv.conf

It does not contain DNS server addresses; systemd-resolved creates the target under /run from runtime network configuration. Base image authors should normally use the rule provided by the systemd-resolved package. A custom distribution integration can ship an equivalent vendor rule in /usr/lib/tmpfiles.d.

Do not disable systemd-resolved while retaining this rule and symlink. That leaves /etc/resolv.conf pointing to a runtime file that no enabled service creates.

Fedora CoreOS example

Fedora CoreOS uses NetworkManager with systemd-resolved. It retains the resolver stub symlink and configures DNS through NetworkManager connection profiles. For example, its Butane configuration examples write NetworkManager keyfiles under /etc/NetworkManager/system-connections/ and set the dns and dns-search properties there.

See the Fedora CoreOS documentation for host network configuration and Fedora's description of its systemd-resolved integration. Fedora CoreOS also has an integration test for its resolver policy.

This is an example of a complete base-image policy, not a requirement that all bootc images use systemd-resolved.

Base images using a regular /etc/resolv.conf

A base image may instead have NetworkManager or another service write a regular /etc/resolv.conf at runtime, on the booted host; nothing in the container build produces that file. In that case it should ensure that:

  • systemd-resolved is not enabled.
  • No tmpfiles rule recreates the systemd-resolved symlink.
  • /etc/resolv.conf is absent or is a regular file that the selected service is configured to manage.

For Fedora-derived images, removing the systemd-resolved package is generally clearer than only disabling its service: the package also owns the tmpfiles rule that creates the symlink. Do the removal in a RUN --network=none step: with networking enabled, /etc/resolv.conf is the container runtime's bind mount and the package's uninstall scriptlet fails when it tries to handle it (package removal needs no network). The inherited stub symlink is left behind by the removal; remove it in the same step:

RUN --network=none dnf -y remove systemd-resolved && rm -f /etc/resolv.conf

If the package must remain installed, mask its tmpfiles rule with a symlink to /dev/null at /etc/tmpfiles.d/systemd-resolve.conf and disable the service.

Any Containerfile operation that removes the inherited resolver symlink should use RUN --network=none, because a networked build step sees a bind mount at /etc/resolv.conf instead of the image's file.

Content that the image provides for /etc/resolv.conf should reach it through a symlink created by a systemd-tmpfiles rule, not as a regular file in the image: /etc is three-way merged across upgrades, and once a regular file there has been modified locally a later image default no longer takes effect (see Filesystem: /etc). The target depends on the policy:

  • a file under /run when the resolver is dynamic -- this is what the systemd-resolved rule above does;
  • a file under /usr when the configuration is truly static (below).

A regular file that a service such as NetworkManager generates at runtime is a different matter: the service owns it and rewrites it on every boot, so it does not carry image content and the merge behavior does not apply to it.

Static resolver configuration

Static DNS is configured through the network stack, like any other DNS setting, and that is the preferred mechanism even when the values never change:

  • NetworkManager: set ipv4.dns / ipv6.dns on the connection profile (with ipv4.ignore-auto-dns yes if DHCP must not add servers), typically together with a static address.
  • systemd-resolved: drop-ins in /etc/systemd/resolved.conf.d/.

Baking DNS server addresses into the image as a resolv.conf file is discouraged: they are machine or environment configuration, and the file bypasses whatever the network stack would otherwise manage. If an image must ship one anyway, put the content under /usr, for example /usr/lib/resolv.conf, and point the symlink at it with a rule mirroring the systemd-resolved one:

L /etc/resolv.conf - - - - ../usr/lib/resolv.conf

Then mask the vendor systemd-resolved rule for the same path (a symlink to /dev/null at /etc/tmpfiles.d/systemd-resolve.conf; two rules for one path are an error in tmpfiles.d), configure the network stack not to manage the file (dns=none in the [main] section of NetworkManager.conf; do not enable systemd-resolved), and use RUN --network=none for the build step that removes an inherited symlink.

Further reading

Behavior such as ndots, timeouts, retries, caching, and split DNS is defined by the resolver the base image selected, not by bootc. See the systemd-resolved documentation and NetworkManager's documentation for NetworkManager.conf and connection settings.

Application containers

Application containers and bootc hosts have different runtime behavior. By default, Podman creates /etc/hosts, /etc/hostname, and /etc/resolv.conf for each application container. Runtime options such as podman run --dns control that generated file, and --dns=none requests use of the image's file.

The DNS options to podman build apply to networked RUN instructions; they do not define the DNS policy of a host booted from the completed image. See the Podman documentation for podman run and podman build.


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

Users and groups

This is one of the more complex topics. Generally speaking, bootc has nothing to do directly with configuring users or groups; it is a generic OS update/configuration mechanism. (There is currently just one small exception in that bootc install has a special case --root-ssh-authorized-keys argument, but it's very much optional).

Generic base images

Commonly OS/distribution base images will be generic, i.e. without any configuration. It is very strongly recommended to avoid hardcoded passwords and ssh keys with publicly-available private keys (as Vagrant does) in generic images.

Injecting SSH keys via systemd credentials

The systemd project has documentation for credentials which can be used in some environments to inject a root password or SSH authorized_keys. For many cases, this is a best practice.

At the time of this writing this relies on SMBIOS which is mainly configurable in local virtualization environments. (qemu).

Injecting users and SSH keys via cloud-init, etc.

Many IaaS and virtualization systems are oriented towards a "metadata server" (see e.g. AWS instance metadata) that are commonly processed by software such as cloud-init or Ignition or equivalent.

The base image you're using may include such software, or you can install it in your own derived images.

In this model, SSH configuration is managed outside of the bootable image. See e.g. GCP oslogin for an example of this where operating system identities are linked to the underlying Google accounts.

Adding users and credentials via custom logic (container or unit)

Of course, systems like cloud-init are not privileged; you can inject any logic you want to manage credentials via e.g. a systemd unit (which may launch a container image) that manages things however you prefer. Commonly, this would be a custom network-hosted source. For example, FreeIPA.

Another example in a Kubernetes-oriented infrastructure would be a container image that fetches desired authentication credentials from a CRD hosted in the API server. (To do things like this it's suggested to reuse the kubelet credentials)

System users and groups (added via packages, etc)

It is common for packages (deb/rpm/etc) to allocate system users or groups as part of e.g apt|dnf install <server package> such as Apache or MySQL, and this is often done by directly invoking useradd or groupadd as part of package pre/post installation scripts.

With theshadow-utils implementation of useradd and the default glibc files this will result in changes to the traditional /etc/passwd and /etc/shadow files as part of the container build.

System drift from local /etc/passwd modifications

When the system is initially installed, the /etc/passwd in the container image will be applied and contain desired users.

By default (without etc = transient, see below), the /etc directory is machine-local persistent state. If subsequently /etc/passwd is modified local to the machine (as is common for e.g. setting a root password) then any new changes in the container image (such as users from new packages) will not appear on subsequent updates by default (they will be in /usr/etc/passwd instead - the default image version).

The general best fix for this is to use systemd-sysusers instead of allocating a user/group at build time at all.

Using systemd-sysusers

See systemd-sysusers. For example in your derived build:

COPY mycustom-user.conf /usr/lib/sysusers.d

A key aspect of how this works is that sysusers will make changes to the traditional /etc/passwd file as necessary on boot instead of at build time. If /etc is persistent, this can avoid uid/gid drift (but in the general case it does mean that uid/gid allocation can depend on how a specific machine was upgraded over time).

Note that the default sysusers design is that users are allocated on the client side (per machine). Avoid having non-root owned files managed by sysusers inside your image, especially underneath /usr. With the exception of setuid or setgid binaries (which should also be strongly avoided), there is generally no valid reason for having non-root owned files in /usr or other runtime-immutable directories.

User and group home directories and /var

For systems configured with persistent /home → /var/home, any changes to /var made in the container image after initial installation will not be applied on subsequent updates. If for example you inject /var/home/someuser/.ssh/authorized_keys into a container build, existing systems will not get the updated authorized keys file.

Using DynamicUser=yes for systemd units

For "system" users it's strongly recommended to use systemd DynamicUser=yes where possible.

This is significantly better than the pattern of allocating users/groups at "package install time" (e.g. Fedora package user/group guidelines) because it avoids potential UID/GID drift (see below).

Using systemd JSON user records

See JSON user records. Unlike sysusers, the canonical state for these live in /usr - if a subsequent image drops a user record, then it will also vanish from the system - unlike sysusers.d.

nss-altfiles

The nss-altfiles project (long) predates systemd JSON user records. It aims to help split "system" users into /usr/lib/passwd and /usr/lib/group. It's very important to understand that this aligns with the way the OSTree project handles the "3 way merge" for /etc as it relates to /etc/passwd. Currently, if the /etc/passwd file is modified in any way on the local system, then subsequent changes to /etc/passwd in the container image will not be applied.

Some base images may have nss-altfiles enabled by default; this is currently the case for base images built by rpm-ostree.

Commonly, base images will have some "system" users pre-allocated and managed via this file again to avoid uid/gid drift.

In a derived container build, you can also append users to /usr/lib/passwd for example. (At the time of this writing there is no command line to do so though).

Typically it is more preferable to use sysusers.d or DynamicUser=yes.

Modifying or adding members to system groups

groupmod, gpasswd, and libuser's lgroupadd operate on /etc/group. Many system groups also have an entry there (for example wheel) and are managed normally. But some — typically hardware/device groups such as cdrom or dialout — exist only in /usr/lib/group. Such a group resolves through getent group yet is absent from /etc/group, so these tools fail or silently do nothing:

  • groupmod -g <gid> cdrom reports group 'cdrom' does not exist in /etc/group.
  • gpasswd -a <user> cdrom and lgroupadd report the group does not exist in /etc/group.
  • usermod -aG cdrom <user> exits 0 but does not add the member.

This is expected behavior of the split, not a bug. Check where a group lives before managing it:

$ grep '^cdrom:' /etc/group    # no output => the group is only in /usr/lib/group
$ getent group cdrom           # still resolves, via nss-altfiles

For a group that exists only in /usr/lib/group, add its line to /etc/group (writable machine-local state) first, then manage it normally:

$ getent group cdrom >> /etc/group
$ gpasswd -a <user> cdrom

Do not do this for a group that is already in /etc/group (such as wheel): a duplicate entry breaks these tools (Multiple entries named 'wheel' in /etc/group). The membership added this way is machine-local; to make it part of the image instead, add it as image content (a sysusers.d m <user> <group> line, or by editing /usr/lib/group in the build). To change a system group's GID, see Reusing a UID/GID reserved by the base image.

Machine-local state for users

At this point, it is important to understand the filesystem layout - the default is up to the base image.

The default Linux concept of a user has data stored in both /etc (/etc/passwd, /etc/shadow and groups) and /home. The choice for how these work is up to the base image, but a common default for generic base images is to have both be machine-local persistent state. In this model /home would be a symlink to /var/home/someuser.

Injecting users and SSH keys via at system provisioning time

For base images where /etc and /var are configured to persist by default, it will then be generally supported to inject users via "installers" such as Anaconda (interactively or via kickstart) or any others.

Typically generic installers such as this are designed for "one time bootstrap" and again then the configuration becomes mutable machine-local state that can be changed "day 2" via some other mechanism.

The simple case is a user with a password - typically the installer helps set the initial password, but to change it there is a different in-system tool (such as passwd or a GUI as part of Cockpit, GNOME/KDE/etc).

It is intended that these flows work equivalently in a bootc-compatible system, to support users directly installing "generic" base images, without requiring changes to the tools above.

Transient home directories

Many operating system deployments will want to minimize persistent, mutable and executable state - and user home directories are that

But it is also valid to default to having e.g. /home be a tmpfs to ensure user data is cleaned up across reboots (and this pairs particularly well with a transient /etc as well):

In order to set up the user's home directory to e.g. inject SSH authorized_keys or other files, a good approach is to use systemd tmpfiles.d snippets:

f~ /home/someuser/.ssh/authorized_keys 600 someuser someuser - <base64 encoded data>

which can be embedded in the image as /usr/lib/tmpfiles.d/someuser-keys.conf.

Or a service embedded in the image can fetch keys from the network and write them; this is the pattern used by cloud-init and afterburn.

UID/GID drift

Any invocation of useradd or groupadd that does not allocate a fixed UID/GID may be subject to "drift" in subsequent rebuilds by default.

One possibility is to explicitly force these user/group allocations into a static state, via systemd-sysusers (per above) or explicitly adding the users with static IDs before a dpkg/RPM installation script operates on it:

RUN <<EORUN
set -xeuo pipefail
groupadd -g 10044 mycustom-group
useradd -u 10044 -g 10044 -d /dev/null -M mycustom-user
dnf install -y mycustom-package.rpm
bootc container lint
EORUN

Ultimately the /etc/passwd and similar files are a mapping between names and numeric identifiers. A problem then becomes when this mapping is dynamic and mixed with "stateless" container image builds.

For example today the CentOS Stream 9 postgresql package allocates a static uid of 26.

This means that

RUN dnf -y install postgresql

will always result in a change to /etc/passwd that allocates uid 26 and data in /var/lib/postgres will always be owned by that UID.

However in contrast, the cockpit project allocates a floating cockpit-ws user.

This means that each container image build (without additional work, unlike the example at the beginning of this section),may (due to RPM installation ordering or other reasons) result in the uid changing.

This can be a problem if that user maintains persistent state. Such cases are best handled by being converted to use sysusers.d (see Fedora change) - or again even better, using DynamicUser=yes (see above).

Reusing a UID/GID reserved by the base image

Sometimes a local group or an application needs a specific numeric GID (or UID) that a package in the base image already reserves. For example a fleet has historically used GID 17 for a developers group, but the base image ships a system group on GID 17.

The obvious commands fail. groupadd -g 17 developers reports GID '17' already exists, because the base group is a "system" entry served by nss-altfiles from /usr/lib/group and answers the lookup for that GID; and groupmod/gpasswd cannot edit it there (see Modifying or adding members to system groups). systemd-sysusers will not force an already-used GID either; given a conflict it allocates a different one.

You do not need to remove the package that owns the group. There are two supported approaches.

Prefer a non-conflicting GID

If the application refers to the group by name, the numeric value does not matter to it; give the local group any free GID. Numbers below the distribution's SYS_GID_MAX (see /etc/login.defs) are reserved for system allocation, so choose from the free range and allocate it statically, e.g. via sysusers.d or by appending to /usr/lib/group. getent group <gid> on the base image confirms whether a number is free.

This avoids changing anything in the base image and is the right choice whenever the specific number is not an external requirement.

Reassigning the base group's GID

When a specific reserved number really must be reused (a legacy assignment already baked into a fleet's file ownership, for instance), reassign the base group to a different free GID in your derived build. This is a configuration change, not a package removal, and it must happen at build time — before the allocation reaches a running machine's files. The group is defined in two places that both have to agree:

FROM <base image>
RUN set -xeu; \
    # 1. move the base group off the wanted GID in the altfiles source
    sed -i 's/^cdrom:x:17:/cdrom:x:4711:/' /usr/lib/group; \
    # 2. keep systemd-sysusers consistent: a lexically-earlier drop-in wins
    #    over the base package's entry, so it will not try to recreate the
    #    group on the old GID at boot
    printf 'g cdrom 4711\n' > /usr/lib/sysusers.d/00-reassign-cdrom.conf; \
    # 3. allocate the local group at the now-free GID, statically in the image
    echo 'developers:x:17:' >> /usr/lib/group; \
    bootc container lint

cdrom, 17, and developers are placeholders: substitute the group that actually holds the GID (getent group <gid> names it), the GID you need, and a free replacement GID from the reserved range. Note that on many bases cdrom is a different GID, so the sed above matches nothing unless adapted.

Editing /usr/lib/group is necessary because altfiles keeps answering for the old GID otherwise; the sysusers.d drop-in alone is not enough, and sysusers.d alone cannot move an existing entry.

After this, getent group 17 resolves to developers, the base group exists on its new GID, and the mapping survives reboots and the /etc three-way merge. Everything that referenced the base group by name continues to work because name resolution stays consistent; only code that hardcodes the old numeric GID would be affected, which is unusual.

Note that adding members to a group that lives in /usr/lib/group (such as the reassigned base group) is itself an altfiles behavior: gpasswd/usermod -aG operate on /etc/group, so promote the group's line into /etc/group first, or manage the membership as image content.

tmpfiles.d use for setting ownership

Systemd's tmpfiles.d provides a way to define files and directories in a way that will be processes at startup as needed. One way to work around SELinux security context and user or group ownership of a directory or file can be by using the z or Z directives.

These directives will adjust the access mode, user and group ownership and the SELinux security context as stated on the doc linked above.

For example, if we need /var/lib/my_file.conf to be part of the tss group but owned by root we could create a tmpfiles.d entry with:

+z /var/lib/my_file 0640 root tss -

The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

NAME

bootc-sysusers-shadow-sync.service

DESCRIPTION

This systemd service removes orphaned and duplicate entries from /etc/shadow and /etc/gshadow before systemd-sysusers.service runs.

If users or groups are dropped out from pristine /etc or /usr/lib copies of passwd or group, and they have systemd-sysusers entries, it's possible that stale data in the shadow files will cause systemd-sysusers to fail.

This service runs before systemd-sysusers.service, and trims stale data.

The service is only enabled on ostree and composefs boots; it has no effect on conventional package-managed systems.

SEE ALSO

bootc(8), bootc-systemd-generator(8), systemd-sysusers.service(8), lckpwdf(3)

VERSION


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

Kernel arguments

The default bootc model uses "type 1" bootloader config files stored in /boot/loader/entries, which define arguments provided to the Linux kernel.

The set of kernel arguments can be machine-specific state, but can also be managed via container updates.

The bootloader entries are currently written by the OSTree backend.

More on Linux kernel arguments: https://docs.kernel.org/admin-guide/kernel-parameters.html

/usr/lib/bootc/kargs.d

Many bootc use cases will use generic "OS/distribution" kernels. In order to support injecting kernel arguments, bootc supports a small custom config file format in /usr/lib/bootc/kargs.d in TOML format, that have the following form:

# /usr/lib/bootc/kargs.d/10-example.toml
kargs = ["mitigations=auto,nosmt"]

There is also support for making these kernel arguments architecture specific via the match-architectures key:

# /usr/lib/bootc/kargs.d/00-console.toml
kargs = ["console=ttyS0,115200n8"]
match-architectures = ["x86_64"]

NOTE: The architecture matching here accepts values defined by the Rust standard library (using the architecture of the bootc binary itself).

In some cases for Linux, this matches the value of uname -m, but definitely not all. For example, on Fedora derivatives there is ppc64le, but in Rust only powerpc64. A common discrepancy is that Debian derivatives use amd64, whereas Rust (and Fedora derivatives) use x86_64.

Changing kernel arguments post-install via kargs.d

Changes to kargs.d files included in a container build are honored post-install; the difference between the set of kernel arguments is applied to the current bootloader configuration. This will preserve any machine-local kernel arguments.

Kernel arguments injected at installation time

The bootc install flow supports a --karg to provide install-time kernel arguments. These become machine-local state.

Higher level install tools (ideally at least using bootc install to-filesystem can inject kernel arguments this way) too; for example, the Anaconda installer has a bootloader verb which ultimately uses an API similar to this.

Post-install, it is supported for any tool to edit the /boot/loader/entries files, which are in a standardized format.

Typically, /boot is mounted read-only to limit the set of tools which write to this filesystem. It is not "physically" read-only by default. One approach to edit them is to run a tool under a new mount namespace, e.g.

unshare -m
mount -o remount,rw /boot
# tool to edit /boot/loader/entries

At the current time, bootc does not itself offer an API to manipulate kernel arguments maintained per-machine.

Other projects such as rpm-ostree do, via e.g. rpm-ostree kargs, which is just a frontend for editing the bootloader configuration files. Note an important detail is that rpm-ostree kargs always creates a new deployment.

rpm-ostree kargs and bootc will interoperate as they both use the ostree backend today, and any kernel arguments changed via that mechanism will persist across upgrades.

It is currently undefined behavior to remove kernel arguments locally that are included in the base image via /usr/lib/bootc/kargs.d.

Injecting default arguments into custom kernels

The Linux kernel supports building in arguments into the kernel binary, at the time of this writing via the config CMDLINE build option. If you are building a custom kernel, then it often makes sense to use this instead of /usr/lib/bootc/kargs.d for example.


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

Sealed images

How Sealed Images Work

A sealed image is a cryptographically signed and verified bootc image that provides end-to-end integrity protection. This is achieved through:

  • Unified Kernel Images (UKIs): Combining kernel, initramfs, and boot parameters into a single signed binary
  • Composefs integration: Using composefs with fs-verity for content-addressed filesystem verification
  • Secure Boot: Cryptographic signatures on both the UKI and systemd-boot loader

A sealed image includes:

  1. composefs digest: A SHA-512 hash of the entire root filesystem, computed at build time
  2. Unified Kernel Image (UKI): A single EFI binary containing the kernel, initramfs, and kernel command line with the composefs digest embedded
  3. Secure Boot signature: The UKI is signed with your private key

At boot time, the composefs digest in the kernel command line (e.g. composefs.digest=v1-sha512-12:<digest>) is verified against the mounted root filesystem. This creates a chain of trust from firmware to userspace, ensuring the system will only boot if the root filesystem matches exactly what was signed.

Building Sealed Images

Prerequisites

For sealed images, the container must:

  • Include a kernel and initramfs in /usr/lib/modules/<kver>/
  • Have systemd-boot available (and NOT have bootupd)
  • Not include a pre-built UKI (the build process generates one)

Sealed images also require:

  • Secure Boot support in the target system firmware
  • A filesystem with fs-verity support (e.g., ext4, btrfs) for the root partition

Using without Secure Boot

You can use a sealed UKI without Secure Boot enabled. The composefs and mounting code is fully orthogonal to Secure Boot - the fs-verity digest of the root filesystem and all of its contents will still be validated at runtime, which does provide an increased level of integrity.

However: nothing validates that root digest itself, meaning any locally running code can replace the UKI (e.g. after a container breakout) and fully control the next boot.

It is intentional to support booting with Secure Boot disabled, because a valid use case is to temporarily disable it in order to test a change locally on e.g. one machine, then re-enable it later. However at the current time it is not yet streamlined to regenerate the UKI locally.

This is independent of --allow-missing-verity, which instead makes fs-verity on the root filesystem optional.

Build Pattern: Split the Kernel, Then Generate the UKI in a Separate Stage

Building a sealed image involves three stages: build the rootfs, split the kernel and initramfs out of it, and generate the signed UKI from the split rootfs in a tools stage:

# Build your rootfs with all packages and configuration
FROM <base-image> as rootfs
RUN apt|dnf|zypper install ... && bootc container lint --fatal-warnings

# Split the kernel and initramfs out of the rootfs. This moves
# /usr/lib/modules/<kver>/{vmlinuz,initramfs.img} into /kernel/<kver>/,
# since for a sealed image they end up embedded in the UKI instead.
FROM rootfs as split
RUN mkdir /kernel && bootc container split-kernel-and-rootfs --rootfs / --output /kernel

# Generate the sealed UKI in a tools stage
FROM <tools-image> as sealed-uki
RUN --mount=type=bind,from=split,target=/target \
    --mount=type=bind,from=split,source=/kernel,target=/kernel \
    --mount=type=secret,id=secureboot_key \
    --mount=type=secret,id=secureboot_cert <<EORUN
set -euo pipefail

mkdir -p /out
kver=$(ls /kernel)

# `bootc container ukify` computes the composefs digest of /target, reads
# extra kernel arguments from /target/usr/lib/bootc/kargs.d, and invokes the
# real `ukify` binary with the digest embedded in the cmdline. Everything
# after `--` is passed straight through to ukify.
bootc container ukify \
  --rootfs /target \
  --kernel-dir "/kernel/${kver}" \
  -- \
  --output "/out/${kver}.efi" \
  --signtool sbsign \
  --secureboot-private-key /run/secrets/secureboot_key \
  --secureboot-certificate /run/secrets/secureboot_cert
EORUN

# Final image: the split rootfs (kernel/initramfs already removed) plus the signed UKI
FROM split
COPY --from=sealed-uki /out/*.efi /boot/EFI/Linux/

This pattern works because:

  1. bootc container split-kernel-and-rootfs removes the raw kernel and initramfs from the rootfs ahead of time, so the final image never carries a duplicate copy of them (they end up embedded in the UKI instead)
  2. bootc container ukify handles computing the composefs digest and assembling the kernel command line, so you only need to pass ukify-specific options (like signing) after --
  3. The final stage copies the signed UKI into the already-split rootfs

The bootc container ukify Command

bootc container ukify --rootfs <PATH> [OPTIONS] -- [UKIFY_ARGS...]

This is the recommended way to build a UKI for a bootc image. It computes the composefs digest of --rootfs (using the lower-level compute-composefs-digest primitive described below), reads extra kernel arguments from /usr/lib/bootc/kargs.d, and invokes the system ukify binary with the resulting cmdline. Anything after -- is forwarded to ukify unchanged (e.g. --output, --signtool, signing key/cert options).

Options:

  • --rootfs <PATH>: Root filesystem to operate on (default: /)
  • --kernel-dir <PATH>: Directory containing vmlinuz/initramfs.img, named /parent/<kernel-version>. Needed when the kernel has already been split out of --rootfs, e.g. via split-kernel-and-rootfs
  • --allow-missing-verity: Make fs-verity validation optional, for filesystems that don't support it (e.g. XFS)
  • --erofs-version <v1|v2>: v1 (the default) writes a V1 argument followed by a V2 fallback; v2 writes only V2. See EROFS formats.
  • --write-dumpfile-to <PATH>: Write a composefs dumpfile for debugging

The bootc container compute-composefs-digest Command

bootc container compute-composefs-digest [PATH]

A lower-level primitive, used internally by ukify above, that computes just the composefs digest for a filesystem without building a UKI. The digest is a 128-character SHA-512 hex string that uniquely identifies the filesystem contents. Useful for scripting or debugging outside of the UKI build flow.

Options:

  • PATH: Path to the filesystem root (default: /target)
  • --erofs-version <v1|v2>: EROFS format for the computed digest (default: v1)
  • --write-dumpfile-to <PATH>: Generate a dumpfile for debugging

See also bootc-container-compute-composefs-digest(8).

Final Image Structure

The sealed image should have:

  • The signed UKI at /boot/EFI/Linux/<kver>.efi
  • A signed systemd-boot at /boot/EFI/BOOT/BOOTX64.EFI and /boot/EFI/systemd/systemd-bootx64.efi
  • The raw vmlinuz and initramfs.img removed from /usr/lib/modules/<kver>/ (they're now embedded in the UKI)

External Signing Workflow

For production environments with dedicated signing infrastructure:

  1. Build unsigned UKI: Compute digest and create an unsigned UKI (omit --signtool from ukify)
  2. Sign externally: Take the unsigned UKI to your signing infrastructure
  3. Complete the seal: Inject the signed UKI into the final image

This workflow is planned for streamlining in future releases (see #1498).


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

Secrets (e.g. container pull secrets)

To have bootc fetch updates from registry which requires authentication, you must include a pull secret in one of /etc/ostree/auth.json, /run/ostree/auth.json or /usr/lib/ostree/auth.json.

The path to the authentication file differs from that used by e.g. podman by default as some of the file paths used there are not appropriate for system services (e.g. reading the /root home directory).

Regardless, injecting this data is a good example of a generic "secret". The bootc project does not currently include one single opinionated mechanism for secrets.

Synchronizing the bootc and podman credentials

See the containers-auth.json man page. In many cases, you will want to keep both the bootc and podman/skopeo credentials in sync. One pattern is to symlink the two via e.g. a systemd tmpfiles.d fragment.

If you have a process invoking podman login (which by default writes to an ephemeral $XDG_RUNTIME_DIR/containers/auth.json) you can then ln -s /run/user/0/containers/auth.json /run/ostree/auth.json.

Performing an explicit login

If you have automation (or manual processes) performing a login, you can pass --authfile to set the bootc authfile explicitly; for example

echo <somepassword> | podman login \
  --authfile /run/ostree/auth.json -u someuser --password-stdin

This pattern of using the ephemeral location in /run can work well when the credentials are derived on system start from an external system. For example, aws ecr get-login-password --region region as suggested by this document.

You can also use the machine-local persistent location /etc/ostree/auth.json via this method.

Using a credential helper

In order to use a credential helper as configured in registries.conf such as credential-helpers = ["ecr-login"], you must currently also write a "no-op" authentication file with the contents {} (i.e. an empty JSON object, not an empty file) into the pull secret location.

Embedding in container build

This was mentioned above; you can include secrets in the container image if the registry server is suitably protected.

In some cases, embedding only "bootstrap" secrets into the container image is a viable pattern, especially alongside a mechanism for having a machine authenticate to a cluster. In this pattern, a provisioning tool (whether run as part of the host system or a container image) uses the bootstrap secret to lay down and keep updated other secrets (for example, SSH keys, certificates).

Via cloud metadata

Most production IaaS systems support a "metadata server" or equivalent which can securely host secrets - particularly "bootstrap secrets". Your container image can include tooling such as cloud-init or ignition which fetches these secrets.

Embedded in disk images

Another pattern is to embed bootstrap secrets only in disk images. For example, when generating a cloud disk image (AMI, OpenStack glance image, etc.) from an input container image, the disk image can contain secrets that are effectively machine-local state. Rotating them would require an additional management tool, or refreshing disk images.

Injected via baremetal installers

It is common for installer tools to support injecting configuration which can commonly cover secrets like this.

Injecting secrets via systemd credentials

The systemd project has documentation for credentials which applies in some deployment methodologies.


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

Management services

When running a fleet of systems, it is common to use a central management service. Commonly, these services provide a client to be installed on each system which connects to the central service. Often, the management service requires the client to perform a one time registration.

The following example shows how to install the client into a bootc image and run it at startup to register the system. This example assumes the management-client handles future connections to the server, e.g. via a cron job or a separate systemd service. This example could be modified to create a persistent systemd service if that is required. The Containerfile is not optimized in order to more clarly explain each step, e.g. it's generally better to invoke RUN a single time to avoid creating multiple layers in the image.

FROM <bootc base image>

# Typically when using a management service, it will determine when to upgrade the system.
# So, disable bootc-fetch-apply-updates.timer if it is included in the base image.
RUN systemctl disable bootc-fetch-apply-updates.timer

# Install the client from dnf, or some other method that applies for your client
RUN dnf install management-client -y && dnf clean all

# Bake the credentials for the management service into the image
ARG activation_key=

# The existence of .run_next_boot acts as a flag to determine if the
# registration is required to run when booting
RUN touch /etc/management-client/.run_next_boot

COPY <<"EOT" /usr/lib/systemd/system/management-client.service
[Unit]
Description=Run management client at boot
After=network-online.target
ConditionPathExists=/etc/management-client/.run_client_next_boot

[Service]
Type=oneshot
EnvironmentFile=/etc/management-client/.credentials
ExecStart=/usr/bin/management-client register --activation-key ${CLIENT_ACTIVATION_KEY}
ExecStartPre=/bin/rm -f /etc/management-client/.run_next_boot
ExecStop=/bin/rm -f /etc/management-client/.credentials

[Install]
WantedBy=multi-user.target
EOT

# Link the service to run at startup
RUN ln -s /usr/lib/systemd/system/management-client.service \
    /usr/lib/systemd/system/multi-user.target.wants/management-client.service

# Store the credentials in a file to be used by the systemd service
RUN echo -e "CLIENT_ACTIVATION_KEY=${activation_key}" > /etc/management-client/.credentials

# Set the flag to enable the service to run one time
# The systemd service will remove this file after the registration completes the first time
RUN touch /etc/management-client/.run_next_boot

The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

Managing upgrades

Right now, bootc is a quite simple tool that is designed to do just a few things well. One of those is transactionally fetching new operating system updates from a registry and booting into them, while supporting rollback.

The bootc upgrade verb

This will query the container image source and queue an updated container image for the next boot.

With the default OSTree backend, this implements an A/B style upgrade system. Changes to the base image are staged, and the running system is not changed by default.

Use bootc upgrade --apply to auto-apply if there are queued changes.

Staged updates with --download-only

The --download-only flag allows you to prepare updates without automatically applying them on the next reboot:

bootc upgrade --download-only

This will pull the new container image from the container image source and create a staged deployment in download-only mode. The deployment will not be applied on shutdown or reboot until you explicitly apply it.

Checking download-only status

To see whether a staged deployment is in download-only mode, use:

bootc status --verbose

In the output, you'll see Download-only: yes for deployments in download-only mode or Download-only: no for deployments that will apply automatically. This status is only shown in verbose mode.

Applying download-only updates

There are three ways to apply a staged update that is in download-only mode:

Option 1: Apply the staged update without checking for newer updates

bootc upgrade --from-downloaded

This unlocks the staged deployment for automatic application on the next shutdown or reboot, without fetching updates from the container image source. This is useful when you want to apply the already-downloaded update at a scheduled time.

Option 2: Apply the staged update and reboot immediately

bootc upgrade --from-downloaded --apply

This unlocks the staged deployment and immediately reboots into it, without checking for newer updates.

Option 3: Check for newer updates and apply

bootc upgrade

Running bootc upgrade without flags will pull from the container image source to check for updates. If the staged deployment matches the latest available update, it will be unlocked. If a newer update is available, the staged deployment will be replaced with the newer version.

Checking for updates without side effects

To check if updates are available without modifying the download-only state:

bootc upgrade --check

This only downloads updated metadata without changing the download-only state.

Example workflow

A typical workflow for controlled updates:

# 1. Download the update in download-only mode
bootc upgrade --download-only

# 2. Verify the staged deployment
bootc status --verbose
# Output shows: Download-only: yes

# 3. Test or wait for maintenance window...

# 4. Apply the update (choose one):
# Option A: Apply staged update without fetching from image source
bootc upgrade --from-downloaded

# Option B: Apply staged update and reboot immediately (without fetching from image source)
bootc upgrade --from-downloaded --apply

# Option C: Check for newer updates first, then apply
bootc upgrade

Important notes:

  • Image source check difference: bootc upgrade --from-downloaded does NOT fetch from the container image source to check for newer updates, while bootc upgrade always does. Use --from-downloaded when you want to apply the specific version you already downloaded, regardless of whether newer updates are available.

  • If you reboot before applying a download-only update, the system will boot into the current deployment and the staged deployment will be discarded. However, the downloaded image data remains cached, so re-running bootc upgrade --download-only will be fast and won't re-download the container image.

  • If you switch to a different image (using bootc switch or bootc upgrade to a different image), the new staged deployment will replace the previous download-only deployment, and the previously cached image will become eligible for garbage collection.

There is also an opinionated bootc-fetch-apply-updates.timer and corresponding service available in upstream for operating systems and distributions to enable.

Man page: bootc-upgrade.

Changing the container image source

Another useful pattern to implement can be to use a management agent to invoke bootc switch (or declaratively via bootc edit) to implement e.g. blue/green deployments, where some hosts are rolled onto a new image independently of others.

bootc switch quay.io/examplecorp/os-prod-blue:latest

bootc switch has the same effect as bootc upgrade; there is no semantic difference between the two other than changing the container image being tracked.

This will preserve existing state in /etc and /var - for example, host SSH keys and home directories.

Man page: bootc-switch.

Soft reboots

Soft reboot restarts userspace without restarting the kernel, avoiding a full hardware reboot. On the OSTree backend, bootc upgrade and bootc switch support these --soft-reboot modes:

  • required: Fails if the target deployment is not soft-reboot capable.
  • auto: Prepares a soft reboot if the target deployment is capable; otherwise, leaves it configured for a regular reboot.

Use --apply to request an immediate restart after preparing the deployment. Without --apply, --soft-reboot prepares the deployment but does not restart the system immediately. Without --soft-reboot, --apply requests a regular reboot.

The composefs backend currently differs: both modes fail if systemd lacks soft-reboot support. If the target deployment is not soft-reboot capable, auto leaves it staged without restarting, even with --apply; it does not automatically fall back to a regular reboot in this case.

Rollback

There is a bootc rollback verb, and associated declarative interface accessible to tools via bootc edit. This will swap the bootloader ordering to the previous boot entry.

Man page: bootc-rollback.


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

Upgrade/rollback failure detection in bootc

This document describes how to detect when a reboot failed to enable the staged image in bootc.

Overview

bootc uses different mechanisms to detect boot failures depending on the backend (OSTree vs. composefs+UKI) and the specific point of failure. Understanding these mechanisms is crucial for system administrators and automated tooling that needs to detect failed updates.

OSTree Backend Boot Failure Detection

For systems using the traditional OSTree backend, bootc relies on OSTree's built-in boot failure detection mechanisms.

Key Services

  1. ostree-finalize-staged.service - Runs during shutdown to finalize staged deployments
  2. ostree-boot-complete.service - Runs early in boot to detect finalization failures

When ostree-finalize-staged.service fails during shutdown/reboot, this will create a stamp file in /boot, and then on a subsequent reboot the ostree-boot-complete.service service will detect it, and then itself exit with a failure mode.

You can monitor the success of both services, though for ostree-finalize-staged.service note that the failure occurred during the previous boot's shutdown.

Composefs Backend Boot Failure Detection

Key Services

There is a bootc-finalize-staged.service which is similar to ostree-finalize-staged.service, but there is not currently a similar -boot-complete.service. There is also a bootc-root-setup.service that runs during initramfs to mount the composefs image and set up /etc and /var - but if this service fails, the system will not boot at all (emergency mode or hang).

At the current time then, it is recommended to check the journal for failures from the previous boot:

# Check for finalization failures from previous boot
journalctl -u bootc-finalize-staged.service -b -1

Systemd Boot Assessment Integration

As of a recent OSTree with this commit it is possible to configure the boot loader entry counting.

At the current time, the composefs backend does not configure boot entry counting, this is likely to be added in the future.

See Also


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

Accessing registries and disconnected updates

The bootc project uses the containers/image library to fetch container images (the same used by podman) which means it honors almost all the same configuration options in /etc/containers.

Insecure registries

Container clients such as podman pull and docker pull have a --tls-verify=false flag which says to disable TLS verification when accessing the registry. bootc has no such option. Instead, you can globally configure the option to disable TLS verification when accessing a specific registry via the /etc/containers/registries.conf.d configuration mechanism, for example:

# /etc/containers/registries.conf.d/local-registry.conf
[[registry]]
location="localhost:5000"
insecure=true

For more, see containers-registries.conf.

Private registries

It's common to use a private repository when deploying a fleet of bootc instances.

In addition to registry configuration, private registries require authentication. This is configured by placing an auth.json file at /etc/ostree/auth.json.

For more, see auth.json

Disconnected and offline updates

It is common (a best practice even) to maintain systems which default to being disconnected from the public Internet.

Pulling updates from a local mirror

Everything in the section remapping and mirroring images applies to bootc as well.

Performing offline updates via USB

In a usage scenario where the operating system update is in a fully disconnected environment and you want to perform updates via e.g. inserting a USB drive, one can do this by copying the desired OS container image to e.g. an oci directory:

skopeo copy docker://quay.io/exampleos/myos:latest oci:/path/to/filesystem/myos.oci

Then once the USB device containing the myos.oci OCI directory is mounted on the target, use

bootc switch --transport oci /var/mnt/usb/myos.oci

The above command is only necessary once, and thereafter will be idempotent. Then, use bootc upgrade --apply to fetch and apply the update from the USB device.

This process can all be automated by creating systemd units that look for a USB device with a specific label, mount (optionally with LUKS for example), and then trigger the bootc upgrade.


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

Logically Bound Images

About logically bound images

This feature enables an association of container "app" images to a base bootc system image. Use cases for this include:

  • Logging (e.g. journald->remote log forwarder container)
  • Monitoring (e.g. Prometheus node_exporter)
  • Configuration management agents
  • Security agents

These types of things are commonly not updated outside of the host, and there's a secondary important property: We always want them present and available on the host, possibly from very early on in the boot. In contrast with default usage of tools like podman or docker, images may be pulled dynamically after the boot starts; requiring functioning networking, etc. For example if the remote registry is unavailable temporarily, the host system may run for a longer period of time without log forwarding or monitoring, which can be very undesirable.

Another simple way to say this is that logically bound images allow you to reference container images with the same confidence you can with ExecStart= in a systemd unit.

The term "logically bound" was created to contrast with physically bound images. There are some trade-offs between the two approaches. Some benefits of logically bound images are:

  • The bootc system image can be updated without re-downloading the app image bits.
  • The app images can be updated without modifying the bootc system image, this would be especially useful for development work

Using logically bound images

Each image is defined in a Podman Quadlet .image or .container file. An image is selected to be bound by creating a symlink in the /usr/lib/bootc/bound-images.d directory pointing to a .image or .container file.

With these defined, during a bootc upgrade or bootc switch the bound images defined in the new bootc image will be automatically pulled into the bootc image storage, and are available to container runtimes such as podman by explicitly configuring them to point to the bootc storage as an "additional image store", via e.g.:

podman --storage-opt=additionalimagestore=/usr/lib/bootc/storage run <image> ...

An example Containerfile

FROM quay.io/myorg/myimage:latest

COPY ./my-app.image /usr/share/containers/systemd/my-app.image
COPY ./another-app.container /usr/share/containers/systemd/another-app.container

RUN ln -s /usr/share/containers/systemd/my-app.image \
    /usr/lib/bootc/bound-images.d/my-app.image && \
    ln -s /usr/share/containers/systemd/another-app.container \
    /usr/lib/bootc/bound-images.d/another-app.container

In the .container definition, you should use:

GlobalArgs=--storage-opt=additionalimagestore=/usr/lib/bootc/storage

NOTE: Do not attempt to globally enable /usr/lib/bootc/storage in /etc/containers/storage.conf; only use the bootc storage for logically bound images, not also floating images. For more, see below.

Pull secret

Images are fetched using the global bootc pull secret by default (/etc/ostree/auth.json). It is not yet supported to configure PullSecret in these image definitions.

Garbage collection

The bootc image store is owned by bootc; images will be garbage collected when they are no longer referenced by a file in /usr/lib/bootc/bound-images.d.

Installation

Logically bound images must be present in the default container store (/var/lib/containers) when invoking bootc install; the images will be copied into the target system and present directly at boot, alongside the bootc base image.

Limitations

The only field parsed and honored by bootc currently is the Image field of a .image or .container file.

Other pull-relevant flags such as PullSecret= for example are not supported (see above). Another example unsupported flag is Arch (the default host architecture is always used).

There is no mechanism to inject arbitrary arguments to the podman pull (or equivalent) invocation used by bootc. However, many properties used for container registry interaction can be configured via containers-registries.conf and apply to all commands operating on that image.

It is not currently supported in general to launch "rootless" containers from system-owned image stores in general, whether from /var/lib/containers or the /usr/lib/bootc/storage. There is no integration between bootc and "rootless" storage today, and none is planned. Instead, it's recommended to ensure that your "system" or "rootful" containers drop privileges. More in e.g. https://github.com/containers/podman/discussions/13728.

Distro/OS installer support

At the current time, logically bound images are not supported by Anaconda.

Comparison with default podman systemd units

In the comparison below, the term "floating" will be used for non-logically bound images. These images are often fetched by e.g. podman-systemd and may be upgraded, added or removed independently of the host upgrade lifecycle.

Lifecycle

  • Floating image: The images are downloaded by the machine the first time it starts (requiring networking typically). Tools such as podman auto-update can be used to upgrade them independently of the host.
  • Logically bound image: The images are referenced by the bootable container and are ensured to be available when the (bootc based) server starts. The image is always upgraded via bootc upgrade and appears read-only to other processes (e.g. podman).

Upgrades, rollbacks and garbage collection

  • Floating image: Managed by the user (podman auto-update, podman image prune). This can be triggered at anytime independent of the host upgrades or rollbacks, and host upgrades/rollbacks do not affect the set of images.
  • Logically bound image: Managed exclusively by bootc during upgrades. The logically bound images corresponding to rollback deployments will also be retained. bootc performs garbage collection of unused images.

"rootless" container image

  • Floating image: Supported.
  • Logically bound image: Not supported (bootc cannot be invoked as non-root). Instead, it's recommended to just drop most privileges for launched logically bound containers.

Avoid using /usr/lib/bootc/storage for floating images

Because images and in particular layers of images can be removed over time as the OS upgrades, if you attempt to globally enable /usr/lib/bootc/storage in the global /etc/containers/storage.conf that would also apply to "floating" container images (i.e. the default podman run and other runtimes), it can cause a bug where floating images can later fail if layers that were reused in the LBI storage are removed. In the future, this restriction may be lifted, but at the current time you can only configure this additional storage for logically bound images.


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

Booting local builds

In some scenarios, you may want to boot a locally built container image, in order to apply a persistent hotfix to a specific server, or as part of a development/testing scenario.

Building a new local image

At the current time, the bootc host container storage is distinct from that of the podman container runtime storage (default configuration in /var/lib/containers).

It not currently streamlined to export the booted host container storage into the podman storage.

Hence today, to replicate the exact container image the host has booted, take the container image referenced in bootc status and turn it into a podman pull invocation.

Next, craft a container build file with your desired changes:

FROM <image>
RUN apt|dnf upgrade https://example.com/systemd-hotfix.package

Copying an updated image into the bootc storage

This command is straightforward; we just need to tell bootc to fetch updates from containers-storage, which is the local "application" container runtime (podman) storage:

$ bootc switch --transport containers-storage quay.io/fedora/fedora-bootc:40

From there, the new image will be queued for the next boot and a reboot will apply it.

For more on valid transports, see containers-transports.

Automating local rebuilds

The build and switch above can be automated with a systemd service and timer. For example, keep the build context in /var/lib/machine-bootc and build the image on a schedule:

# /etc/systemd/system/machine-bootc-build.service
[Unit]
Description=Build the machine-local bootc image
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
WorkingDirectory=/var/lib/machine-bootc
ExecStart=/usr/bin/podman build --security-opt=label=disable --pull=newer --tag localhost/machine-bootc:latest .
# /etc/systemd/system/machine-bootc-build.timer
[Unit]
Description=Build the machine-local bootc image daily

[Timer]
OnCalendar=*-*-* 06:00:00
Persistent=true

[Install]
WantedBy=timers.target
$ sudo systemctl daemon-reload
$ sudo systemctl enable --now machine-bootc-build.timer

This automates only the build. Staging the new image with bootc switch and rebooting are intentionally left as explicit operations; automate them only with a reboot policy appropriate for the machine.

Automating the rebuild is only worthwhile if the build reliably produces a new image when, and only when, an input changed. With --pull=newer an identical rebuild is a full cache hit and yields the same image, so nothing new is deployed. But that relies on the build cache, not on reproducible output: a step whose result is not deterministic (regenerating an initramfs is a common example) can produce a different image on a cache-busted rebuild even when nothing meaningful changed, causing needless reboots. Prefer reproducible build steps for anything driven on a timer.


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

Managing the initramfs after installation

The initramfs is part of the container image and is updated together with the image. On systems using the OSTree backend with a split kernel and initramfs, the canonical path is /usr/lib/modules/$kver/initramfs.img. Editing the copy in /boot is not supported because bootc replaces it from the container image during an update.

For how to modify and regenerate the initramfs inside an image build, including dracut drop-in configuration, see your operating system's documentation, for example Fedora's bootc initramfs guide.

Prefer generic configuration

Content that at first looks machine-specific often does not need to be. A single generic image can carry configuration that dispatches on a machine-specific identifier at runtime — a hardware MAC address, a DMI property, a disk serial, and so on. A udev rule that matches particular hardware, for instance, can ship in the base or a shared derived image and still only act on the machine that has that hardware.

Reach for a machine-local image only when the content genuinely cannot be expressed generically. Keeping configuration in shared images means fewer distinct images to build, test, and update.

Machine-local initramfs content

When machine-specific initramfs content really is required — such as a udev rule needed to unlock local storage that cannot be selected generically — and until bootc has a dedicated interface for this use case, build a machine-local derived image containing the configuration and its regenerated initramfs. This keeps the image, rather than mutable files in /boot, as the source of truth.

This procedure applies to the OSTree backend with a split kernel and initramfs. It does not apply to sealed composefs/UKI images.

Create a build context containing the machine-specific files. For example:

.
├── Containerfile
└── 98-storage.rules

The Containerfile derives from the image shown by bootc status, adds the files, and regenerates the initramfs. Configure dracut with a drop-in under /usr/lib/dracut/dracut.conf.d and regenerate for the image's kernel, per your OS's initramfs documentation:

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

COPY 98-storage.rules /etc/udev/rules.d/98-storage.rules
RUN echo 'install_items+=" /etc/udev/rules.d/98-storage.rules "' \
      > /usr/lib/dracut/dracut.conf.d/50-storage.conf

RUN set -xe; kver=$(ls /usr/lib/modules); \
    env DRACUT_NO_XATTR=1 dracut -vf "/usr/lib/modules/${kver}/initramfs.img" "$kver"; \
    bootc container lint

A bootc image must contain exactly one kernel, so ls /usr/lib/modules must resolve to a single directory; bootc container lint checks this image invariant. Dracut must be told the kernel version explicitly, because its default targets the running kernel, which is not what a build should use. The exact dracut modules and arguments depend on the base image and the content being added.

Build the image locally and deploy it exactly as any other local build; see Booting local builds for building against the booted image, the containers-storage transport, and automating the rebuild. Rebuild and switch to this derived image whenever either the base image or the machine-specific configuration changes.

The rpm-ostree client-side initramfs mechanism

On rpm-ostree-managed systems, rpm-ostree initramfs --enable enables client-side initramfs regeneration and accepts additional dracut arguments. However, this mechanism predates bootc and records the regenerated initramfs as a local rpm-ostree modification in the deployment origin (a regenerate-initramfs key under [rpmostree]).

Bootc does not currently know how to reproduce or carry that configuration onto a new container-image deployment. It marks a deployment with rpm-ostree local modifications as incompatible, and bootc upgrade refuses to update it with an error such as "Deployment contains local rpm-ostree modifications". Running rpm-ostree reset removes the local modifications and allows bootc to manage the deployment again, but also removes the client-side initramfs configuration. It may remove other rpm-ostree package layering and overrides as well, so inspect the pending changes before running it. Therefore, do not use rpm-ostree initramfs --enable for this workflow if the system is intended to continue receiving updates through bootc; use a derived container image instead. See also Relationship with rpm-ostree.

Future direction

UKI add-ons are the intended mechanism for adding machine-specific kernel arguments or initrd content without rebuilding a Unified Kernel Image. The bootc project is working toward this model for composefs/UKI systems.

For OSTree environments that do not use composefs with sealed UKIs, support for supplementary initrds has been requested in ostree#3634, and a general bootc interface is being designed in bootc#2414. Until that design is implemented, use the machine-local derived-image workflow above.


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

NAME

bootc - Deploy and transactionally in-place with bootable container images

SYNOPSIS

bootc [OPTIONS...] <SUBCOMMAND>

DESCRIPTION

Deploy and transactionally in-place with bootable container images.

The bootc project currently uses ostree-containers as a backend to support a model of bootable container images. Once installed, whether directly via bootc install (executed as part of a container) or via another mechanism such as an OS installer tool, further updates can be pulled and bootc upgrade.

For guides to building, installing, and managing bootable images, see bootc-docs(7).

SUBCOMMANDS

CommandDescription
bootc upgradeDownload and queue an updated container image to apply
bootc switchTarget a new container image reference to boot
bootc rollbackChange the bootloader entry ordering; the deployment under rollback will be queued for the next boot, and the current will become rollback. If there is a staged entry (an unapplied, queued upgrade) then it will be discarded
bootc editApply full changes to the host specification
bootc statusDisplay status
bootc usr-overlayAdd a transient overlayfs on /usr
bootc installInstall the running container to a target
bootc containerOperations which can be executed as part of a container build
bootc loader-entriesOperations on Boot Loader Specification (BLS) entries

SEE ALSO

bootc-docs(7)

VERSION


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

NAME

bootc-config - Configuration file format for bootc

SYNOPSIS

/etc/bootc/config.toml

DESCRIPTION

The bootc configuration file uses TOML format to specify various settings for bootc operation.

FILE FORMAT

The configuration file is in TOML format with the following sections:

[core]

Core configuration options.

auto_updates = boolean Enable or disable automatic updates. Default: false

update_interval = string Update check interval (e.g., "daily", "weekly"). Default: "weekly"

[storage]

Storage-related configuration.

root = path Root storage path. Default: "/sysroot/ostree"

EXAMPLES

A basic configuration file:

[core]
auto_updates = true
update_interval = "daily"

[storage]
root = "/var/lib/bootc"

FILES

/etc/bootc/config.toml System-wide configuration file

SEE ALSO

bootc(8), toml(5)

VERSION


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

NAME

bootc-config-diff - Diff current /etc configuration versus default

SYNOPSIS

bootc config-diff

DESCRIPTION

Diff current /etc configuration versus default

EXAMPLES

TODO: Add practical examples showing how to use this command.

SEE ALSO

bootc(8)

VERSION


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

NAME

bootc-edit - Apply full changes to the host specification

SYNOPSIS

bootc edit [OPTIONS...]

DESCRIPTION

Apply full changes to the host specification.

This command operates very similarly to kubectl apply; if invoked interactively, then the current host specification will be presented in the system default \$EDITOR for interactive changes.

It is also possible to directly provide new contents via bootc edit \--filename.

Only changes to the spec section are honored.

OPTIONS

-f, --filename=FILENAME

Use filename to edit system specification

--quiet

Don't display progress

VERSION


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

NAME

bootc-status - Display status

SYNOPSIS

bootc status [OPTIONS...]

DESCRIPTION

Display status.

If standard output is a terminal, this will output a description of the bootc system state. If standard output is not a terminal, output a YAML-formatted object using a schema intended to match a Kubernetes resource that describes the state of the booted system.

Parsing output via programs

Either the default YAML format or --format=json can be used. Do not attempt to explicitly parse the output of --format=humanreadable as it will very likely change over time.

Programmatically detecting whether the system is deployed via bootc

Invoke e.g. bootc status --json, and check if status.booted is not null.

Detecting rpm-ostree vs bootc

There is no "bootc runtime". When used with the default ostree backend, bootc and tools like rpm-ostree end up sharing the same code and doing effectively the same thing. Hence, there isn't a mechanism to detect if a system "is bootc" or "is rpm-ostree".

However, if the incompatible flag is set on a deployment, then there are layered packages and rpm-ostree must be used for mutation.

OPTIONS

--format=FORMAT

The output format

Possible values:
- humanreadable
- yaml
- json

--format-version=FORMAT_VERSION

The desired format version. There is currently one supported version, which is exposed as both `0` and `1`. Pass this option to explicitly request it; it is possible that another future version 2 or newer will be supported in the future

--booted

Only display status for the booted deployment

-v, --verbose

Include additional fields in human readable format

EXAMPLES

Show current system status:

bootc status

Show status in JSON format:

bootc status --format=json

Show detailed status with verbose output:

bootc status --verbose

Show only booted deployment status:

bootc status --booted

SEE ALSO

bootc(8), bootc-upgrade(8), bootc-switch(8), bootc-rollback(8)

VERSION


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

NAME

bootc-upgrade - Download and queue an updated container image to apply

SYNOPSIS

bootc upgrade [OPTIONS...]

DESCRIPTION

Download and queue an updated container image to apply.

This does not affect the running system; updates operate in an "A/B" style by default.

A queued update is visible as staged in bootc status.

Checking for Updates

The --check option allows you to verify if updates are available without downloading the full image layers. This only downloads the updated manifest and image configuration (typically kilobyte-sized metadata), making it much faster than a full upgrade.

Applying Updates

Currently by default, the update will be applied at shutdown time via ostree-finalize-staged.service. There is also an explicit bootc upgrade --apply verb which will automatically take action (rebooting) if the system has changed.

However, in the future this is likely to change such that reboots outside of a bootc upgrade --apply do not automatically apply the update in addition.

Soft Reboot

For shared --apply and --soft-reboot behavior, see Soft reboots.

OPTIONS

--quiet

Don't display progress

--check

Check if an update is available without applying it

--apply

Restart or reboot into the new target image

--soft-reboot=SOFT_REBOOT

Configure soft reboot behavior

Possible values:
- required
- auto

--download-only

Download and stage the update without applying it

--from-downloaded

Apply a staged deployment that was previously downloaded with --download-only

--tag=TAG

Upgrade to a different tag of the currently booted image

EXAMPLES

Check for available updates:

bootc upgrade --check

Upgrade and immediately apply the changes:

bootc upgrade --apply

Upgrade with soft reboot if possible:

bootc upgrade --apply --soft-reboot=auto

Upgrade to a different tag:

bootc upgrade --tag v1.2

Check if a specific tag has updates before applying:

bootc upgrade --tag prod --check

Upgrade to a tag and immediately apply:

bootc upgrade --tag v2.0 --apply

SEE ALSO

bootc(8), bootc-switch(8), bootc-status(8), bootc-rollback(8)

VERSION


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

NAME

bootc-switch - Target a new container image reference to boot

SYNOPSIS

bootc switch [OPTIONS...] <TARGET>

DESCRIPTION

Target a new container image reference to boot.

This is almost exactly the same operation as upgrade, but additionally changes the container image reference instead.

Usage

A common pattern is to have a management agent control operating system updates via container image tags; for example, quay.io/exampleos/someuser:v1.0 and quay.io/exampleos/someuser:v1.1 where some machines are tracking :v1.0, and as a rollout progresses, machines can be switched to v:1.1.

It is also supported to provide explicit digests, via e.g. bootc switch quay.io/exampleos/someuser@sha256:9cca0703342e24806a9f64e08c053dca7f2cd90f10529af8ea872afb0a0c77d4. When you do this, bootc upgrade will always be a no-op. In this model, upgrades are then always triggered by further switch operations.

Applying Changes

The --apply option will automatically restart the system if it has changed after switching to the new image.

Soft Reboot

For shared --apply and --soft-reboot behavior, see Soft reboots.

OPTIONS

TARGET

Target image to use for the next boot. Required unless `--from-downloaded` is present

--quiet

Don't display progress

--apply

Restart or reboot into the new target image

--soft-reboot=SOFT_REBOOT

Configure soft reboot behavior

Possible values:
- required
- auto

--transport=TRANSPORT

The transport; e.g. registry, oci, oci-archive, docker-daemon, containers-storage.  Defaults to `registry`

Default: registry

--download-only

Download and stage the update without applying it

--from-downloaded

Apply a staged deployment that was previously downloaded with --download-only

--enforce-container-sigpolicy

This is the inverse of the previous `--target-no-signature-verification` (which is now a no-op)

--retain

Retain reference to currently booted image

EXAMPLES

Switch to a different image version:

bootc switch quay.io/exampleos/myapp:v1.1

Switch and immediately apply the changes:

bootc switch --apply quay.io/exampleos/myapp:v1.1

Switch with soft reboot if possible:

bootc switch --apply --soft-reboot=auto quay.io/exampleos/myapp:v1.1

SEE ALSO

bootc(8), bootc-upgrade(8), bootc-status(8), bootc-rollback(8)

VERSION


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

NAME

bootc-rollback - Change the bootloader entry ordering

SYNOPSIS

bootc rollback [OPTIONS...]

DESCRIPTION

Change the bootloader entry ordering; the deployment under rollback will be queued for the next boot, and the current will become rollback. If there is a staged entry (an unapplied, queued upgrade) then it will be discarded.

Note that absent any additional control logic, if there is an active agent doing automated upgrades (such as the default bootc-fetch-apply-updates.timer and associated .service) the change here may be reverted. It's recommended to only use this in concert with an agent that is in active control.

A systemd journal message will be logged with MESSAGE_ID=26f3b1eb24464d12aa5e7b544a6b5468 in order to detect a rollback invocation.

Note on Rollbacks and the /etc Directory

When you perform a rollback (e.g., with bootc rollback), any changes made to files in the /etc directory won't carry over to the rolled-back deployment. The /etc files will revert to their state from that previous deployment instead.

This is because bootc rollback just reorders the existing deployments. It doesn't create new deployments. The /etc merges happen when new deployments are created.

OPTIONS

--apply

Restart or reboot into the rollback image

--soft-reboot=SOFT_REBOOT

Configure soft reboot behavior

Possible values:
- required
- auto

EXAMPLES

Rollback to the previous deployment:

bootc rollback

Rollback and immediately apply the changes:

bootc rollback --apply

Rollback with soft reboot if possible:

bootc rollback --apply --soft-reboot=auto

SEE ALSO

bootc(8), bootc-upgrade(8), bootc-switch(8), bootc-status(8)

VERSION


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

NAME

bootc-usr-overlay - Adds a transient overlayfs on /usr that will be discarded on reboot

SYNOPSIS

bootc usr-overlay [OPTIONS...]

DESCRIPTION

Adds a transient overlayfs on /usr that will be discarded on reboot. The overlayfs is read/write by default.

USE CASES

A common pattern is wanting to use tracing/debugging tools, such as strace that may not be in the base image. A system package manager such as apt or dnf can apply changes into this transient overlay that will be discarded on reboot.

STORAGE AND MEMORY REQUIREMENTS

The transient overlay is backed by a tmpfs filesystem, which means all data written to the overlay (i.e. installed packages) is stored in system memory (RAM), not on persistent disk. By default the kernel sizes this tmpfs at 50% of physical RAM.

On systems with limited memory, installing large packages into the overlay can exhaust available RAM and result in ENOSPC ("No space left on device") errors. There is currently no pre-flight space check and no early warning before the tmpfs is full.

Keep this in mind when using transient overlays in memory-constrained environments such as CI runners or small virtual machines. If the combined size of the packages being installed approaches half of the system's RAM, consider increasing the VM's memory allocation instead.

/ETC AND /VAR

However, this command has no effect on /etc and /var - changes written there will persist. It is common for package installations to modify these directories.

UNMOUNTING

Almost always, a system process will hold a reference to the open mount point. You can however invoke umount -l /usr to perform a "lazy unmount".

OPTIONS

--read-only

Mount the overlayfs as read-only. A read-only overlayfs is useful since it may be remounted as read/write in a private mount namespace and written to while the mount point remains read-only to the rest of the system

VERSION


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

NAME

bootc-fetch-apply-updates.service

DESCRIPTION

This service causes bootc to perform the following steps:

  • Check the source registry for an updated container image
  • If one is found, download it
  • Reboot

This service also comes with a companion bootc-fetch-apply-updates.timer systemd unit. The current default systemd timer shipped in the upstream project is enabled for daily updates.

However, it is fully expected that different operating systems and distributions choose different defaults.

CUSTOMIZING UPDATES

Note that all three of these steps can be decoupled; they are:

  • bootc upgrade --check
  • bootc upgrade
  • bootc upgrade --apply

SEE ALSO

bootc(1)

VERSION


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

NAME

bootc-status-updated.path

DESCRIPTION

This unit watches the bootc root directory (/ostree/bootc) for modification, and triggers the companion bootc-status-updated.target systemd unit.

The bootc program updates the mtime on its root directory when the contents of bootc status changes as a result of an update/upgrade/edit/switch/rollback operation.

SEE ALSO

bootc(1), bootc-status-updated.target(5)

VERSION


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

NAME

bootc-status-updated.target

DESCRIPTION

This unit is triggered by the companion bootc-status-updated.path systemd unit. This target is intended to enable users to add custom services to trigger as a result of bootc status changing.

Add the following to your unit configuration to active it when bootc status changes:

[Install]
WantedBy=bootc-status-updated.target

SEE ALSO

bootc(1), bootc-status-updated.path(5)

VERSION


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

Using bootc via API

At the current time, bootc is primarily intended to be driven via a fork/exec model. The core CLI verbs are stable and will not change.

Using bootc edit and bootc status --json

While bootc does not depend on Kubernetes, it does currently also offer a Kubernetes style API, especially oriented towards the spec and status and other conventions.

In general, most use cases of driving bootc via API are probably most easily done by forking off bootc upgrade when desired, and viewing bootc status --json --format-version=1.

JSON Schema

The current API org.containers.bootc/v1 is stable. In order to support the future introduction of a v2 or newer format, please change your code now to explicitly request --format-version=1 as referenced above. (Available since bootc 0.1.15, --format-version=0 in bootc 0.1.14).

There is a JSON schema generated from the Rust source code available here: host-v1.schema.json.

A common way to use this is to run a code generator such as go-jsonschema on the input schema.


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

Installing "bootc compatible" images

A key goal of the bootc project is to think of bootable operating systems as container images. Docker/OCI container images are just tarballs wrapped with some JSON. But in order to boot a system (whether on bare metal or virtualized), one needs a few key components:

  • bootloader
  • kernel (and optionally initramfs)
  • root filesystem (xfs/ext4/btrfs etc.)

Bootloader installation depends on the platform and selected bootloader. For example, GRUB installation uses bootupd, while systemd-boot uses bootctl and s390x uses zipl. Bootloader installation can also be disabled. The default expectation is that bootloader contents and install logic come from the container image in a bootc based system.

The Linux kernel (and optionally initramfs) is embedded in the container image; the canonical location is /usr/lib/modules/$kver/vmlinuz, and the initramfs should be in initramfs.img in that directory.

The bootc install command bridges the two worlds of a standard, runnable OCI image and a bootable system by running tooling logic embedded in the container image to create the filesystem and bootloader setup dynamically. This requires running the container via --privileged; it uses the running Linux kernel on the host to write the file content from the running container image; not the kernel inside the container.

There are two sub-commands: bootc install to-disk and bootc install to-filesystem.

However, nothing else (external) is required to perform a basic installation to disk - the container image itself comes with a baseline self-sufficient installer that sets things up ready to boot.

Internal vs external installers

The bootc install to-disk process only sets up a very simple filesystem layout, using the default filesystem type defined in the container image, plus hardcoded requisite platform-specific partitions such as the ESP.

In general, the to-disk flow should be considered mainly a "demo" for the bootc install to-filesystem flow, which can be used by "external" installers today. For example, in the Fedora/CentOS bootc project project, there are two "external" installers in Anaconda and bootc-image-builder.

More on this below.

Executing bootc install

The two installation commands allow you to install the container image either directly to a block device (bootc install to-disk) or to an existing filesystem (bootc install to-filesystem).

The installation commands MUST be run from the container image that will be installed, using --privileged and a few other options. This means you are (currently) not able to install bootc to an existing system and install your container image. Failure to run bootc from a container image will result in an error.

Here's an example of using bootc install (root/elevated permission required):

podman run --rm --privileged --pid=host --ipc=host \
  -v /var/lib/containers:/var/lib/containers -v /dev:/dev \
  --security-opt label=type:unconfined_t \
  <image> bootc install to-disk /path/to/disk

Note that while --privileged is used, this command will not perform any destructive action on the host system. Among other things, --privileged makes sure that all host devices are mounted into container. /path/to/disk is the host's block device where <image> will be installed on.

The --pid=host --ipc=host --security-opt label=type:unconfined_t today make it more convenient for bootc to perform some privileged operations; in the future these requirements may be dropped.

The -v /var/lib/containers:/var/lib/containers option is required in order for the container to access its own underlying image, which is used by the installation process.

Jump to the section for install to-filesystem later in this document for additional information about that method.

"day 2" updates, security and fetch configuration

By default the bootc install path will find the pull specification used for the podman run invocation and use it to set up "day 2" OS updates that bootc update will use.

For example, if you invoke podman run --privileged ... quay.io/examplecorp/exampleos:latest bootc install ... then the installed operating system will fetch updates from quay.io/examplecorp/exampleos:latest. This can be overridden via --target_imgref; this is handy in cases like performing installation in a manufacturing environment from a mirrored registry.

By default, the installation process will verify that the container (representing the target OS) can fetch its own updates.

Additionally note that to perform an upgrade with a target image reference set to an authenticated registry, you must provide a pull secret. One path is to embed the pull secret into the image in /etc/ostree/auth.json.

Configuring the default root filesystem type

To use the to-disk installation flow, the container should include a root filesystem type. If it does not, then each user will need to specify install to-disk --filesystem.

To set a default filesystem type for bootc install to-disk as part of your OS/distribution base image, create a file named /usr/lib/bootc/install/00-<osname>.toml with the contents of the form:

[install.filesystem.root]
type = "xfs"

For example, a derived image can supply 50-myos.toml with type = "btrfs" to override this default. See bootc-install-config for file discovery, merge precedence, and the available configuration fields.

The storage backend

The storage backend is determined by the image. It is installed with the composefs backend when it ships a UKI, or when it matches both of these rules:

  • it ships /usr/lib/composefs/setup-root-conf.toml (which may be empty; see bootc-setup-root-conf.toml(5));
  • it has no ostree prepare-root.conf, in either /usr/lib/ostree or /etc/ostree.

Any other image is installed with ostree. bootc container lint uses the same setup-root-conf.toml marker to stop requiring the ostree-specific parts of an image, such as the /ostree symlink.

Like the install configuration, these files are read from the root bootc runs in, also with --source-imgref.

Installing an "unconfigured" image

The bootc project aims to support generic/general-purpose operating systems and distributions that will ship unconfigured images. An unconfigured image does not have a default password or SSH key, etc.

For more information, see Image building and configuration guidance.

composefs backend

The storage backend is selected by the image; see composefs backend and sealed images.

With a traditional kernel and initramfs, the initramfs of a composefs image must also include bootc's dracut module (51bootc), which mounts the composefs root. That module is not enabled by default: the reference baseimage configuration enables it, and other base images need to as well; see bootc-root-setup.service(5).

On a root filesystem without fs-verity support (such as XFS), fs-verity is made optional automatically for a traditional kernel install; --allow-missing-verity does this explicitly.

More advanced installation with to-filesystem

The basic bootc install to-disk logic is really a pretty small (but opinionated) wrapper for a set of lower level tools that can also be invoked independently.

The bootc install to-disk command is effectively:

  • mkfs.$fs /dev/disk
  • mount /dev/disk /mnt
  • bootc install to-filesystem --karg=root=UUID=<uuid of /mnt> --imgref $self /mnt

There may be a bit more involved here; for example configuring --block-setup tpm2-luks will configure the root filesystem with LUKS bound to the TPM2 chip, currently via systemd-cryptenroll. We don't recommend this for new deployments; see Disk encryption (e.g. LUKS) for why, and for the recommended approach of setting up encryption independently of bootc via systemd-cryptsetup or Ignition.

Some OS/distributions may not want to enable it at all; it can be configured off at build time via Cargo features.

Using bootc install to-filesystem

The usual expected way for an external storage system to work is to provide root=<UUID> and rootflags kernel arguments to describe to the initial RAM disk how to find and mount the root partition. For more on this, see the below section discussing mounting the root filesystem.

Note that if a separate /boot is needed (e.g. for LUKS) you will also need to provide --boot-mount-spec UUID=....

The bootc install to-filesystem command allows an operating system or distribution to ship a separate installer that creates more complex block storage or filesystem setups, but reuses the "top half" of the logic. For example, Anaconda has a bootc kickstart command that drives to-filesystem this way.

Postprocessing after to-filesystem

Some installation tools may want to inject additional data, such as adding an /etc/hostname into the target root. Mount the offline deployment explicitly in the caller's mount namespace:

mkdir /mnt/installed
bootc install mount --sysroot /path/to/target --latest /mnt/installed
# mutate /mnt/installed/etc and /mnt/installed/var as needed
umount -R /mnt/installed

The deployment root and /usr are always read-only, while /etc and /var are writable unless --read-only is given. /var is the deployment's state directory on the target sysroot; if you set up a separate /var filesystem, mount it on top yourself. The mounts live in the caller's mount namespace until it removes them with umount -R, so use a target with no unrelated mounts below it. See bootc-install-mount(8).

However, for tools that do perform any changes, there is a new bootc install finalize command which is optional, but recommended to run as the penultimate step before unmounting the target filesystem.

This command will perform some basic sanity checks and may also perform fixups on the target root. For example, a direction currently for bootc is to stop using /etc/fstab. While install finalize does not do this today, in the future it may automatically migrate etc/fstab to rootflags kernel arguments.

Using bootc install to-disk --via-loopback

Because every bootc system comes with an opinionated default installation process, you can create a raw disk image that you can boot via virtualization. Run these commands as root:

truncate -s 10G myimage.raw
podman run --rm --privileged --pid=host --ipc=host \
  --security-opt label=type:unconfined_t -v /dev:/dev \
  -v /var/lib/containers:/var/lib/containers -v .:/output \
  <yourimage> bootc install to-disk --generic-image \
  --via-loopback /output/myimage.raw

Notice that we use --generic-image for this use case.

Set the environment variable BOOTC_DIRECT_IO=on to create the loopback device with direct-io enabled.

Using bootc install to-existing-root

This is a variant of install to-filesystem, which maximizes convenience for using an existing Linux system, converting it into the target container image. Note that the /boot partition and the EFI/ and loader/ directories of the ESP will be reinitialized - so this is a somewhat destructive operation for the existing Linux installation. Other ESP content (e.g. Asahi's m1n1/) is preserved.

Also, because the filesystem is reused, it's required that the target system kernel support the root storage setup already initialized.

The core command should look like this (root/elevated permission required):

podman run --rm --privileged -v /dev:/dev \
  -v /var/lib/containers:/var/lib/containers -v /:/target \
  --pid=host --security-opt label=type:unconfined_t \
  <image> \
  bootc install to-existing-root

It is assumed in this command that the target rootfs is passed via -v /:/target at this time.

As noted above, the data in /boot will be wiped, but everything else in the existing operating / is NOT automatically cleaned up. This can be useful, because it allows the new image to access data from the previous host system. For example, container images, database, user home directory data, and config files in /etc are all available after the subsequent reboot in /sysroot (which is the "physical root").

However, previous mount points or subvolumes will not be automatically mounted in the new system, e.g. a btrfs subvolume for /home will not be automatically mounted to /sysroot/home. These filesystems will persist and can be handled any way you want like manually mounting them or defining the mount points as part of the bootc image.

Managing configuration: before and after reboot

There are two distinct scenarios for managing configuration with to-existing-root:

Before rebooting (injecting new configuration): You can inject new configuration files into the newly installed deployment before the first boot. This is useful for adding custom /etc/fstab entries, systemd mount units, or other fresh configuration that the new system should have from the start.

After rebooting (migrating old configuration): You can copy or migrate configuration and data from the old system (now accessible at /sysroot) to the new system. This is useful for preserving network settings, user accounts, or application data from the previous installation.

Before reboot: Injecting new configuration

After running bootc install to-existing-root, you may want to inject configuration files (such as /etc/fstab, systemd units, or other configuration) into the newly installed system before rebooting. Mount the target explicitly and mutate it through the mounted view:

mkdir /mnt/installed
bootc install mount --sysroot /target --latest /mnt/installed

# Add a systemd mount unit
cat > /mnt/installed/etc/systemd/system/data.mount <<EOF
[Unit]
Description=Data partition

[Mount]
What=UUID=...
Where=/data
Type=xfs

[Install]
WantedBy=local-fs.target
EOF
umount -R /mnt/installed
Injecting kernel arguments for local state

An alternative approach is to key machine-local configuration from kernel arguments via the --karg option to bootc install to-existing-root.

For example with filesystem mounts, systemd offers a systemd.mount-extra that can be used instead of /etc/fstab:

bootc install to-existing-root \
  --karg="systemd.mount-extra=UUID=<uuid>:/data:xfs:defaults"

The systemd.mount-extra syntax is: source:path:type:options

After reboot: Migrating data from the old system

After rebooting into the new bootc system, the previous root filesystem is mounted at /sysroot. You can then migrate configuration and data from the old system to the new one.

Important: Any data from /etc that you want to use in the new system must be manually copied from /sysroot/etc to /etc after rebooting into the new system. There is currently no automated mechanism for migrating this configuration data. This applies to network configurations, user accounts, application settings, and other system configuration stored in /etc.

For example, after rebooting:

# Copy network configuration from the old system
cp /sysroot/etc/sysconfig/network-scripts/ifcfg-eth0 /etc/sysconfig/network-scripts/

# Copy application configuration
cp -r /sysroot/etc/myapp /etc/

A special case is using the --root-ssh-authorized-keys flag during installation to automatically inherit root's SSH keys (which may have been injected from e.g. cloud instance userdata via a tool like cloud-init). To do this, add --root-ssh-authorized-keys /target/root/.ssh/authorized_keys to the install command.

See also the bootc-install-to-existing-root(8) man page for more details.

Using system-reinstall-bootc

This is a separate binary included with bootc. It is an opinionated, interactive CLI that wraps bootc install to-existing-root. See bootc install to-existing-root for details on the installation operation.

system-reinstall-bootc can be run from an existing Linux system. It will pull the supplied image, prompt to setup SSH keys for accessing the system, and run bootc install to-existing-root with all the bind mounts and SSH keys configured.

It will also add the bootc-destructive-cleanup.service systemd unit that will run on first boot to cleanup parts of the previous system. The cleanup actions can be configured per distribution by creating a script and packaging it similar to this one for Fedora.

Using bootc install to-filesystem --source-imgref <imgref>

By default, bootc install has to be run inside a podman container. With this assumption, it can escape the container, find the source container image (including its layers) in the podman's container storage and use it to create the image.

When --source-imgref <imgref> is given, bootc no longer assumes that it runs inside podman. Instead, the given container image reference (see containers-transports(5) for accepted formats) is used to fetch the image. Note that bootc install still has to be run inside a chroot created from the container image. However, this allows users to use a different sandboxing tool (e.g. bubblewrap).

This argument is mainly useful for 3rd-party tooling for building disk images from bootable containers (e.g. based on osbuild).

Discoverable Partitions Specification (DPS)

As of bootc 1.11, the default partitioning layout for bootc install to-disk uses the Discoverable Partitions Specification (DPS) from the UAPI Group. This is an important foundation for modern Linux systems.

What is DPS?

The Discoverable Partitions Specification defines well-known partition type GUIDs for different purposes (root filesystem, ESP, swap, /home, etc.) and for different CPU architectures. When partitions use these standardized type GUIDs, systemd and other tools can automatically discover and mount them without explicit configuration.

Each supported CPU architecture has its own root partition type GUID. See the DPS specification for the complete list of partition types.

How bootc uses DPS

When bootc install to-disk creates partitions, it sets the appropriate DPS partition type GUID based on the target architecture. This enables:

  1. Automatic root discovery: With a DPS-aware bootloader and initramfs, the root filesystem can be discovered automatically without a root= kernel argument. This is handled by systemd-gpt-auto-generator.

  2. Sealed/verified boot paths: When using composefs with UKIs (Unified Kernel Images), bootc can omit the root= kernel argument entirely. The initramfs uses DPS to find the root partition, and composefs provides integrity verification.

  3. Future systemd-repart integration: DPS partition types allow systemd-repart to automatically grow or create partitions based on declarative configuration.

When is DPS used vs explicit kernel arguments?

Installation ModeRoot Discovery Method
to-diskExplicit root=UUID=... karg
to-filesystem --root-mount-spec=""DPS auto-discovery (no root= karg)
to-filesystem (default)Uses filesystem UUID as root=UUID=... karg
to-existing-rootInherits from existing system

For to-disk, bootc always injects a root=UUID=<uuid> kernel argument for compatibility, even though the partition type is set to the DPS GUID. This ensures the system boots on initramfs implementations that don't support DPS auto-discovery.

For to-filesystem, the --root-mount-spec="" option (empty string) can be used to omit the root= kernel argument entirely, enabling DPS auto-discovery. This is useful when the bootloader and initramfs both support the Boot Loader Interface.

Bootloader requirements for DPS auto-discovery

DPS auto-discovery requires a bootloader that implements the Boot Loader Interface and sets the LoaderDevicePartUUID EFI variable. Supported bootloaders include:

  • GRUB 2.12+ with the bli module (included in Fedora 43+, requires EFI boot)
  • systemd-boot (always supports the Boot Loader Interface)

Older GRUB versions (without the bli module) do not set this variable and DPS auto-discovery will not work.

Implications for external installers

If you're building tooling that uses bootc install to-filesystem, you should:

  1. Set appropriate partition types: Use the DPS type GUID for the root partition when creating partitions externally.

  2. Consider auto-discovery: If your bootloader and initramfs support DPS, you may be able to omit root= kernel arguments entirely.

  3. Use rootflags for mount options: Prefer the rootflags= kernel argument over /etc/fstab for root mount options, as this works better with composefs and DPS auto-discovery.

systemd-repart

When systemd-repart is available and repart.d definitions are present in the container image, bootc install to-disk uses systemd-repart instead of sfdisk to partition the target disk.

How definitions are discovered

Definitions are searched in the standard systemd-repart locations relative to the container root:

  • /etc/repart.d/*.conf
  • /run/repart.d/*.conf
  • /usr/local/lib/repart.d/*.conf
  • /usr/lib/repart.d/*.conf

If systemd-repart is not installed or no .conf files exist in any of these directories, bootc falls back to its built-in sfdisk partitioning.

Root partition handling

All repart.d definitions are passed to systemd-repart together so that it can plan a correct layout with proper space allocation across all partitions.

If the image's repart.d definitions do not include a root partition (Type=root), bootc automatically injects one into /run/repart.d/ before invoking systemd-repart. The generated root partition definition:

  • Uses Type=root (architecture-specific DPS GUID is resolved by systemd-repart)
  • Applies Format=<fs> from the configured root filesystem type
  • Honours --root-size if specified (via SizeMinBytes/SizeMaxBytes)
  • Otherwise takes all remaining space on the disk

This means images can ship repart.d definitions for additional partitions (e.g. /home, swap, /var) without needing to also define root. All definitions run at install time so that systemd-repart can plan the layout with correct space allocation across all partitions. This is important because the root filesystem is mounted read-only on bootc systems (/sysroot is ro), so systemd-repart cannot resize it after installation.

Important: A root filesystem type is always required. It can come from any of these sources (checked in order):

  1. --filesystem CLI argument
  2. install.filesystem.root.type in the install configuration
  3. Format= in the repart.d root partition definition

If none of these provide a filesystem type, the installation will fail.

Firstboot definitions

Images may include repart.d definitions for partitions beyond root, ESP, and xbootldr, for example /home, swap, or /var. Because all definitions run at install time, systemd-repart allocates space for all of them during installation.

DPS auto-mount and symlinked directories

systemd-gpt-auto-generator maps DPS partition types to fixed mount points (e.g. Type=home mounts at /home). If the mount point is a symlink as is common in ostree-based systems where /home -> /var/home, the auto-generated mount unit will fail.

For this to work, the container image must ensure that /home is a real directory, not a symlink. Alternatively, use Type=linux-generic with a partition label and mount it explicitly via a systemd.mount-extra kernel argument:

systemd.mount-extra=PARTLABEL=home:/var/home:ext4

Filesystem creation

When systemd-repart creates partitions, it also creates filesystems according to the Format= directive in each definition. In this case bootc reads the filesystem UUIDs assigned by systemd-repart rather than running mkfs itself.

LUKS (tpm2-luks)

systemd-repart integration is not supported with --block-setup tpm2-luks. When LUKS is configured, bootc always falls back to sfdisk partitioning. This is because LUKS needs to format the root partition with cryptsetup luksFormat after partitioning, which conflicts with systemd-repart having already created a filesystem on that partition.

Examples

For the full definition file format, see repart.d(5).

Minimal: let bootc handle root

An image that only wants a custom ESP size:

# /usr/lib/repart.d/00-esp.conf
[Partition]
Type=esp
Format=vfat
SizeMinBytes=2G
SizeMaxBytes=2G

bootc will inject a root partition definition automatically.

Finding and configuring the physical root filesystem

On a bootc system, the "physical root" is different from the "logical root" of the booted container. For more on that, see filesystem. This section is about how the physical root filesystem is discovered.

Systems using systemd will often default to using systemd-fstab-generator and/or systemd-gpt-auto-generator. Support for the latter though for the root filesystem is conditional on EFI and a bootloader implementing the bootloader interface.

Outside of the discoverable partition model, a common baseline default for installers is to set root=UUID= (and optionally rootflags=) kernel arguments as machine specific state. When using install to-filesystem, you should provide these as explicit kernel arguments.

Some installation tools may want to generate an /etc/fstab. An important consideration is that when composefs is on by default (as it is expected to be) it will no longer work to have an entry for / in /etc/fstab (or a systemd .mount unit) that handles remounting the rootfs with updated options after exiting the initrd.

In general, prefer using the rootflags kernel argument for that use case; it ensures that the filesystem is mounted with the correct options to start, and avoid having an entry for / in /etc/fstab.

The physical root is mounted at /sysroot. It is an option for legacy /etc/fstab references for / to use /sysroot by default, but rootflags is preferred.

Configuring machine-local state

Per the filesystem section, /etc and /var are machine-local state by default. To inject additional content after installation, use bootc install mount --sysroot /path/to/target --latest /mnt/installed and mutate /mnt/installed/etc or /mnt/installed/var. This is the backend-neutral interface for installation software such as Anaconda to implement %post scripts before first boot.

Provisioning and first boot

After bootc install completes, the system is ready for first boot. A key design principle is that minimal machine-specific configuration should be injected at install time. Instead, most configuration should happen at runtime—either at first boot or on every boot—using standard Linux mechanisms.

This approach has several benefits:

  • Simpler installation: The install process doesn't need to know about every possible configuration option
  • Better fits the container model: Configuration logic lives in the container image, not in external tooling
  • Easier updates: Runtime configuration naturally applies to updated deployments

Install-time configuration

When some machine-specific data must be provided at install time, the preferred approaches depend on your boot setup:

  • Type 1 BLS setups: Use bootc install --karg to inject kernel arguments. For example, --karg ip=192.168.1.100::192.168.1.1:255.255.255.0:host1::none for static IP configuration, or --karg console=ttyS0,115200 for serial console.

  • UKI setups: Use UKI addons (additional signed PE binaries containing extra kernel arguments or initrd content) to provide machine-specific configuration while preserving the signed UKI.

Runtime configuration approaches

Since bootc systems are standard Linux systems, any provisioning tool that works on Linux will work with bootc. Common approaches include:

  • cloud-init: If included in your container image, runs at first boot to configure users, SSH keys, networking, and run custom scripts
  • Ignition: Runs in the initramfs before the real root is mounted; used by Fedora CoreOS-style systems
  • systemd-firstboot: Configures locale, timezone, hostname, etc. on first boot
  • Custom systemd services: Use ConditionFirstBoot=yes for one-time setup, or run on every boot for dynamic configuration

The choice of provisioning tool depends on your base image and deployment environment—bootc itself is agnostic.

SSH key injection

For simple SSH access without a full provisioning system, bootc provides the --root-ssh-authorized-keys option:

bootc install to-disk --root-ssh-authorized-keys /path/to/authorized_keys /dev/sda

This writes a systemd-tmpfiles configuration that ensures the SSH keys are present on every boot, even if /root is a tmpfs.

The .bootc-aleph.json file

After installation, bootc writes a JSON file at the root of the physical filesystem (.bootc-aleph.json) containing installation provenance information:

  • The source image reference and digest
  • The target image reference (if provided)
  • The OCI image labels from the installed image
  • Installation timestamp
  • bootc version
  • Kernel version
  • SELinux state

This file is useful for auditing and understanding how a system was provisioned. From the booted system, this file is accessible at /sysroot/.bootc-aleph.json.


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

NAME

bootc-install - Install the running container to a target

SYNOPSIS

bootc install [OPTIONS...] <SUBCOMMAND>

DESCRIPTION

Install the running container to a target.

Understanding installations

The bootc install flow turns a container image into a bootable system, including filesystem, bootloader, and update metadata setup. It is not simply a copy of the container filesystem.

See Installing bootc compatible images for the installation model, prerequisites, and end-to-end examples. This reference documents the command and its subcommands.

Secure Boot Keys

When installing with systemd-boot, bootc can let systemd-boot can handle enrollment of Secure Boot keys by putting signed EFI signature lists in /usr/lib/bootc/install/secureboot-keys which will copy over into ESP/loader/keys after bootloader installation. The keys will be copied to loader/keys subdirectory of the ESP. after installing systemd-boot to the system. More information on how key enrollment works with systemd-boot is available in the systemd-boot man page.

SUBCOMMANDS

CommandDescription
bootc install mountMount an installed deployment into a caller-owned directory
bootc install to-diskInstall to the target block device
bootc install to-filesystemInstall to an externally created filesystem structure
bootc install to-existing-rootInstall to the host root filesystem
bootc install finalizeExecute this as the penultimate step of an installation using install to-filesystem
bootc install ensure-completionIntended for use in environments that are performing an ostree-based installation, not bootc
bootc install print-configurationOutput JSON to stdout that contains the merged installation configuration as it may be relevant to calling processes using install to-filesystem that in particular want to discover the desired root filesystem type from the container image

VERSION


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

NAME

bootc-install-config.toml

DESCRIPTION

The bootc install process supports customization through TOML drop-in files in /usr/lib/bootc/install, /usr/local/lib/bootc/install, /etc/bootc/install, and /run/bootc/install. If the same filename occurs in multiple directories, the file in the later directory in this list takes precedence. The selected files are then processed in alphanumerical filename order.

Fragments whose match_architectures includes the current architecture, or which omit that field, are merged. Values such as the root filesystem type are overridden when specified in a later fragment. The kargs and karg-deletes lists are appended instead of replaced.

The individual files are merged into a single final installation config, so it is supported for e.g. a container base image to provide a default root filesystem type, that can be overridden in a derived container image.

install

This is the only defined toplevel table.

The install section supports these subfields:

  • block: An array of supported to-disk backends enabled by this base container image; if not specified, this will just be direct. The only other supported value is tpm2-luks. The first value specified will be the default. To enable both, use block = ["direct", "tpm2-luks"].
  • filesystem: See below.
  • kargs: An array of strings; this will be appended to the set of kernel arguments.
  • match_architectures: An array of strings; this filters the install config.
  • ostree: See below.
  • stateroot: The stateroot name to use. Defaults to default.
  • root-mount-spec: A string specifying the root filesystem mount specification. For example, UUID=2e9f4241-229b-4202-8429-62d2302382e1 or LABEL=rootfs. If not provided, the UUID of the target filesystem will be used. An empty string signals to omit boot mount kargs entirely.
  • boot-mount-spec: A string specifying the /boot filesystem mount specification. If not provided and /boot is a separate mount, its UUID will be used. An empty string signals to omit boot mount kargs entirely.
  • discoverable-partitions: Boolean. When true, root discovery uses the Discoverable Partitions Specification via systemd-gpt-auto-generator and the root= kernel argument is omitted. This requires the bootloader to implement the Boot Loader Interface (BLI); systemd-boot always does, GRUB needs the bli module (available in newer builds). Defaults to true when using systemd-boot, false otherwise.
  • enforce-container-sigpolicy: A boolean that controls whether to enforce that containers-policy.json (see man containers-policy.json for the full search path) includes a default policy which requires signatures. When true, image pulls will be rejected if the policy file specifies insecureAcceptAnything as the default. Defaults to false. This is equivalent to the --enforce-container-sigpolicy CLI flag.

filesystem

There is one valid field:

  • root: An instance of "filesystem-root"; see below

filesystem-root

There is one valid field:

type: This can be any basic Linux filesystem with a mkfs.$fstype. For example, ext4, xfs, etc.

ostree

Configuration options for the ostree repository. There is one valid field:

  • bls-append-except-default: A string of kernel arguments that will be appended to Boot Loader Spec entries, except for the default entry. This is useful for configuring arguments that should only apply to non-default deployments.

bootupd

Configuration options for bootupd, responsible of setting up the bootloader. There is only one valid field:

  • skip-boot-uuid: A boolean that controls whether to skip writing partition UUIDs to the bootloader configuration. When true, bootupd is invoked with --with-static-configs instead of --write-uuid. Defaults to false (UUIDs are written by default).

Examples

[install.filesystem.root]
type = "xfs"

[install]
kargs = ["nosmt", "console=tty0"]
stateroot = "myos"
root-mount-spec = "LABEL=rootfs"
boot-mount-spec = "UUID=abcd-1234"
enforce-container-sigpolicy = true

[install.ostree]
bls-append-except-default = 'grub_users=""'

Enable DPS auto-discovery for root (requires a BLI-capable bootloader):

[install]
discoverable-partitions = true

SEE ALSO

bootc(1)

VERSION


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

NAME

bootc-install-to-disk - Install to the target block device

SYNOPSIS

bootc install to-disk [OPTIONS...] <DEVICE>

DESCRIPTION

Install to the target block device.

This command must be invoked inside of the container, which will be installed. The container must be run in --privileged mode, and hence will be able to see all block devices on the system.

The default storage layout uses the root filesystem type configured in the container image, alongside any required system partitions such as the EFI system partition. Use install to-filesystem for anything more complex such as RAID, LVM, LUKS etc.

Partitioning details

The default as of bootc 1.11 uses the Discoverable Partitions Specification (DPS) for the generated root filesystem, as well as any required system partitions such as the EFI system partition.

Partition layout

The installer creates a GPT partition table with architecture-appropriate partitions. The exact layout varies by architecture but generally includes:

  • Boot partition (architecture-specific): BIOS boot for x86_64, PReP for ppc64le, etc.
  • ESP: EFI System Partition on UEFI architectures (x86_64, aarch64), at least 512 MiB
  • Boot: Separate /boot partition, only created when using LUKS encryption
  • Root: The root filesystem, using the remaining disk space

The root partition uses an architecture-specific DPS type GUID. Specific partition sizes and type GUIDs are implementation details that may change between versions; use install to-filesystem if you need precise control over the partition layout.

Root filesystem discovery

The root partition can be discovered at boot time in two ways:

  • UUID mode (default): A kernel argument root=UUID=<uuid> is injected, providing broad compatibility with all initramfs implementations and bootloaders.

  • DPS auto-discovery: The root= kernel argument is omitted entirely. systemd-gpt-auto-generator in the initramfs discovers the root partition by its DPS type GUID. This enables transparent block-layer changes (such as adding LUKS encryption) without updating kernel arguments. DPS auto-discovery requires the bootloader to implement the Boot Loader Interface (BLI). systemd-boot always supports this; GRUB supports it only with newer builds that include the bli module.

When using systemd-boot, DPS auto-discovery is enabled by default. For GRUB, container base images that ship a BLI-capable build should set discoverable-partitions = true in their install configuration (see bootc-install-config(5)).

OPTIONS

DEVICE

Target block device for installation.  The entire device will be wiped

This argument is required.

--wipe

Automatically wipe all existing data on device

--block-setup=BLOCK_SETUP

Target root block device setup

Possible values:
- direct
- tpm2-luks

--filesystem=FILESYSTEM

Target root filesystem type

Possible values:
- xfs
- ext4
- btrfs

--root-size=ROOT_SIZE

Size of the root partition (default specifier: M).  Allowed specifiers: M (mebibytes), G (gibibytes), T (tebibytes)

--source-imgref=SOURCE_IMGREF

Install the system from an explicitly given source

--target-transport=TARGET_TRANSPORT

The transport; e.g. oci, oci-archive, containers-storage.  Defaults to `registry`

Default: registry

--target-imgref=TARGET_IMGREF

Specify the image to fetch for subsequent updates

--enforce-container-sigpolicy

This is the inverse of the previous `--target-no-signature-verification` (which is now a no-op).  Enabling this option enforces that `containers-policy.json` (see `man containers-policy.json` for the full search path) includes a default policy which requires signatures

--run-fetch-check

Verify the image can be fetched from the bootc image. Updates may fail when the installation host is authenticated with the registry but the pull secret is not in the bootc image

--skip-fetch-check

Verify the image can be fetched from the bootc image. Updates may fail when the installation host is authenticated with the registry but the pull secret is not in the bootc image

--disable-selinux

Disable SELinux in the target (installed) system

--karg=KARG

Add a kernel argument.  This option can be provided multiple times

--karg-delete=KARG_DELETE

Remove a kernel argument.  This option can be provided multiple times

--root-ssh-authorized-keys=ROOT_SSH_AUTHORIZED_KEYS

The path to an `authorized_keys` that will be injected into the `root` account

--generic-image

Perform configuration changes suitable for a "generic" disk image. At the moment:

--bound-images=BOUND_IMAGES

How should logically bound images be retrieved

Possible values:
- stored
- skip
- pull

Default: stored

--stateroot=STATEROOT

The stateroot name to use. Defaults to `default`

--bootupd-skip-boot-uuid

Don't pass --write-uuid to bootupd during bootloader installation

--bootloader=BOOTLOADER

The bootloader to use

Possible values:
- grub
- grub-cc
- systemd
- none

--via-loopback

Instead of targeting a block device, write to a file via loopback

--composefs-backend

Use the composefs backend instead of ostree. This is the default for images with a UKI, and for images with /usr/lib/composefs/setup-root-conf.toml and no ostree prepare-root.conf

Default: false

--allow-missing-verity

Make fs-verity validation optional in case the filesystem doesn't support it (composefs backend only)

Default: false

--uki-addon=UKI_ADDON

Name of the UKI addons to install without the ".efi.addon" suffix. This option can be provided multiple times if multiple addons are to be installed (composefs backend only)

--run-repart

Whether to use systemd-repart for partitioning. This requires the image to have repart.d definitions

EXAMPLES

Install to a disk, wiping all existing data:

bootc install to-disk --wipe /dev/sda

Install with a specific root filesystem type:

bootc install to-disk --filesystem xfs /dev/nvme0n1

Install with TPM2 LUKS encryption:

bootc install to-disk --block-setup tpm2-luks /dev/sda

Install with custom kernel arguments:

bootc install to-disk --karg=nosmt --karg=console=ttyS0 /dev/sda

SEE ALSO

bootc(8), bootc-install(8), bootc-install-to-filesystem(8)

VERSION


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

NAME

bootc-install-to-filesystem - Install to an externally created filesystem structure

SYNOPSIS

bootc install to-filesystem [OPTIONS...] <ROOT_PATH>

DESCRIPTION

Install to an externally created filesystem structure.

In this variant of installation, the root filesystem alongside any necessary platform partitions (such as the EFI system partition) are prepared and mounted by an external tool or script. The root filesystem is currently expected to be empty by default.

OPTIONS

ROOT_PATH

Path to the mounted root filesystem

This argument is required.

--root-mount-spec=ROOT_MOUNT_SPEC

Source device specification for the root filesystem.  For example, `UUID=2e9f4241-229b-4202-8429-62d2302382e1`. If not provided, the UUID of the target filesystem will be used. This option is provided as some use cases might prefer to mount by a label instead via e.g. `LABEL=rootfs`

--boot-mount-spec=BOOT_MOUNT_SPEC

Mount specification for the /boot filesystem

--replace=REPLACE

Initialize the system in-place; at the moment, only one mode for this is implemented. In the future, it may also be supported to set up an explicit "dual boot" system

Possible values:
- wipe
- alongside

--acknowledge-destructive

If the target is the running system's root filesystem, this will skip any warnings

--skip-finalize

The default mode is to "finalize" the target filesystem by invoking `fstrim` and similar operations, and finally mounting it readonly.  This option skips those operations.  It is then the responsibility of the invoking code to perform those operations

--source-imgref=SOURCE_IMGREF

Install the system from an explicitly given source

--target-transport=TARGET_TRANSPORT

The transport; e.g. oci, oci-archive, containers-storage.  Defaults to `registry`

Default: registry

--target-imgref=TARGET_IMGREF

Specify the image to fetch for subsequent updates

--enforce-container-sigpolicy

This is the inverse of the previous `--target-no-signature-verification` (which is now a no-op).  Enabling this option enforces that `containers-policy.json` (see `man containers-policy.json` for the full search path) includes a default policy which requires signatures

--run-fetch-check

Verify the image can be fetched from the bootc image. Updates may fail when the installation host is authenticated with the registry but the pull secret is not in the bootc image

--skip-fetch-check

Verify the image can be fetched from the bootc image. Updates may fail when the installation host is authenticated with the registry but the pull secret is not in the bootc image

--disable-selinux

Disable SELinux in the target (installed) system

--karg=KARG

Add a kernel argument.  This option can be provided multiple times

--karg-delete=KARG_DELETE

Remove a kernel argument.  This option can be provided multiple times

--root-ssh-authorized-keys=ROOT_SSH_AUTHORIZED_KEYS

The path to an `authorized_keys` that will be injected into the `root` account

--generic-image

Perform configuration changes suitable for a "generic" disk image. At the moment:

--bound-images=BOUND_IMAGES

How should logically bound images be retrieved

Possible values:
- stored
- skip
- pull

Default: stored

--stateroot=STATEROOT

The stateroot name to use. Defaults to `default`

--bootupd-skip-boot-uuid

Don't pass --write-uuid to bootupd during bootloader installation

--bootloader=BOOTLOADER

The bootloader to use

Possible values:
- grub
- grub-cc
- systemd
- none

--composefs-backend

Use the composefs backend instead of ostree. This is the default for images with a UKI, and for images with /usr/lib/composefs/setup-root-conf.toml and no ostree prepare-root.conf

Default: false

--allow-missing-verity

Make fs-verity validation optional in case the filesystem doesn't support it (composefs backend only)

Default: false

--uki-addon=UKI_ADDON

Name of the UKI addons to install without the ".efi.addon" suffix. This option can be provided multiple times if multiple addons are to be installed (composefs backend only)

VERSION


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

NAME

bootc-install-to-existing-root - Install to the host root filesystem

SYNOPSIS

bootc install to-existing-root [OPTIONS...] [ROOT_PATH]

DESCRIPTION

Install to the host root filesystem.

This is a variant of install to-filesystem that is designed to install "alongside" the running host root filesystem. Currently, the host root filesystem's /boot partition will be wiped, but the content of the existing root will otherwise be retained, and will need to be cleaned up if desired when rebooted into the new root.

Managing configuration: before and after reboot

When using to-existing-root, there are two distinct scenarios for managing configuration files:

  1. Before rebooting: Injecting new configuration into the newly installed system
  2. After rebooting: Migrating configuration from the old system to the new system

Before reboot: Injecting new configuration

If you need to inject new configuration files (such as custom /etc/fstab entries, systemd mount units, or other configuration) into the newly installed system before rebooting, mount it with bootc-install-mount(8) and modify it through the mounted view. For example, to add a systemd mount unit:

mkdir /mnt/installed
bootc install mount --sysroot /target --latest /mnt/installed
vi /mnt/installed/etc/systemd/system/data.mount
umount -R /mnt/installed

Injecting kernel arguments for local state

A better approach for machine-local configuration like filesystem mounts is to inject kernel arguments during installation. Kernel arguments are ideal for local/machine-specific state in a bootc system.

For filesystem mounts, use systemd.mount-extra instead of /etc/fstab:

# Add a mount via kernel argument (preferred over /etc/fstab)
bootc install to-existing-root \
  --karg="systemd.mount-extra=UUID=<uuid>:/data:xfs:defaults"

The systemd.mount-extra syntax is: source:path:type:options

You can also inject other local kernel arguments for machine-specific configuration:

# Add console settings for serial access
bootc install to-existing-root --karg="console=ttyS0,115200"

# Add storage-specific options
bootc install to-existing-root --karg="rootflags=subvol=root"

This approach is cleaner than editing configuration files because kernel arguments are explicitly designed for local/machine-specific state in a bootc system.

Note: In the future, this functionality will be provided via a dedicated bootc API to make finding and modifying the deployment more straightforward.

After reboot: Migrating data from the old system

After rebooting into the new bootc system, the previous root filesystem data is accessible at /sysroot (the "physical root"). This allows you to migrate data from the old system to the new one.

Important: Any configuration data from /etc that you want to use in the new system must be manually copied from /sysroot/etc to /etc after rebooting. There is currently no automated mechanism for migrating this data.

For example, to migrate configuration after rebooting:

# After rebooting into the new system
# Copy network configuration from the old system
cp /sysroot/etc/sysconfig/network-scripts/ifcfg-eth0 /etc/sysconfig/network-scripts/

# Copy application configuration
cp -r /sysroot/etc/myapp /etc/

# Identify the old resolver policy, then migrate its authoritative
# network manager profiles or resolver configuration
stat -c '%F %N' /sysroot/etc/resolv.conf

# For user accounts, use proper tools
vipw  # Carefully review and merge users from /sysroot/etc/passwd

This applies to network configurations, user accounts, application settings, and other system configuration stored in /etc. Review files in /sysroot/etc and manually copy or merge what you need into /etc.

Note: A generated /etc/resolv.conf should not normally be copied or edited directly. It may be a dangling symlink after reboot or contain only a local resolver address such as 127.0.0.53, rather than the upstream DNS servers. Migrate the authoritative configuration, such as NetworkManager connection profiles or resolver drop-ins, into the configuration used by the new system. If both systems intentionally use a hand-managed static file, it may instead be selectively migrated. See DNS and /etc/resolv.conf for details.

Note: For filesystem mounts from /etc/fstab in the old system, consider using kernel arguments (via systemd.mount-extra) injected before reboot instead of migrating the fstab entries. See the "Injecting kernel arguments" section above.

OPTIONS

ROOT_PATH

Path to the mounted root; this is now not necessary to provide. Historically it was necessary to ensure the host rootfs was mounted at here via e.g. `-v /:/target`

--replace=REPLACE

Configure how existing data is treated

Possible values:
- wipe
- alongside

Default: alongside

--source-imgref=SOURCE_IMGREF

Install the system from an explicitly given source

--target-transport=TARGET_TRANSPORT

The transport; e.g. oci, oci-archive, containers-storage.  Defaults to `registry`

Default: registry

--target-imgref=TARGET_IMGREF

Specify the image to fetch for subsequent updates

--enforce-container-sigpolicy

This is the inverse of the previous `--target-no-signature-verification` (which is now a no-op).  Enabling this option enforces that `containers-policy.json` (see `man containers-policy.json` for the full search path) includes a default policy which requires signatures

--run-fetch-check

Verify the image can be fetched from the bootc image. Updates may fail when the installation host is authenticated with the registry but the pull secret is not in the bootc image

--skip-fetch-check

Verify the image can be fetched from the bootc image. Updates may fail when the installation host is authenticated with the registry but the pull secret is not in the bootc image

--disable-selinux

Disable SELinux in the target (installed) system

--karg=KARG

Add a kernel argument.  This option can be provided multiple times

--karg-delete=KARG_DELETE

Remove a kernel argument.  This option can be provided multiple times

--root-ssh-authorized-keys=ROOT_SSH_AUTHORIZED_KEYS

The path to an `authorized_keys` that will be injected into the `root` account

--generic-image

Perform configuration changes suitable for a "generic" disk image. At the moment:

--bound-images=BOUND_IMAGES

How should logically bound images be retrieved

Possible values:
- stored
- skip
- pull

Default: stored

--stateroot=STATEROOT

The stateroot name to use. Defaults to `default`

--bootupd-skip-boot-uuid

Don't pass --write-uuid to bootupd during bootloader installation

--bootloader=BOOTLOADER

The bootloader to use

Possible values:
- grub
- grub-cc
- systemd
- none

--acknowledge-destructive

Accept that this is a destructive action and skip a warning timer

--cleanup

Add the bootc-destructive-cleanup systemd service to delete files from the previous install on first boot

--composefs-backend

Use the composefs backend instead of ostree. This is the default for images with a UKI, and for images with /usr/lib/composefs/setup-root-conf.toml and no ostree prepare-root.conf

Default: false

--allow-missing-verity

Make fs-verity validation optional in case the filesystem doesn't support it (composefs backend only)

Default: false

--uki-addon=UKI_ADDON

Name of the UKI addons to install without the ".efi.addon" suffix. This option can be provided multiple times if multiple addons are to be installed (composefs backend only)

VERSION


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

NAME

bootc-install-mount - Mount an installed deployment into a caller-owned directory

SYNOPSIS

bootc install mount --sysroot=SYSROOT --latest [--read-only] TARGET

DESCRIPTION

Mount the deployment from an offline physical sysroot, such as one just created by bootc install to-filesystem, at TARGET. Both the OSTree and composefs backends are supported. TARGET is an absolute path to a directory; as with mount(8), whatever is there is hidden (bootc warns if it is not empty).

A deployment selector is required; currently the only one is --latest. For now the sysroot must also contain exactly one deployment, as it does right after installation; otherwise bootc refuses to guess which one to mount. Requiring the selector leaves room to choose among multiple deployments in the future without changing what existing invocations mean.

The mount remains in the caller's mount namespace after this command exits; bootc does not create a container, chroot, or private mount namespace.

The root follows what the initramfs does at boot. For composefs it is the deployment's image. For OSTree it is the deployment's composefs image (.ostree.cfs), mounted as ostree-prepare-root(1) does, unless prepare-root.conf sets composefs.enabled = no or the deployment has no image, in which case it is the deployment directory itself. Unlike ostree-prepare-root, bootc also uses the image when composefs.enabled is unset, does not consult kernel arguments, and for signed only requires fs-verity without checking the commit signature.

The deployment root and /usr are always read-only. The persistent /etc and /var are writable, unless --read-only is specified. The caller owns the resulting mount tree and should clean it up recursively with umount -R.

/etc and /var are set up the way the deployment's root setup will mount them at boot: for composefs as configured by bootc-setup-root-conf(5), and for OSTree following etc.transient in prepare-root.conf. A transient /etc is a fresh overlay of the image's /etc, so changes to it are discarded on unmount, just as they would be on reboot. A transient root (root.transient) is not applied; the root stays read-only.

/var is always the deployment's state directory on SYSROOT. bootc does not look in the image or its configuration (such as /etc/fstab or systemd mount units) for a separate /var filesystem, and does not consider kernel arguments such as systemd.volatile. If the installation puts /var on its own partition, mount that on top of TARGET/var yourself.

Plain chroot(1) into TARGET is not enough to run programs from the deployment: it sets up no /proc, /sys, /dev or /run. Use a tool that provides those, such as bwrap, podman run --rootfs, or systemd-nspawn(1).

OPTIONS

TARGET

Directory receiving the deployment mount

This argument is required.

--sysroot=SYSROOT

Offline target sysroot

--latest

Mount the latest deployment. Currently the sysroot must contain exactly one

--read-only

Mount /etc and /var read-only too. The deployment root is always read-only

EXAMPLES

Mount an installation and modify its persistent configuration and state:

mkdir /mnt/installed
bootc install mount --sysroot /mnt/sysroot --latest /mnt/installed
install -D -m 0644 hostname /mnt/installed/etc/hostname
umount -R /mnt/installed

SEE ALSO

bootc(8), bootc-install(8)

VERSION


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

NAME

bootc-install-finalize - Execute this as the penultimate step of an installation using install to-filesystem

SYNOPSIS

bootc install finalize [OPTIONS...] <ROOT_PATH>

DESCRIPTION

Execute this as the penultimate step of an installation using install to-filesystem

OPTIONS

ROOT_PATH

Path to the mounted root filesystem

This argument is required.

ARGUMENTS

<ROOT_PATH>

: Path to the mounted root filesystem

VERSION


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

NAME

bootc-install-ensure-completion - Intended for use in environments that are performing an ostree-based installation, not bootc

SYNOPSIS

bootc install ensure-completion [OPTIONS...]

DESCRIPTION

Intended for use in environments that are performing an ostree-based installation, not bootc.

In this scenario the installation may be missing bootc specific features such as kernel arguments, logically bound images and more. This command can be used to attempt to reconcile. At the current time, the only tested environment is Anaconda using ostreecontainer and it is recommended to avoid usage outside of that environment. Instead, ensure your code is using bootc install to-filesystem from the start.

VERSION


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

NAME

bootc-install-print-configuration - Output JSON to stdout that contains the merged installation configuration as it may be relevant to calling processes using install to-filesystem that in particular want to discover the desired root filesystem type from the container image

SYNOPSIS

bootc install print-configuration [OPTIONS...]

DESCRIPTION

Output JSON to stdout that contains the merged installation configuration as it may be relevant to calling processes using install to-filesystem that in particular want to discover the desired root filesystem type from the container image.

At the current time, the only output key is root-fs-type which is a string-valued filesystem name suitable for passing to mkfs.\$type.

OPTIONS

--all

Print all configuration

VERSION


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

NAME

system-reinstall-bootc - Reinstall the current system with a bootc image

SYNOPSIS

system-reinstall-bootc <BOOTC_IMAGE>

DESCRIPTION

system-reinstall-bootc is a utility that allows you to reinstall your current system using a bootc container image. This tool provides an interactive way to replace your existing system with a new bootc-based system while preserving SSH access and making the previous root filesystem (including user data) available in /sysroot.

The utility will:

  • Pull the specified bootc container image
  • Collect SSH keys for root access after reinstall
  • Execute a bootc install to replace the current system
  • Reboot into the new system

After reboot, the previous root filesystem will be available in /sysroot, and some automatic cleanup of the previous root will be performed. Note that existing mounts will not be automatically mounted by the bootc system unless they are defined in the bootc image.

This is primarily intended as a way to "take over" cloud virtual machine images, effectively using them as an installer environment.

ARGUMENTS

BOOTC_IMAGE

The bootc container image to install (e.g., quay.io/fedora/fedora-bootc:41)

This argument is required.

EXAMPLES

Reinstall with a custom bootc image:

system-reinstall-bootc registry.example.com/my-bootc:latest

ENVIRONMENT

BOOTC_REINSTALL_CONFIG

This variable is deprecated.

SEE ALSO

bootc(8), bootc-install(8)

VERSION


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

NAME

bootc-destructive-cleanup.service

DESCRIPTION

This systemd service runs on first boot after an "alongside" installation using bootc install to-existing-root --cleanup. Its purpose is to clean up files from the previous operating system.

The service runs as a oneshot unit and executes a distribution-specific cleanup script located at /usr/lib/bootc/fedora-bootc-destructive-cleanup (for Fedora derivatives).

How it works

  1. During bootc install to-existing-root --cleanup, a stamp file is created at /sysroot/etc/bootc-destructive-cleanup
  2. A systemd generator (bootc-systemd-generator) detects this stamp file at boot time and enables the bootc-destructive-cleanup.service unit
  3. The service runs the cleanup script on first boot

What the cleanup script does

On Fedora derivatives, the cleanup script performs the following actions:

  • Remounts /sysroot as read-write
  • Removes all RPM packages installed in the physical root (the previous OS)
  • Removes all container images from /sysroot/var/lib/containers using podman system prune --all -f

Note: The cleanup script does not remove stopped containers, so some storage may remain. This behavior may change in the future.

CUSTOMIZING THE CLEANUP SCRIPT

The current implementation ships a Fedora-specific cleanup script. Other distributions can provide their own cleanup script by creating an executable at /usr/lib/bootc/fedora-bootc-destructive-cleanup or by modifying the systemd unit file to reference a different path.

For an example implementation, see the Fedora cleanup script.

PREVIOUS FILESYSTEM DATA

After an alongside installation, the previous root filesystem data is accessible at /sysroot (the "physical root"). Previous mount points or subvolumes will not be automatically mounted in the new system; for example, a btrfs subvolume for /home will not be automatically mounted to /sysroot/home. These filesystems persist and can be handled manually or defined as mount points in the bootc image.

SEE ALSO

bootc(8), bootc-install-to-existing-root(8), system-reinstall-bootc(8)

VERSION


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

bootc is read-only when run in a default container

Currently, running e.g. podman run <someimage> bootc upgrade will not work. There are a variety of reasons for this, such as the basic fact that by default a docker|podman run <image> doesn't know where to update itself; the image reference is not exposed into the target image (for security/operational reasons).

Supported operations

There are only two supported operations in a container environment today:

  • bootc status: This can reliably be used to detect whether the system is actually booted via bootc or not.
  • bootc container lint: See man/bootc-container-lint.8.md.

Testing bootc in a container

Eventually we would like to support having bootc run inside a container environment primarily for testing purposes. For this, please see the tracking issue.


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

NAME

bootc-container - Operations which can be executed as part of a container build

SYNOPSIS

bootc container [OPTIONS...] <SUBCOMMAND>

DESCRIPTION

Operations which can be executed as part of a container build

SUBCOMMANDS

CommandDescription
bootc container inspectOutput information about the container image
bootc container lintPerform relatively inexpensive static analysis checks as part of a container build
bootc container compute-composefs-digestOutput the bootable composefs digest for a directory
bootc container split-kernel-and-rootfsSplit kernel and rootfs from a container image
bootc container ukifyBuild a Unified Kernel Image (UKI) using ukify

VERSION


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

NAME

bootc-container-inspect - Output JSON to stdout containing the container image metadata

SYNOPSIS

bootc container inspect

DESCRIPTION

Output JSON to stdout containing the container image metadata.

OUTPUT

The command outputs a JSON object with the following fields:

  • kargs: An array of kernel arguments embedded in the container image.
  • kernel: An object containing kernel information (or null if no kernel is found):
    • version: The kernel version identifier. For vmlinuz kernels, this is derived from the /usr/lib/modules/<version> directory name (equivalent to uname -r). For UKI images, this is the UKI filename without the .efi extension - which should usually be the same as the uname.
    • unified: A boolean indicating whether the kernel is packaged as a UKI (Unified Kernel Image).

OPTIONS

--rootfs=ROOTFS

Operate on the provided rootfs

Default: /

--json

Output in JSON format

--format=FORMAT

The output format

Possible values:
- humanreadable
- yaml
- json

EXAMPLES

Inspect container image metadata:

bootc container inspect

Example output (vmlinuz kernel):

{
  "kargs": [
    "console=ttyS0",
    "quiet"
  ],
  "kernel": {
    "version": "6.12.0-0.rc6.51.fc42.x86_64",
    "unified": false
  }
}

Example output (UKI):

{
  "kargs": [],
  "kernel": {
    "version": "7e11ac46e3e022053e7226a20104ac656bf72d1a",
    "unified": true
  }
}

SEE ALSO

bootc(8)

VERSION


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

NAME

bootc-container-split-kernel-and-rootfs - Split kernel and rootfs from a container image

SYNOPSIS

bootc container split-kernel-and-rootfs

DESCRIPTION

Split kernel and rootfs from a container image

OPTIONS

--rootfs=ROOTFS

Operate on the provided rootfs

Default: /

--output=OUTPUT

Output directory for the extracted kernel files

EXAMPLES

Extract kernel files from the current root filesystem to /kernel:

bootc container split-kernel-and-rootfs --output /kernel

This extracts the kernel and initramfs from the current root filesystem (/) and places them in /kernel/<kernel-version>/ with filenames vmlinuz and initramfs.img. Files that belong to the kernel binary are moved along with it under the same names, when present. Currently that is only the FIPS HMAC file (.vmlinuz.hmac) shipped by Fedora and RHEL derivatives. Other files in /usr/lib/modules/<kernel-version>/, such as config or System.map, stay in the rootfs.

Extract kernel files from a mounted container rootfs:

bootc container split-kernel-and-rootfs --rootfs /mnt/container-rootfs --output /output/kernels

This extracts kernel files from a container filesystem mounted at /mnt/container-rootfs and places them in the output directory.

Example output structure:

After running the command, the output directory will contain:

/output/kernels/
└── 6.5.0-15-generic/
    ├── .vmlinuz.hmac (Fedora/RHEL, if present)
    ├── vmlinuz
    └── initramfs.img

where 6.5.0-15-generic is the detected kernel version from the source rootfs.

SEE ALSO

bootc(8)

VERSION


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

NAME

bootc-container-ukify - Build a Unified Kernel Image (UKI) using ukify

SYNOPSIS

bootc container ukify [OPTIONS] [-- UKIFY_ARGS...]

DESCRIPTION

Build a Unified Kernel Image (UKI) using ukify

This command computes the necessary arguments from the container image (kernel, initrd, cmdline, os-release) and invokes ukify with them. Any additional arguments after -- are passed through to ukify unchanged.

Unless --erofs-version=v2 is specified, the generated UKI contains the V1 composefs argument followed by a V2 fallback. The command does not inspect the initramfs to choose a format. Explicit V2 emits only the legacy V2 argument. When using the default output, the initramfs in the image must be regenerated with a bootc version that supports V1 before the UKI is built.

OPTIONS

ARGS

Additional arguments to pass to ukify (after `--`)

--rootfs=ROOTFS

Operate on the provided rootfs

Default: /

--allow-missing-verity

Make fs-verity validation optional in case the filesystem doesn't support it

--erofs-version=EROFS_VERSION

EROFS format version to use when computing the composefs digest

Possible values:
- v1
- v2

--write-dumpfile-to=WRITE_DUMPFILE_TO

Write a dumpfile to this path

--kernel-dir=KERNEL_DIR

The directory containing the kernel and initramfs.img Must be of the format /parent/$kernel_version

EXAMPLES

bootc container ukify --rootfs /target -- --output /output/uki.efi

SEE ALSO

bootc(8), ukify(1)

VERSION


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

NAME

bootc-container-compute-composefs-digest - Output the bootable composefs digest for a directory

SYNOPSIS

bootc container compute-composefs-digest [OPTIONS] [PATH]

DESCRIPTION

Output the bootable composefs digest for a directory

This is the digest that bootc container ukify embeds in the kernel command line of a UKI. It is a 128-character SHA-512 hex string that identifies the filesystem contents. Most image builds should use bootc-container-ukify(8), which computes it internally; this command is useful for scripting and debugging outside of that flow.

The digest depends on the EROFS format; see --erofs-version. It must be run against a separate mount of the root filesystem, not the running root.

OPTIONS

PATH

Path to the filesystem root

--write-dumpfile-to=WRITE_DUMPFILE_TO

Additionally generate a dumpfile for the preferred digest, written to the target path

--erofs-version=EROFS_VERSION

EROFS format version to use when computing the composefs digest

Possible values:
- v1
- v2

Default: v1

EXAMPLES

Compute the digest of a root filesystem mounted at /target:

bootc container compute-composefs-digest /target

Also write a composefs dumpfile, to compare against another build:

bootc container compute-composefs-digest --write-dumpfile-to /tmp/rootfs.dump /target

SEE ALSO

bootc(8), bootc-container-ukify(8)

VERSION


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

NAME

bootc-container-lint - Perform relatively inexpensive static analysis checks as part of a container build

SYNOPSIS

bootc container lint [OPTIONS...]

DESCRIPTION

Perform relatively inexpensive static analysis checks as part of a container build.

This is intended to be invoked via e.g. RUN bootc container lint as part of a build process; it will error if any problems are detected.

OPTIONS

--rootfs=ROOTFS

Operate on the provided rootfs

Default: /

--fatal-warnings

Make warnings fatal

--list

Instead of executing the lints, just print all available lints. At the current time, this will output in YAML format because it's reasonably human friendly. However, there is no commitment to maintaining this exact format; do not parse it via code or scripts

--skip=SKIP

Skip checking the targeted lints, by name. Use `--list` to discover the set of available lints

--no-truncate

Don't truncate the output. By default, only a limited number of entries are shown for each lint, followed by a count of remaining entries

VERSION


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

"bootc compatible" images

It is a toplevel goal of this project to tightly integrate with the OCI ecosystem and make booting containers a normal activity.

However, there are a number of basic requirements and integration points, some of which have distribution-specific variants.

Generic requirements (composefs or ostree backends)

/sysroot

Your container image must have a /sysroot directory - this is where the "physical root" will be mounted. The permissions (mode) should generally be the same as /usr i.e. 0755 or similar.

LABEL containers.bootc=1

The rationale for this required label is that many higher level tools which expect to operate only on bootc-compatible OCI images will want to be able to present only compatible images.

Kernel (split)

The Linux kernel (and optionally initramfs) is embedded in the container image; the canonical location is /usr/lib/modules/$kver/vmlinuz, and the initramfs should be in initramfs.img in that directory. You should not include any content in /boot in your container image. Bootc will take care of copying the kernel/initramfs as needed from the container image to /boot.

Kernel (sealed UKI)

For the composefs backend, the UKI must be located at /boot/EFI/Linux/$kver.efi. See sealed images.

bootc container lint requires a /ostree -> sysroot/ostree symlink, unless the image is composefs-native, which is signaled by the presence of /usr/lib/composefs/setup-root-conf.toml (it may be empty); see bootc-setup-root-conf.toml(5). Such images also don't need ostree's prepare-root.conf to enable composefs.

composefs backend

There are no strict additional basic filesystem/layout requirements for images which plan to deploy with composefs. However, see also bootloaders.

ostree backend

prepare-root.conf

The upstream ostree builds today do not default to composefs. You must enable this via a prepare-root.conf:

[composefs]
enabled = true

This is checked by bootc container lint.

Historical usage of /ostree

Some images include a /ostree directory. A requirement for this was dropped in bootc 1.1.3, and it is recommended that new images do not include it.

Suggested image content

The bootc project provides a baseimage reference set of configuration files for base images. In particular at the current time the content defined by base must be used (or recreated). There is also suggested integration there with e.g. dracut to ensure the initramfs is set up, etc.

The bootc container lint command will check this.

SELinux

The default mechanism for labeling today is that bootc will load the file contexts from the image (e.g. /etc/selinux/policy) and apply labels dynamically. This is the only mechanism that will work today with a generic bootc-unaware build tooling.

It is not supported to add security.selinux extended attributes into the OCI tar layers, though support for this may be added if requested.

More details

Container runtimes such as podman and docker commonly apply a "coarse" SELinux policy to running containers. See container-selinux. It is very important to understand that non-bootc base images do not (usually) have any embedded security.selinux metadata at all; all labels on the toplevel container image are dynamically generated per container invocation, and there are no individually distinct e.g. etc_t and usr_t types.

/ostree

Only with the ostree backend, there is support for including the ostree commit metadata in the OCI image, which includes all xattrs. File content in derived layers will be labeled using the default file contexts (from /etc/selinux). For example, you can do this (as of bootc 1.1.0):

RUN semanage fcontext -a -t httpd_sys_content_t "/web(/.*)?"

(This command will write to /etc/selinux/$policy/policy/.)

It will currently not work to do e.g.:

RUN chcon -t foo_t /usr/bin/foo

Because the container runtime state will deny the attempt to "physically" set the security.selinux extended attribute.


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

Filesystem

As noted in other chapters, the bootc project currently depends on the ostree project for storing the base container image. Additionally there is a containers/storage instance for logically bound images.

However, bootc is intending to be a "fresh, new container-native interface", and ostree is an implementation detail.

First, it is strongly recommended that bootc consumers use the ostree composefs backend; to do this, ensure that you have a /usr/lib/ostree/prepare-root.conf that contains at least

[composefs]
enabled = true

This will ensure that the entire / is a read-only filesystem which is very important for achieving correct semantics.

Understanding container build/runtime vs deployment

When run as a container (e.g. as part of a container build), the filesystem is fully mutable in order to allow derivation to work. For more on container builds, see build guidance.

The rest of this document describes the state of the system when "deployed" to a physical or virtual machine, and managed by bootc.

Timestamps

bootc uses ostree, which currently squashes all timestamps to zero. This is now viewed as an implementation bug and will be changed in the future. For more information, see this tracker issue.

Understanding physical vs logical root with /sysroot

When the system is fully booted, it is into the equivalent of a chroot. The "physical" host root filesystem will be mounted at /sysroot. For more on this, see filesystem: sysroot.

This chroot filesystem is called a "deployment root". All the remaining filesystem paths below are part of a deployment root which is used as a final target for the system boot. The target deployment is determined via the ostree= kernel commandline argument.

/usr

The overall recommendation is to keep all operating system content in /usr, with directories such as /bin being symbolic links to /usr/bin, etc. See UsrMove for example.

However, with composefs enabled /usr is not different from /; they are part of the same immutable image. So there is not a fundamental need to do a full "UsrMove" with a bootc system.

/usr/local

The OSTree upstream recommendation suggests making /usr/local a symbolic link to /var/usrlocal. But because the emphasis of a bootc-oriented system is on users deriving custom container images as the default entrypoint, it is recommended here that base images configure /usr/local be a regular directory (i.e. the default).

Projects that want to produce "final" images that are themselves not intended to be derived from in general can enable that symbolic link in derived builds.

/etc

The /etc directory contains mutable persistent state by default; however, it is supported (and encouraged) to enable the etc.transient config option, see below as well.

When in persistent mode, it inherits the OSTree semantics of performing a 3-way merge across upgrades. In a nutshell:

  • The new default /etc is used as a base
  • The diff between current and previous /etc is applied to the new /etc
  • Locally modified files in /etc different from the default /usr/etc (of the same deployment) will be retained

You can view the state via ostree admin config-diff. Note that the "diff" here includes metadata (uid, gid, extended attributes), so changing any of those will also mean that updated files from the image are not applied.

The implementation of this defaults to being executed by ostree-finalize-staged.service at shutdown time, before the new bootloader entry is created.

The rationale for this design is that in practice today, many components of a Linux system end up shipping default configuration files in /etc. And even if the default package doesn't, often the software only looks for config files there by default.

Some other image-based update systems do not have distinct "versions" of /etc and it may be populated only set up at install time, and untouched thereafter. But that creates "hysteresis" where the state of the system's /etc is strongly influenced by the initial image version. This can lead to problems where e.g. a change to /etc/sudoers (to give one simple example) would require external intervention to apply.

For more on configuration file best practices, see Building.

To emphasize again, it's recommended to enable etc.transient if possible, though when using that you may need to store some machine-specific state in e.g. the kernel commandline if applicable.

/usr/etc

The /usr/etc tree is generated client side and contains the default container image's view of /etc. This should generally be considered an internal implementation detail of bootc/ostree. Do not explicitly put files into this location, it can create undefined behavior. There is a check for this in bootc container lint.

systemd-confext and systemd-sysext

systemd-confext and systemd-sysext are not currently supported on bootc-managed systems.

These tools may be present in the operating system image (for example, because they ship in the systemd package), but bootc has not validated them on deployed systems. Using them on a bootc host is not recommended. There are two categories of concern to be aware of.

Conflicts with transient overlays

confext and sysext apply their content by stacking overlayfs mounts. This can conflict with the non-default use of transient overlays for / and /etc. bootc systems commonly use composefs for the root filesystem, and may also enable transient root and/or transient /etc, each of which also uses overlayfs stacking. The Linux kernel limits filesystem stacking depth (FILESYSTEM_MAX_STACK_DEPTH = 2), so combining these features can exhaust the available depth and cause confext or sysext overlays to fail.

The /etc case in particular is expected to become tractable: configuring the composefs backend with etc = none (there is not yet a corresponding ostree knob) leaves /etc free for confext to manage.

OS-specific integration concerns

Beyond the bootc-specific overlay concerns above, there may be additional integration issues (for example, SELinux labeling) that are specific to a particular OS or distribution. Consult your OS/distribution documentation for guidance on confext and sysext.

For per-host configuration, use the patterns described in Building images: Configuration instead: image-embedded configuration (prefer /usr where possible), persistent /etc with day-2 configuration management tools, or machine-local kernel arguments via rpm-ostree kargs or /usr/lib/bootc/kargs.d.

For more on the design rationale, see Relationship with systemd "particles".

/var

Content in /var persists by default; it is however supported to make it or subdirectories mount points (whether network or tmpfs). There is exactly one /var. If it is not a distinct partition, then it is automatically made a bind from /ostree/deploy/$stateroot/var and shared across "deployments" (bootloader entries).

You may include content in /var in your image - and reference base images may have a few basic directories such as /var/tmp (in order to ease use in container builds).

However, it is very important to understand that content included in /var in the container image acts like a Docker VOLUME /var. This means its contents are unpacked only from the initial image - subsequent changes to /var in a container image are not automatically applied.

A common case is for applications to want some directory structure (e.g. /var/lib/postgresql) to be pre-created. It's recommended to use systemd tmpfiles.d for this. An even better approach where applicable is StateDirectory= in units.

As of bootc 1.1.6, the bootc container lint command will check for missing tmpfiles.d entries and warn.

Note this is very different from the handling of /etc. The rationale for this is that /etc is relatively small configuration files, and the expected configuration files are often bound to the operating system binaries in /usr.

But /var has arbitrarily large data (system logs, databases, etc.). It would also not be expected to be rolled back if the operating system state is rolled back. A simple example is that an apt|dnf downgrade postgresql should not affect the physical database in general in /var/lib/postgres. Similarly, a bootc update or rollback should not affect this application data.

Having /var separate also makes it work cleanly to "stage" new operating system updates before applying them (they're downloaded and ready, but only take effect on reboot).

In general, this is the same rationale for Docker VOLUME: decouple the application code from its data.

Other directories

It is not supported to ship content in /run or /proc or other API Filesystems in container images.

Besides those, for other toplevel directories such as /usr /opt, they will be lifecycled with the container image.

/opt

In the default suggested model of using composefs (per above) the /opt directory will be read-only, alongside other toplevels such as /usr.

Some software (especially "3rd party" deb/rpm packages) expect to be able to write to a subdirectory of /opt such as /opt/examplepkg.

See building images for recommendations on how to build container images and adjust the filesystem for cases like this.

However, for some use cases, it may be easier to allow some level of mutability. There are two options for this, each with separate trade-offs: transient roots and state overlays.

Other toplevel directories

Creating other toplevel directories and content (e.g. /afs, /arbitrarymountpoint) or in general further nested data is supported - just create the directory as part of your container image build process (e.g. RUN mkdir /arbitrarymountpoint). These directories will be lifecycled with the container image state, and appear immutable by default, the same as all other directories such as /usr and /opt.

Mounting separate filesystems there can be done by the usual mechanisms of /etc/fstab, systemd .mount units, etc.

SELinux for arbitrary toplevels

Note that operating systems using SELinux may use a label such as default_t for unknown toplevel directories, which may not be accessible by some processes. In this situation you currently may need to also ensure a label is defined for them in the file contexts.

Enabling transient root

This feature enables a fully transient writable rootfs by default. To do this, set the

[root]
transient = true

option in /usr/lib/ostree/prepare-root.conf. In particular this will allow software to write (transiently, i.e. until the next reboot) to all top-level directories, including /usr and /opt, with symlinks to /var for content that should persist.

This can be combined with etc.transient as well (below).

More on prepare-root: https://ostreedev.github.io/ostree/man/ostree-prepare-root.html

Note that regenerating the initramfs is required when changing this file.

Dynamic mountpoints with transient-ro

The transient-ro option allows privileged users to create dynamic toplevel mountpoints at runtime while keeping the filesystem read-only by default. This is particularly useful for applications that need to bind mount host paths that may be platform-specific or dynamic.

Use cases

This feature addresses scenarios where:

  • Applications need to bind mount host directories that match the host's absolute paths
  • Platform-specific mountpoints are required (e.g., /Users on macOS)
  • Dynamic mountpoints need to be created after deployment but before application startup
  • The filesystem should remain read-only for regular processes

Configuration

To enable this feature, add the following to /usr/lib/ostree/prepare-root.conf:

[root]
transient-ro = true

How it works

When transient-ro=true is set:

  1. The overlayfs upper directory is mounted read-only by default
  2. Privileged processes can remount it as writable only in a new mount namespace, and perform arbitrary changes there, such as creating new toplevel mountpoints
  3. These mountpoints persist for the current boot but do not survive reboots or upgrades
  4. Regular processes continue to see a read-only filesystem

A privileged process can achieve this using standard Linux commands. For example:

# unshare -m -- /bin/sh -c 'mount -o remount,rw / && mkdir /new-mountpoint'

Example: Podman machine integration

A common use case is with podman machine on macOS, where the VM needs to bind mount host paths like /Users/username into the VM. With transient-ro, the system can:

  1. Create the /Users directory dynamically at runtime
  2. Bind mount the host's /Users directory to the VM's /Users
  3. Keep the rest of the filesystem read-only for security

Enabling transient etc

The default (per above) is to have /etc persist. If however you do not need to use it for any per-machine state, then enabling a transient /etc is a great way to reduce the amount of possible state drift. Set the

[etc]
transient = true

option in /usr/lib/ostree/prepare-root.conf.

This can be combined with root.transient as well (above).

More on prepare-root: https://ostreedev.github.io/ostree/man/ostree-prepare-root.html

Note that regenerating the initramfs is required when changing this file.

Enabling state overlays

This feature enables a writable overlay on top of /opt (or really, any toplevel or subdirectory baked into the image that is normally read-only).

The semantics here are somewhat nuanced:

  • Changes persist across reboots by default
  • During updates, new files from the container image override any locally modified version

The advantages are:

  • It makes it very easy to make compatible applications that install into /opt.
  • In contrast to transient root (above), a smaller surface of the filesystem is mutable.

The disadvantages are:

  • There is no equivalent to this feature in the Docker/Podman ecosystem.
  • It allows for some temporary state drift until the next update.

To enable this feature, instantiate the ostree-state-overlay@.service unit template on the target path. For example, for /opt:

RUN systemctl enable ostree-state-overlay@opt.service

More generally dealing with /opt

Both transient root and state overlays above provide ways for packages that install in /opt to operate. However, for maximum immutability the best approach is simply to symlink just the parts of the /opt needed into /var. See the section on /opt in Image building and configuration guidance for a more concrete example.

Increased filesystem integrity with fsverity

The bootc project uses composefs by default for the root filesystem (using ostree's support for composefs). However, the default configuration as recommended for base images uses composefs in a mode that does not require signatures or fsverity.

bootc supports with ostree's model of hard requiring fsverity for underlying objects. Enabling this also causes bootc to error out at install time if the target filesystem does not enable fsverity.

To enable this, inside your container build update /usr/lib/ostree/prepare-root.conf with:

[composefs]
enabled = verity

At the current time, there is no default recommended mechanism to check the integrity of the upper composefs. For more information about this, see this tracking issue.

Note that the default /etc and /var mounts are unaffected by this configuration. Because /etc in particular can easily contain arbitrary executable code (/etc/systemd/system unit files), many deployment scenarios that want to hard require fsverity will also want a "transient etc" model.

Caveats

Does not apply to logically bound images

The logically bound images store is currently implemented using a separate mechanism and configuring fsverity for the bootc storage has no effect on it.

Enabling fsverity across upgrades

At the current time the integration is only for installation; there is not yet support for automatically ensuring that fsverity is enabled when upgrading from a state with composefs.enabled = yes to composefs.enabled = verity. Because older objects may not have fsverity enabled, the new system will likely fail at runtime to access these older files across the upgrade.


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

Filesystem: Physical /sysroot

By default, bootc uses ostree as a backend, and maps fetched container images to a deployment. The layout of the composefs backend is described below.

stateroot

The underlying ostree CLI and API tooling expose a concept of stateroot, which is not yet exposed via bootc. The stateroot used by bootc install is just named default.

The stateroot concept allows having fully separate parallel operating system installations with fully separate /etc and /var, while still sharing an underlying root filesystem.

In the future, this functionality will be exposed and used by bootc.

/sysroot mount

When booted, the physical root will be available at /sysroot as a read-only mount point and the logical root / will be a bind mount pointing to a deployment directory under /sysroot/ostree. This is a key aspect of how bootc upgrade operates: it fetches the updated container image and writes the base image files (using OSTree storage to /sysroot/ostree/repo).

Beyond that and debugging/introspection, there are few use cases for tooling to operate on the physical root.

bootc-owned container storage

For logically bound images, bootc maintains a dedicated containers/storage instance using the overlay backend (the same type of thing that backs /var/lib/containers).

This storage is accessible via a /usr/lib/bootc/storage symbolic link which points into /sysroot. (Avoid directly referencing the /sysroot target)

At the current time, this storage is not used for the base bootable image. This unified storage issue tracks unification.

Expanding the root filesystem

One notable use case that does need to operate on /sysroot is expanding the root filesystem.

Some higher level tools such as e.g. cloud-init may (reasonably) expect the / mount point to be the physical root. Tools like this will need to be adjusted to instead detect this and operate on /sysroot.

Growing the block device

Fundamentally bootc is agnostic to the underlying block device setup. How to grow the root block device depends on the underlying storage stack, from basic partitions to LVM. However, a common tool is the growpart utility from cloud-init.

Growing the filesystem

The systemd project ships a systemd-growfs tool and corresponding systemd-growfs@ services. This is a relatively thin abstraction over detecting the target root filesystem type and running the underlying tool such as xfs_growfs.

At the current time, most Linux filesystems require the target to be mounted writable in order to grow. Hence, an invocation of system-growfs /sysroot or xfs_growfs /sysroot will need to be further wrapped in a temporary mount namespace.

Using a MountFlags=slave drop-in stanza for systemd-growfs@sysroot.service is recommended, along with an ExecStartPre=mount -o remount,rw /sysroot.

Detecting bootc/ostree systems

See the package managers section on "Detecting image based systems".

composefs backend storage

Unlike the ostree backend, which keeps its repository at /ostree/repo, the composefs backend splits its on-disk state across two top-level directories in the physical sysroot:

  • /composefs: The composefs-rs repository (mode 0700), containing:
    • objects/: content-addressed file storage, keyed by SHA-512 fs-verity digest and shared via reflink (FICLONE) where the filesystem supports it
    • images/: EROFS images describing each deployment's root filesystem metadata, possibly in both formats
    • streams/: OCI manifest, config, and layer splitstreams captured during image pulls
    • bootc/storage/: the containers-storage: instance backing logically bound images, reflink-shared with the composefs object store
  • /state/deploy/<deployment-id>/: Persistent per-deployment state, one directory per deployment (see below for how it is named):
    • etc/: a writable copy of the deployment's /etc, bind-mounted onto the booted root's /etc
    • var: a symlink to the shared /state/os/default/var, bind-mounted onto the booted root's /var
    • <deployment-id>.origin: an INI file recording the image reference, boot type (BLS or UKI) and digest, and the OCI manifest digest (the latter is what keeps a deployment's objects alive across garbage collection)

Although composefs-rs supports other fs-verity hash algorithms, bootc currently hardcodes SHA-512 for the repository. This is why EROFS image IDs and object identifiers are 128-character hex strings.

Three kinds of digest show up here and are easy to confuse. The OCI manifest digest names the pulled container image (see the origin file above). An EROFS digest names one bootable image under images/ and is what the kernel command line refers to; a deployment may have one of each format. The deployment ID names the state directory; it is the digest of the boot image selected when the deployment was staged.

There is no /ostree/repo; the composefs backend doesn't use the ostree repository at all. A minimal /ostree directory is still created, but only to hold a compatibility symlink (ostree/bootc -> ../composefs/bootc) so that existing tooling expecting /usr/lib/bootc/storage to resolve through ostree/bootc keeps working.

Transient, not-yet-finalized deployment state (used while staging an update before reboot) lives under /run/composefs/staged-deployment and is never persisted to disk.


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

Container storage

The bootc project uses ostree and specifically the ostree-rs-ext Rust library which handles storage of container images on top of an ostree-based system for the booted host, and additionally there is a containers/storage instance for logically bound images.

Architecture

flowchart TD
    bootc --- ostree-rs-ext --- ostree-rs --- ostree
    ostree-rs-ext --- containers-image-proxy-rs --- skopeo --- containers/image
    bootc --- podman --- image-storage["containers/{image,storage}"]

There were two high level goals that drove the design of the current system architecture:

  • Support seamless in-place migrations from existing ostree systems
  • Avoid requiring deep changes to the podman stack

A simple way to explain the current architecture is that podman uses two Go libraries:

Whereas ostree uses a custom container storage, not containers/storage.

Mapping container images to ostree

OCI images are effectively just a standardized format of tarballs wrapped with JSON - specifically "layers" of tarballs.

The ostree-rs-ext project maps layers to OSTree commits. Each layer is stored separately, under an ostree "ref" (like a git branch) under the ostree/container/ namespace:

$ ostree refs ostree/container

Layers

The ostree/container/blob namespace tracks storage of a container layer identified by its blob ID (sha256 digest).

Images

At the current time, ostree always boots into a "flattened" filesystem tree. This is generated as both a hardlinked checkout as well as a composefs image.

The flattened tree is constructed and committed into the ostree/container/image namespace. The commit metadata also includes the OCI manifest and config objects.

This is implemented in the ostree-rs-ext/container module.

SELinux labeling

See the SELinux section of Image layout.

Origin files

ostree has the concept of an origin file which defines the source of truth for upgrades. The container image reference for each deployment is included in its origin.

Booting

A core aspect of this entire design is that once a container image is fetched into the ostree storage, from there on it just appears as an "ostree commit", and so all code built on top can work with it.

For example, the ostree-prepare-root.service which runs in the initramfs is currently agnostic to whether the filesystem tree originated from an OCI image or some other mechanism; it just targets a prepared flattened filesystem tree.

This is what is referenced by the ostree= kernel commandline.

Logically bound images

In addition to the base image, bootc supports logically bound images.


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

composefs backend

bootc has two storage backends. The default is ostree, and the composefs backend uses composefs-rs instead of ostree to store and manage deployments. Both are supported and covered by the project's stability guarantees. In particular, the project is committed to upgrading every composefs system installed since bootc 1.16.0 in place.

The composefs backend is required for sealed images, and the image determines which backend bootc install uses; see Understanding bootc install. Its on-disk layout is described in Filesystem: sysroot.

Overview

The composefs backend has two independent integrity controls:

  • fs-verity enforcement. By default every object in the composefs repository must have fs-verity enabled, and the root filesystem is only mounted if its digest matches the one on the kernel command line. Building a UKI with --allow-missing-verity adds a ? marker to that argument, which makes fs-verity optional (for filesystems such as XFS that lack it). Both UKI and traditional kernel/initramfs installs can enforce fs-verity.
  • Boot authentication. In a sealed deployment fs-verity is enforced and the expected root digest is embedded in a UKI signed for Secure Boot, so firmware authenticates the digest and the digest authenticates the root filesystem. A BLS entry or an unsigned UKI still has fs-verity checked at mount time, but nothing authenticates the digest itself.

EROFS formats

composefs-rs can encode the EROFS image for a root filesystem in two formats, which produce different digests for the same content:

  • V1 is compatible with the C composefs tools and is the default for new repositories.
  • V2 is the older composefs-rs format, kept as a fallback.

The composefs.digest= kernel argument names the format along with the digest, for example composefs.digest=v1-sha512-12:<digest>, so it is unambiguous. The older bare composefs=<digest> argument is not: released UKIs carry either a V1 or a V2 digest there (see below), so bootc tries both.

By default bootc container ukify computes both digests and writes the V1 composefs.digest= argument followed by a bare composefs= argument with the V2 digest, for older clients. --erofs-version=v2 writes only the bare V2 argument.

Each argument names one exact image. Staging fails unless every digest in the UKI matches an image bootc generated for that container image. At boot, bootc's initramfs tries the arguments in order and moves on to the next one if an image is missing, but an image that fails fs-verity checks stops the boot. bootc never substitutes a different digest.

Existing repositories keep the format configuration recorded in their metadata; opening one with a newer bootc doesn't convert it.

The bare composefs= argument

Released UKIs have used the bare composefs=<digest> argument for different formats:

  • bootc 1.16.0 through 1.16.2 predate format versioning and use the original composefs-rs encoding that V2 descends from.
  • bootc 1.16.3 writes a V2 digest.
  • bootc 1.16.4 through 1.16.13 write a V1 digest: the switch to the V1 format shipped before the composefs.digest= argument did.
  • Releases after 1.16.13 write V2 there again, after an explicit V1 argument.

So bootc accepts a bare composefs= digest that matches either a V1 or a V2 image, and only enforces the format for composefs.digest=.

Upgrading from bootc 1.16

When you update bootc in an image, regenerate the initramfs before generating the UKI. The initramfs contains bootc's own mount logic, and keeping an old initramfs with a newer bootc is not supported.

For a sealed deployment, sign the new UKI with a key the existing machine trusts. If the deployment was built with --allow-missing-verity, keep that flag. Then publish the image and run bootc upgrade as usual.

What happens next depends on the bootc version doing the staging. A client that only understands composefs=, such as 1.16.0, stages the V2 fallback; the new initramfs boots it, and the next upgrade (now staged by the new bootc) moves the system to V1. bootc 1.16.4 and later already understand composefs.digest= and stage V1 directly.

The 1.16.0 path is covered by the test-49-composefs-1-16-bridge TMT test for both sealed and --allow-missing-verity UKIs, including rollback and garbage collection. Upgrades of UKI installs from other releases are not yet tested.

Supported configurations

The following are supported with the composefs backend:

  • bootc install, upgrade, switch, rollback, status, usr-overlay and soft reboots.
  • bootc install mount, for changing an installed deployment before its first boot.
  • Traditional kernel and initramfs installs booted through BLS entries, with either GRUB (via bootupd) or systemd-boot, and UKIs booted with systemd-boot, including sealed UKIs signed for Secure Boot. See Bootloaders.
  • Root filesystems with fs-verity support, and filesystems without it (such as XFS) with fs-verity made optional. Sealed images require fs-verity. CI covers ext4 and XFS; btrfs is expected to work but is not tested.
  • The EROFS formats and kernel arguments described above.
  • The image build commands bootc container ukify, bootc container split-kernel-and-rootfs and bootc container compute-composefs-digest, and the initramfs setup configured by setup-root-conf.toml.

On CentOS Stream 9, only sealed UKIs are tested; traditional kernel installs require newer dracut and systemd features. Its dracut also doesn't install setup-root-conf.toml into the initramfs automatically.

Experimental parts

These remain experimental and may change or be removed:

  • The grub-cc bootloader (--bootloader=grub-cc).
  • UKI addons (--uki-addon). Addons are only installed by bootc install: they aren't updated on upgrade, garbage collected, or reverted on rollback.
  • Unified storage.

Limitations

  • bootc edit and bootc install reset are not yet implemented for the composefs backend.
  • There is no bootc-boot-complete.service and no boot counting; see boot failure detection.
  • There is no in-place transition from an ostree system to the composefs backend yet, so for now a system has to be reinstalled. We fully intend to support moving to composefs without a reinstall; see Future work.
  • --bootloader=none is not supported.
  • --soft-reboot=auto doesn't fall back to a regular reboot when the new deployment can't be soft rebooted into; see Soft reboots.
  • Only a single ESP is used (the first one found), and a separate XBOOTLDR partition is not supported.
  • Rollback with GRUB and UKIs assumes exactly two deployments.
  • bootc internals fsck doesn't yet comprehensively cover the composefs backend; see #2497.

For building and testing bootc itself with the composefs backend, see CONTRIBUTING.md.

Known issues

The composefs backend is stable; upgrades and core functionality is supported. These are some of the more prominent known issues.

Future work

Additional Resources


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

NAME

bootc-root-setup.service

DESCRIPTION

A oneshot systemd service that runs in the initramfs to set up the root filesystem when the composefs backend is active. It is gated on the composefs= kernel command line argument and on ConditionPathExists=/etc/initrd-release, so it only runs inside an initramfs.

The service is ordered after sysroot.mount and before initrd-root-fs.target. It performs the following steps:

  1. Opens the composefs repository at /sysroot/composefs.
  2. Mounts the EROFS image identified by the composefs=<digest> kernel argument, with fs-verity verification.
  3. Optionally wraps the root in a transient tmpfs overlay (see root.transient in bootc-setup-root-conf.toml(5)).
  4. Bind-mounts or overlays /etc and /var from the per-deployment state directory at /sysroot/state/deploy/<digest>/.
  5. Replaces /sysroot with the fully assembled root, ready for switch-root.

CONFIGURATION

Behaviour is controlled by an optional TOML file installed into the initramfs:

/usr/lib/composefs/setup-root-conf.toml

See bootc-setup-root-conf.toml(5) for the full option reference.

INSTALLATION

The service and its binary (/usr/lib/bootc/initramfs-setup) are installed into the initramfs by the 51bootc dracut module. The module also installs /usr/lib/composefs/setup-root-conf.toml when it is present on the host image, so image authors do not need manual dracut --include invocations.

The 51bootc module is not enabled by default (so that e.g. apt|dnf install bootc don't pull it in). It's recommended for base images to enable it via a config file in e.g. /usr/lib/dracut/dracut.conf.d.

SEE ALSO

bootc-setup-root-conf.toml(5), bootc(8)

VERSION


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

NAME

bootc-setup-root-conf.toml

SYNOPSIS

/usr/lib/composefs/setup-root-conf.toml

DESCRIPTION

When the composefs backend is active, bootc-root-setup.service runs in the initramfs to mount the root filesystem before switch-root. It reads this optional TOML configuration file to control how /, /etc, and /var are mounted.

If the file does not exist all options take their documented defaults.

The presence of this file (even if empty) also marks the image as composefs-native, which decides the storage backend bootc install uses: if the image also has no ostree prepare-root.conf, it is installed with the composefs backend, and otherwise with ostree (see bootc-installation(7)). bootc container lint then also no longer requires the /ostree symlink or the ostree prepare-root.conf composefs configuration used by the ostree backend.

The 51bootc dracut module installs this file into the initramfs automatically when it is present on the host image. Image authors can therefore ship the file at this path in their container image and rebuild the initramfs with a plain dracut --force; no --include flags are needed.

SECTIONS

[root]

Controls the mount of the root (/) filesystem.

transient (boolean, default: false) If true, the composefs root is wrapped in a tmpfs overlay before switch-root. All writes to / are discarded at the next reboot. This is useful for kiosk or lab systems where runtime modifications must never persist.

[etc]

Controls how /etc is mounted from the deployment state directory.

mount (string) One of "none", "bind" (default), "overlay", or "transient".

  • none (alias "root") — /etc is not remounted; the composefs image's /etc is used directly and is read-only. The system sees exactly the /etc baked into the container image, with no per-deployment state overlaid on top. This requires the OS and all services to work without a writable /etc: SSH host keys, machine-id, NetworkManager leases, and similar files that are normally generated on first boot into the deployment's writable /etc must either be baked into the image or generated elsewhere (e.g. /var, systemd credentials). This mode is most useful in combination with [root] transient = true, where the transient overlay already provides a writable surface over /etc.
  • bind — /etc is bind-mounted from the deployment state directory, preserving persistent per-machine changes across reboots (the default ostree behaviour).
  • overlay — /etc is an overlayfs with the deployment state as the upper layer; writes go to state and persist across reboots.
  • transient — /etc is a tmpfs overlay; all runtime edits are discarded on reboot. Suitable for immutable or sealed images where /etc drift is undesirable.

transient (boolean, default: false) Shorthand for mount = "transient". Ignored when mount is also set.

[var]

Controls how /var is mounted from the deployment state directory.

mount (string) One of "bind" (default) or "none".

  • bind — /var is bind-mounted from the deployment state directory, preserving persistent per-machine data across reboots (the default).
  • none (alias "root") — /var is not remounted; the composefs image's empty /var directory is used. Combine with systemd.volatile=state (see below) to get a fresh tmpfs on every boot.

For a fresh, ephemeral /var on every boot (e.g. for stateless or kiosk systems), use the systemd.volatile=state kernel argument. bootc-root-setup detects this karg automatically and skips the /var bind-mount, so no explicit [var] section is needed. The karg can be baked into the image via /usr/lib/bootc/kargs.d/:

# /usr/lib/bootc/kargs.d/50-var-volatile.toml
kargs = ["systemd.volatile=state"]

This causes systemd to mount /var as a plain tmpfs at early boot, which is fully compatible with tools like podman that use overlayfs under /var. Note: unlike /etc and /root, using overlayfs (the "transient" mount type from earlier releases) for /var is not supported because it breaks podman and other tools that use overlayfs under /var/lib/containers.

EXAMPLES

Default (all persistent, equivalent to an absent file):

[etc]
mount = "bind"

Transient /etc — suitable for sealed or integrity-verified images where runtime /etc changes should be discarded on reboot:

[etc]
transient = true

Transient root with read-only /etc — / and /etc follow the composefs image exactly within the session (all writes discarded on reboot). To also make /var ephemeral, combine with systemd.volatile=state in kargs.d:

[root]
transient = true

[etc]
mount = "root"

FILES

/usr/lib/composefs/setup-root-conf.toml The configuration file read by bootc-root-setup.service.

SEE ALSO

bootc-root-setup.service(5), bootc(8)

VERSION


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

NAME

bootc-composefs-finalize-staged - Finalize a staged composefs deployment

SYNOPSIS

bootc composefs-finalize-staged [OPTIONS...]

DESCRIPTION

Finalize a staged composefs deployment. This is an internal command invoked at shutdown by bootc-finalize-staged.service; it is not intended to be run directly.

When bootc upgrade or bootc switch stages a new deployment on a composefs system, it starts bootc-finalize-staged.service, whose ExecStop runs this command as the system shuts down. It merges the current /etc into the staged deployment and updates the bootloader configuration so that the next boot uses it. If no deployment is staged, it does nothing. It fails on systems using the ostree backend, where ostree-finalize-staged.service does this instead.

The finalize service also pulls in bootc-finalize-staged-hold.service, which runs this command with --hold to keep /boot open while a deployment is staged. Otherwise an automounted /boot (such as the ESP set up by systemd-gpt-auto-generator(8)) could expire while idle, and then either deadlock with shutdown or be unavailable when finalization runs.

OPTIONS

--hold

Hold /boot open until terminated, instead of finalizing

EXAMPLES

Check whether a staged deployment is waiting to be finalized at the next shutdown:

systemctl status bootc-finalize-staged.service bootc-finalize-staged-hold.service

SEE ALSO

bootc(8), bootc-upgrade(8), bootc-switch(8)

VERSION


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

Bootloaders in bootc

bootc supports two ways to manage bootloaders.

bootupd

bootupd is a project explicitly designed to abstract over and manage bootloader installation and configuration. Today it primarily supports GRUB+shim. There are pending patches for it to support systemd-boot as well.

When you run bootc install, it invokes bootupctl backend install to install the bootloader to the target disk or filesystem. The specific bootloader configuration is determined by the container image and the target system's hardware.

Currently, bootc only runs bootupd during the installation process. It does not automatically run bootupctl update to update the bootloader after installation. This means that bootloader updates must be handled separately, typically by the user or an automated system update process.

systemd-boot

NOTE: systemd-boot is only supported for Composefs Backend and not for Ostree

If bootupd is not present in the input container image, then systemd-boot will be used by default (except on s390x).

s390x

bootc uses zipl.

none

It is possible to skip bootloader installation entirely by using --bootloader=none (or bootloader = "none" in the [install] section of the config file).

With this option, users can have explicit control over how the boot loading is handled, without bootc or bootupd intervention.

NOTE: none is only supported for the Ostree backend and not for Composefs. It is also not supported for the s390x architecture. If used with --generic-image, it will lead to a generic image that does not have support for any bootloader.

composefs backend

Whenever the container image has a UKI, bootc automatically selects the composefs backend during installation. The prerequisites for sealed images describe the currently-supported UKI + systemd-boot configuration. Note that having a UKI does not by itself make an install sealed — that also depends on whether fs-verity enforcement is on.

Composefs installs using a traditional vmlinuz/initramfs.img layout instead of a UKI can enforce fs-verity, but are never sealed, since nothing authenticates the root digest. They can use either bootupd (GRUB) or systemd-boot. Under the hood, bootc writes standard BLS boot entries for both UKI and traditional kernels; see the composefs boot module documentation for details on how entry filenames and sort-keys are chosen to sort correctly on both GRUB and systemd-boot.


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

NAME

bootc-loader-entries - Operations on Boot Loader Specification (BLS) entries

SYNOPSIS

bootc loader-entries COMMAND

DESCRIPTION

Manage kernel arguments from multiple independent sources by tracking argument ownership via x-options-source-<name> extension keys in BLS config files.

This solves the problem of kernel argument accumulation on bootc systems with transient /etc, where tools like TuneD lose their state files on reboot and cannot track which kargs they previously set.

COMMANDS

set-options-for-source : Set or update the kernel arguments owned by a specific source.

SEE ALSO

bootc(8), bootc-loader-entries-set-options-for-source(8)

VERSION


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

NAME

bootc-loader-entries-set-options-for-source - Set or update the kernel arguments owned by a specific source

SYNOPSIS

bootc loader-entries set-options-for-source --source NAME [--options "KARGS"]

DESCRIPTION

Set or update the kernel arguments owned by a specific source. Each source's arguments are tracked via x-options-source-<name> extension keys in BLS config files on /boot. The options line is recomputed as the merge of all tracked sources plus any untracked (pre-existing) options.

This command stages a new deployment with the updated kernel arguments. Changes take effect on the next reboot.

When a staged deployment already exists (e.g. from bootc upgrade), it is replaced using the staged deployment's commit and origin, preserving the pending upgrade while layering the kargs change on top.

OPTIONS

--source=SOURCE

The name of the source that owns these kernel arguments

--options=OPTIONS

The kernel arguments to set for this source

REQUIREMENTS

This command requires ostree >= 2026.1 with bootconfig-extra support for preserving extension BLS keys through staged deployment roundtrips. On older ostree versions, bootc loader-entries is unavailable: it is omitted from bootc --help, and invoking it (including with --help) prints an error and exits with status 77, distinct from usage errors (2) and other failures (1). Callers can probe for the feature by checking the exit status of bootc loader-entries set-options-for-source --help.

EXAMPLES

Add TuneD kernel arguments:

bootc loader-entries set-options-for-source --source tuned \
    --options "isolcpus=1-3 nohz_full=1-3"

Update TuneD kernel arguments (replaces previous values):

bootc loader-entries set-options-for-source --source tuned \
    --options "isolcpus=0-7"

Remove all kernel arguments owned by TuneD:

bootc loader-entries set-options-for-source --source tuned

Multiple sources can coexist independently:

bootc loader-entries set-options-for-source --source tuned \
    --options "nohz=full isolcpus=1-3"
bootc loader-entries set-options-for-source --source dracut \
    --options "rd.driver.pre=vfio-pci"

KNOWN LIMITATIONS

Source keys set by prior calls in the same boot cycle (before any reboot) are discovered by reading the staged deployment data file at /run/ostree/staged-deployment. If this file is missing or cannot be parsed, sources from prior calls may not be discovered, potentially orphaning their kargs. In practice this should not occur, as the file is managed by ostree and always present when a staged deployment exists.

SEE ALSO

bootc(8), bootc-loader-entries(8)

VERSION


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

Disk encryption (e.g. LUKS)

bootc supports any Linux filesystem and block storage setup, and does not dictate any particular architecture. Once a writable Linux filesystem is mounted, bootc can write content (images) into it, in the same way e.g. podman, apt, and dnf do.

Concretely bootc install to-filesystem and bootc upgrade both operate on pre-existing mounted filesystems. This includes LUKS and RAID underneath for block storage.

However, LUKS is important enough that it deserves its own documentation.

Recommendation: handle root filesystem encryption independently of bootc

If you want an encrypted root, set that up independently of bootc, and then point bootc install to-filesystem at the resulting mounted filesystem.

Reference implementations:

  • Anaconda, via its bootc kickstart command (which drives to-filesystem directly): kickstart's --encrypted options set up LUKS during install, and a %post script can add TPM2 binding with systemd-cryptenroll.
  • systemd-cryptsetup and systemd-cryptenroll directly, for installers or provisioning tooling that don't use Anaconda. Note that data for this can be provided via systemd-imdsd (systemd v261+): on recognized clouds.

Whichever you use, that setup is owned by whoever builds and maintains the image or installer, not by bootc.

/boot or XBOOTLDR partition

Some layouts need a separate /boot or XBOOTLDR partition — for example tpm2-luks requires one, since GRUB and most bootloaders can't read a LUKS-encrypted /boot. If yours does, pass --boot-mount-spec to to-filesystem so bootc knows where to install boot assets. See More advanced installation with to-filesystem.

Root filesystem discovery

This is no different from non-LUKS cases; either use discoverable partitions or root= plus luks.uuid style kernel arguments.

First-boot encryption of an already-populated filesystem

It's common to have a "golden image" (a KubeVirt containerdisk, a raw .qcow2, an AMI, and similar) that is already a bootc install (i.e. the target OS) already. Typically, the filesystem is not already encrypted, because there'd be nothing to use as a default unlocking mechanism. Hence encrypting each instance uniquely means converting that already-populated partition to LUKS2 in place, the first time it boots. This is OS/distro-level work, not something bootc implements.

Fedora CoreOS is a reference architecture here: its disk images ship unencrypted, and Ignition can convert the root filesystem to LUKS2 in place during the initramfs of the machine's first real boot, optionally bound via clevis to tpm2, tang (network-bound unlock), or a Shamir threshold across several pins. See the Fedora CoreOS storage docs for details. Other notes and prior art:

This only protects data written after the per-device encryption boundary exists; don't provision machine-specific secrets into the image before that boundary is in place.

Tracking

The following issues track requests to extend tpm2-luks further (configurable PCRs, recovery keys, deferred/first-boot enrollment). Given the direction above, the expectation is that these are better solved by using systemd-cryptsetup/systemd-cryptenroll directly in your own initramfs rather than by adding more options to bootc's installer:

  • #421 install to-disk with LUKS + TPM broken
  • #476 Add config option to configure systemd-cryptenroll PCRs
  • #477 LUKS volumes need configurable password and/or recovery keys
  • #1329 Support LUKS password on bootc install
  • #2089 install to-disk --block-setup tpm2-luks hangs: libdevmapper udev cookie semaphore deadlock in container IPC namespace (closed; background on why install-time LUKS setup inside a container is fragile)

The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

Security and threat model

This page describes bootc's trust boundaries and how to configure stronger guarantees than the defaults. If you believe you've found a valid vulnerability, see SECURITY.md for how to report it.

Privileges

bootc is a binary that is intended to be run with full privileges. It does not expose a service (socket, HTTP endpoint) by default; that's the role of higher level tooling.

All inputs (CLI arguments, etc) are hence considered fully trusted by default.

Container image verification

bootc honors the default /etc/containers/policy.json when fetching images. The upstream default at the time of this writing does not require signatures for generic images.

It is not a vulnerability in bootc that signatures are not required by default.

It is however a very good idea for bootc users to enable signatures for their images.

On-disk integrity vs. pull-time verification

The default backend is ostree (with a composefs mount), but fsverity is not enabled by default.

The composefs backend enables fsverity by default if available, but does not verify the composefs image (because there's nothing to verify it against by default).

The composefs backend with sealed UKIs does verify the composefs mount. With composefs + sealed UKIs, it is a vulnerability if e.g. an attacker can mutate a file in the image store, have that persist across a re-mount without it resulting in an EIO error (default for Linux kernel fsverity).


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

bootc image

Experimental features are subject to change or removal. Please do provide feedback on them.

Tracking issue: https://github.com/bootc-dev/bootc/issues/690

Using bootc image copy-to-storage

This experimental command is intended to aid in booting local builds.

Invoking this command will default to copying the booted container image into the containers-storage: area as used by e.g. podman, under the image tag localhost/bootc by default. It can then be managed independently; used as a base image, pushed to a registry, etc.

Run bootc image copy-to-storage --help for more options.

Example workflow:

$ bootc image copy-to-storage
$ cat Containerfile
FROM localhost/bootc
...
$ podman build -t localhost/bootc-custom .
$ bootc switch --transport containers-storage localhost/bootc-custom

The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

Unified storage

Experimental features are subject to change or removal. Please do provide feedback on them.

Tracking issue: https://github.com/bootc-dev/bootc/issues/20

Overview

Unified storage is the goal of having all storage for bootc be "unified" with the storage used by a container runtime, such as podman.

Currently, bootc uses either ostree or composefs. Logically bound images use the podman container storage.

Goals

  • Direct support for zstd:chunked: Container images using zstd:chunked compression can be efficiently pulled with deduplication
  • Efficient podman run <booted image>: The booted OS image is directly accessible to podman without exporting/copying
  • Shared layer storage: Layers common between the host image and app containers are stored only once
  • When used with bootc image cmd build, can support direct build into the bootc-owned storage without a copy from the podman (or other app container) storage.

Current status

Status: Experimental. The unified storage feature is under active development.

Currently supported:

  • Installation with --experimental-unified-storage flag
  • bootc switch --experimental-unified-storage to force the unified path
  • Onboarding running systems via bootc image set-unified
  • Auto-detection during upgrade/switch when image exists in bootc storage

Why this isn't the default yet

A key blocker for enabling unified storage by default is container-libs#144: the containers/image stack currently copies data between containers-storage: instances by serializing through tarballs. This means that when bootc imports from its container storage into ostree, or when copying between different container storage instances, each layer is fully re-serialized even when both storages are on the same filesystem.

The architectural fix requires separating metadata from data in the copy path, allowing file descriptors to be passed and reflinked rather than streamed through tar. This will be solved by putting composefs-rs in the middle to orchestrate zero-copy pulls. See Future plans: composefs-to-ostree.

Enabling unified storage

During installation

Use the --experimental-unified-storage flag with bootc install:

bootc install to-disk --experimental-unified-storage /dev/sdX

This causes the installation to pull the source image into bootc's container storage first, then import from there into ostree.

On a running system

To onboard an existing system to unified storage, use:

bootc image set-unified

This re-pulls the currently booted image from its original source into the bootc-owned container storage. After this, future bootc upgrade and bootc switch operations will automatically use the unified storage path when the image is detected in bootc storage.

How it works

Pull flow

With unified storage enabled:

  1. The image is pulled using podman/skopeo into /usr/lib/bootc/storage
  2. bootc then imports from containers-storage: transport into ostree
  3. The image remains in bootc storage for podman access and layer sharing

Auto-detection

During bootc upgrade or bootc switch, bootc automatically checks if the target image already exists in the bootc container storage. If so, it uses the unified storage path without requiring any flags. This means once you've onboarded via bootc image set-unified, subsequent upgrades will automatically use the unified path.

Storage location

The bootc-owned container storage is at /usr/lib/bootc/storage, which is a symlink to persistent storage under /sysroot. This is the same location used for logically bound images.

Example workflows

Local build and boot

With unified storage, you can build a derived image locally and boot it directly:

# Copy the booted image to podman storage
bootc image copy-to-storage

# Switch to use containers-storage transport (enables unified path)
bootc switch --transport containers-storage localhost/bootc

# Onboard to unified storage
bootc image set-unified

# Build a derived image directly into bootc storage
bootc image cmd build -t localhost/my-custom .

# Switch to the derived image
bootc switch --transport containers-storage localhost/my-custom

Using podman with the booted image

Once unified storage is enabled, podman can access the booted image:

podman --storage-opt=additionalimagestore=/usr/lib/bootc/storage run localhost/bootc

Relationship to composefs backend

Unified storage is complementary to the composefs backend. While unified storage changes how images are pulled (using containers/storage), the composefs backend changes how the filesystem is stored and verified.

Future plans: composefs-to-ostree

These features will be combined in upcoming work to build a "composefs-first" import pipeline. In this planned model, containers/storage will pull the image, composefs will import it via reflinks (FICLONE), and then ostree will synthesize its commit by FICLONEing from the composefs objects.

This will eliminate tar serialization entirely, meaning only one physical copy of the image data will exist on disk, shared across all three stores.

Future plans: composefs-as-storage

Looking further ahead, the ultimate evolution of unified storage is to make the host's /sysroot/composefs object store the single, global source of truth for all content-addressed files on the system.

Instead of containers/storage maintaining its own copy of application image layers and merely sharing the host OS layers, podman's composefs backend could be configured to write objects directly into /sysroot/composefs on bootc-managed systems.

This means there would be exactly one storage pool for:

  1. The bootc host OS image
  2. Logically bound app containers
  3. Standard Podman app containers
  4. Flatpak apps (by having flatpak's system helper write to the same object store)

Every file across the entire system—whether part of the base OS, a containerized database, or a desktop application—would be deduplicated automatically and perfectly at the object level via fsverity digests.

Implementation notes

For developers, the internal design and target architecture for this three-store unified storage model is documented in the rustdoc comments of the relevant source files:

  • crates/lib/src/store/mod.rs — the target three-store architecture and reflink behavior
  • crates/lib/src/bootc_composefs/repo.rs — composefs unified pull path stages
  • crates/lib/src/deploy.rs — pull dispatch and ostree backend synthesis
  • crates/lib/src/image.rs — bootc image set-unified entrypoints

Limitations

  • Experimental: The feature is not yet suitable for production use
  • Flag is hidden: The --experimental-unified-storage install flag is hidden from --help output
  • Progress reporting: Pull progress from podman is not yet integrated with bootc's progress reporting
  • Garbage collection: Images in bootc storage are garbage collected based on deployment references; see logically-bound-images.md for details

The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

bootc internals fsck

Experimental features are subject to change or removal. Please do provide feedback on them.

Using bootc internals fsck

This command expects a booted system, and performs consistency checks in a read-only fashion.


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

Factory reset with bootc install reset

This is an experimental feature; use --experimental flag to acknowledge.

Overview

The bootc install reset command allows you to perform a non-destructive factory reset of an existing bootc system. This creates a fresh installation state in a new stateroot while preserving the existing system's files on disk. After rebooting into the new deployment, you can still access the old system's data by examining files in /sysroot/ostree/deploy/<old-stateroot>/.

How it works

When you run bootc install reset:

  1. A new stateroot is created with an automatically generated name (format: state-<year>-<serial>, e.g., s2025-0)
  2. A fresh deployment is created in the new stateroot using the currently booted image (or optionally a different image via --target-imgref)
  3. Kernel arguments related to root filesystem configuration are automatically inherited from the current deployment
  4. The /boot fstab entry is preserved from the current system if it exists
  5. The new deployment becomes the default boot target

After rebooting, you'll be running in a completely fresh system state:

  • /etc contains only the configuration from the container image
  • /var is empty (no user data or state from the previous system)
  • The old stateroot's files remain on disk at /sysroot/ostree/deploy/<old-stateroot>/ and can be accessed for data recovery or inspection

Usage

Basic usage (reset to the same image currently running):

bootc install reset --experimental

Reset and switch to a different image:

bootc install reset --experimental --target-imgref quay.io/example/myimage:latest

Reset with custom stateroot name:

bootc install reset --experimental --stateroot production-2025

Reset and immediately reboot:

bootc install reset --experimental --apply

Add custom kernel arguments:

bootc install reset --experimental --karg=console=ttyS0,115200n8

Skip inheriting root filesystem kernel arguments:

bootc install reset --experimental --no-root-kargs

Kernel arguments

By default, bootc install reset automatically inherits kernel arguments from the currently booted deployment that are related to root filesystem configuration. This includes:

  • root= - Root device specification
  • rootflags= - Root filesystem mount options
  • rd.* arguments - Initramfs arguments (e.g., for LVM, LUKS, network root)
  • Kernel arguments defined in /usr/lib/bootc/kargs.d/ and /etc/bootc/kargs.d/

You can:

  • Add additional kernel arguments with --karg (can be specified multiple times)
  • Skip automatic root filesystem argument inheritance with --no-root-kargs

Use cases

  • Development/testing: Quickly return to a clean state while preserving the ability to boot back to your development environment
  • Troubleshooting: Reset to a known-good state without losing access to the problematic deployment for debugging
  • System refresh: Start fresh after accumulating configuration changes, while keeping the old state accessible
  • Image testing: Test a new image version in a separate stateroot before committing to it

Cleaning up the old stateroot

After performing a factory reset and rebooting into the new stateroot, the old stateroot remains on disk at /sysroot/ostree/deploy/<old-stateroot>/. This allows you to access files from the previous system if needed.

Once you no longer need the old stateroot, you can remove it to free up disk space:

  1. First, remove any remaining deployments from the old stateroot:
# List all deployments to find the old stateroot's deployment index
ostree admin status

# Remove the old deployment(s) by index
# The index is shown in the output (e.g., "1" for the second deployment)
ostree admin undeploy <index>
  1. After all deployments from the old stateroot are removed, you can delete the stateroot directory:
# Replace "default" with your old stateroot name if different
mount -o remount,rw /sysroot
rm -rf /sysroot/ostree/deploy/default

Note: You cannot remove the stateroot directory while deployments still exist in it. OSTree protects deployment directories with filesystem-level mechanisms, so you must undeploy them first using ostree admin undeploy.

Limitations

  • This command requires --experimental flag as the feature is still under development
  • Only works on systems already running bootc (not for initial installations)
  • The old stateroot is not automatically removed and will consume disk space until manually deleted (see "Cleaning up the old stateroot" section above)

See also

  • bootc switch - Switch to a different container image
  • bootc status - View current deployment status

The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

Interactive progress with --progress-fd

This is an experimental feature; tracking issue: https://github.com/bootc-dev/bootc/issues/1016

While the bootc status tooling allows a client to discover the state of the system, during interactive changes such as bootc upgrade or bootc switch it is possible to monitor the status of downloads or other operations at a fine-grained level with --progress-fd.

The format of data output over --progress-fd is JSON Lines which is a series of JSON objects separated by newlines (the intermediate JSON content is guaranteed not to contain a literal newline).

You can find the JSON schema describing this version here: progress-v0.schema.json.

Deploying a new image with either switch or upgrade consists of three stages: pulling, importing, and staging. The pulling step downloads the image from the registry, offering per-layer and progress in each message. The importing step imports the image into storage and consists of a single step. Finally, staging runs a variety of staging tasks. Currently, they are staging the image to disk, pulling bound images, and removing old images.

Note that new stages or fields may be added at any time.

Importing and staging are affected by disk speed and the total image size. Pulling is affected by network speed and how many layers invalidate between pulls. Therefore, a large image with a good caching strategy will have longer importing and staging times, and a small bespoke container image will have negligible importing and staging times.


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

container export

Experimental features are subject to change or removal. Please do provide feedback on them.

Overview

The bootc container export command exports a container filesystem as a tar archive suitable for unpacking onto a target system.

However, this is a bit more than a simple format transformation:

  • The output includes SELinux labeling (computed from the image's policy)
  • The kernel can optionally be copied to /boot for compatibility with Anaconda's liveimg command

Usage

bootc container export [OPTIONS] TARGET

Options

  • --format <FORMAT> - Export format (default: tar)
  • -o, --output <PATH> - Output file (defaults to stdout)
  • --kernel-in-boot - Copy kernel and initramfs from /usr/lib/modules to /boot for legacy compatibility
  • --disable-selinux - Disable SELinux labeling in the exported archive

Examples

Complete example using podman

podman run --rm \
    --mount=type=image,source=quay.io/example/example,target=/run/target \
    quay.io/example/example \
    bootc container export --kernel-in-boot /run/target > example.tar

There is also an -o option to write to a file directly, but when using podman run shell redirection is simpler since -o would require a bind mount to write to the host filesystem.

Anaconda liveimg integration

A key targeted use case for this is Anaconda's liveimg kickstart command which accepts any generic filesystem payload (tar, squashfs).

Important considerations

The installed system will not have any bootc/ostree/composefs filesystem structure, will not be read-only etc. The semantics of the installed system are exactly the same as any other usage of Anaconda liveimg or equivalent.

Container image requirements

At the current time this is only tested with a workflow starting FROM quay.io/fedora/fedora-bootc or equivalent. In theory, this workflow would be compatible with an image starting with just FROM fedora then RUN dnf -y install kernel etc., but that is not tested.

For the first case right now, you must include as part of your container build this logic or equivalent:

RUN sed -i '/layout=ostree/d' /usr/lib/kernel/install.conf && \
    rm -vf /usr/lib/kernel/install.conf.d/*-bootc-*.conf \
           /usr/lib/kernel/install.d/*-rpmostree.install

The sed command removes the layout=ostree line from install.conf while preserving any other settings. The rm commands remove the bootc drop-in and rpm-ostree plugin that would otherwise intercept kernel-install and delegate to rpm-ostree (which doesn't work outside an ostree deployment).

Required kickstart configuration

While the liveimg verb handles most of the basics, some %post scripting is also required.

Bootloader setup via kernel-install

The %post script should use kernel-install add to set up the bootloader. This creates BLS entries, copies the kernel, and generates an initramfs via the standard plugin chain (50-dracut, 90-loaderentry, etc.):

%post --erroronfail
set -eux

KVER=$(ls /usr/lib/modules | head -1)

# Ensure machine-id exists (needed by kernel-install for BLS filenames)
if [ ! -s /etc/machine-id ]; then
    systemd-machine-id-setup
fi

# kernel-install creates the BLS entry, copies vmlinuz, and generates
# initramfs via the standard plugin chain (50-dracut, 90-loaderentry, etc.)
kernel-install add "$KVER" "/usr/lib/modules/$KVER/vmlinuz"

# Regenerate grub config to pick up BLS entries
grub2-mkconfig -o /boot/grub2/grub.cfg
%end

The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

Packaging and Integration

This document describes how to build and package bootc for distribution in operating systems.

Build Requirements

  • Rust toolchain (see rust-toolchain.toml for the version)
  • coreutils and make

Basic Build Commands

The primary build targets are:

make all

This builds:

  • Binary artifacts (cargo build --release)
  • Man pages (via cargo xtask manpages)

The built binaries are placed in target/release/:

  • bootc - The main bootc CLI
  • system-reinstall-bootc - System reinstallation tool
  • bootc-initramfs-setup - Initramfs setup utility

Installation

The install target supports the standard DESTDIR variable for staged installations, which is essential for packaging:

make install DESTDIR=/path/to/staging/root

The install target handles:

  • Binary installation to $(prefix)/bin
  • Man pages to $(prefix)/share/man/man{5,8}
  • systemd units to $(prefix)/lib/systemd/system
  • Documentation and examples to $(prefix)/share/doc/bootc
  • Dracut module to /usr/lib/dracut/modules.d/51bootc
  • Base image configuration files

Optional Installation Targets

install-ostree-hooks

For distributions that need bootc to provide compatibility with ostree container commands:

make install-ostree-hooks DESTDIR=/tmp/stage

This creates symbolic links in $(prefix)/libexec/libostree/ext/ for:

  • ostree-container
  • ostree-ima-sign
  • ostree-provisional-repair

Source Packaging

Vendored Dependencies

bootc is written in Rust and has numerous dependencies. For distribution packaging, we recommend using a vendored tarball of Rust crates to ensure reproducible builds and avoid network access during the build process.

Generating the Vendor Tarball

Use the cargo xtask package command to generate both source and vendor tarballs:

cargo xtask package

This creates two files in the target/ directory:

  • bootc-<version>.tar.zstd - Source tarball with git archive contents
  • bootc-<version>-vendor.tar.zstd - Vendored Rust dependencies

The source tarball includes a .cargo/vendor-config.toml file that configures cargo to use the vendored dependencies.

Using Vendored Dependencies in Builds

When building with vendored dependencies:

  1. Extract both tarballs into your build directory
  2. Extract the vendor tarball to create a vendor/ directory
  3. Ensure .cargo/vendor-config.toml is in place (included in source tarball)
  4. Build normally with make all

The cargo build will automatically use the vendored crates instead of fetching from crates.io.

Version Management

The version is derived from git tags. The cargo xtask package command automatically determines the version:

  • If the current commit has a tag: uses the tag (e.g., v1.0.0 becomes 1.0.0)
  • Otherwise: generates a timestamp-based version with commit hash (e.g., 202501181430.g1234567890)

This ensures that development snapshots have monotonically increasing version numbers.

Cargo Features

The build respects the CARGO_FEATURES environment variable. By default, the Makefile auto-detects whether to enable the rhsm (Red Hat Subscription Manager) feature based on the build environment's /usr/lib/os-release.

To explicitly control features:

make all CARGO_FEATURES="rhsm"

The selinux feature is enabled by default and links against libselinux. Distributions that do not use SELinux can build without the default features and add back the others they want (pre-6.15 is only needed to boot composefs systems on kernels older than 6.15):

make bin CARGO_OPTIONS='--no-default-features --features "install-to-disk pre-6.15"'

Such a build treats SELinux as disabled on the host, whatever the kernel says: it never reads selinuxfs, doesn't try to enter the install_t domain and doesn't parse SELinux contexts, so it is not suitable for hosts that run SELinux. Note that libostree may still link libselinux itself, depending on how it was built, and that the unit test targets (make install-unit-tests) always use the default features.

Integration Testing

For distributions that want to include integration tests, use:

make install-all DESTDIR=/tmp/stage

This installs:

  • Everything from make install
  • Everything from make install-ostree-hooks
  • The integration test binary as bootc-integration-tests

Base image content

Alongside building the binary here, you may also want to prepare a base image. For that, see bootc-images.

Additional Resources

  • See Makefile for all available targets and variables
  • See crates/xtask/src/xtask.rs for cargo xtask implementation details
  • See contrib/packaging/bootc.spec for an example RPM spec file that uses all of the above.

The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

Package manager integration

A toplevel goal of bootc is to encourage a default model where Linux systems are built and delivered as (container) images. In this model, the default usage of package managers such as apt and dnf will be at container build time.

However, one may end up shipping the package manager tooling onto the end system. In some cases this may be desirable even, to allow workflows with transient overlays using e.g. bootc usroverlay.

Detecting image-based systems

bootc is not the only image based system; there are many. A common emphasis is on having the operating system content in /usr, and for that filesystem to be mounted read-only at runtime.

A first recommendation here is that package managers should detect if /usr is read-only, and provide a useful error message referring users to documentation guidance.

An example of a non-bootc case is "Live CD" environments, where the physical media is readonly. Some Live operating system environments end up mounting a transient writable overlay (whether via e.g. devicemapper or overlayfs) that make the system appear writable, but it's arguably clearer not to do so by default. Detecting /usr as read-only here and providing the same information would make sense.

To specifically detect if bootc is in use, you can parse its JSON status (if the binary is present) to tell if a system is tracking an image. The following command succeeds if an image is not being tracked: test $(bootc status --format=json | jq .spec.image) = null.

The /run/ostree-booted file

This is created by ostree, and hence created by bootc (with the ostree) backend. You can use it to detect ostree. However, most cases should instead detect via one of the recommendations above.

Running a read-only system via podman/docker

The historical default for docker (inherited into podman) is that the / is a writable (but transient) overlayfs. However, e.g. podman supports a --read-only flag, and Kubernetes pods offer a securityContext.readOnlyRootFilesystem flag.

Running containers in production in this way is a good idea, for exactly the same reasons that bootc defaults to mounting the system read-only.

Ensure that your package manager offers a useful error message in this mode. Today for example:

$ podman run --read-only --rm -ti debian apt update
Reading package lists... Done
E: List directory /var/lib/apt/lists/partial is missing. - Acquire (30: Read-only file system)
$ podman run --read-only --rm -ti quay.io/fedora/fedora:40 dnf -y install strace
Config error: [Errno 30] Read-only file system: '/var/log/dnf.log': '/var/log/dnf.log'

However note that both of these fail on /var being read-only; in a default bootc model, it won't be. A more accurate check is thus closer to:

$ podman run --read-only --rm -ti --tmpfs /var quay.io/fedora/fedora:40 dnf -y install strace
...
Error: Transaction test error:
  installing package strace-6.9-1.fc40.x86_64 needs 2MB more space on the / filesystem
$ podman run --read-only --rm --tmpfs /var -ti debian \
    /bin/sh -c 'apt update && apt -y install strace'
...
dpkg: error processing archive /var/cache/apt/archives/libunwind8_1.6.2-3_amd64.deb (--unpack):
 unable to clean up mess surrounding './usr/lib/x86_64-linux-gnu/libunwind-coredump.so.0.0.0' before installing another version: Read-only file system

These errors message are misleading and confusing for the user. A more useful error may look like e.g.:

$ podman run --read-only --rm --tmpfs /var -ti debian \
    /bin/sh -c 'apt update && apt -y install strace'
error: read-only /usr detected, refusing to operate. See `man apt-image-based` for more information.

Transient overlays

Today there is a simple bootc usroverlay command that adds a transient writable overlayfs for /usr. This makes many package manager operations work; conceptually it is similar to the writable overlay that many "Live CDs" use. However, one cannot change the kernel this way for example.

An optional integration that package managers can do is to detect this transient overlay situation and inform the user that the changes will be ephemeral.

Persistent changes

A bootc system by default does have a writable, persistent data store that holds multiple container image versions (more in filesystem).

Systems such as rpm-ostree implement a "hybrid" mechanism where packages can be persistently layered and re-applied; the system effectively does a "local build", unioning the intermediate filesystems.

One aspect of how rpm-ostree implements this is by caching individual unpacked RPMs as ostree commits in the ostree repo.

This section will be expanded later; you may also be able to find more information in booting local builds.


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

Relationship with other projects

bootc is the key component in a broader mission of bootable containers. Here's its relationship to other moving parts.

Relationship with podman

It gets a bit confusing to talk about shipping bootable operating systems in container images. Again, to be clear: we are reusing container images as:

  • A build mechanism (including running as a standard OCI container image)
  • A transport mechanism

But, actually when a bootc container is booted, podman (or docker, etc.) is not involved. The storage used for the operating system content is distinct from /var/lib/containers. podman image prune --all will not delete your operating system.

That said, a toplevel goal of bootc is alignment with the https://github.com/containers ecosystem, which includes podman. But more specifically at a technical level, today bootc uses skopeo and hence indirectly containers/image as a way to fetch container images.

This means that bootc automatically also honors many of the knobs available in /etc/containers - specifically things like containers-registries.conf.

In other words, if you configure podman to pull images from your local mirror registry, then bootc will automatically honor that as well.

The simple way to say it is: A goal of bootc is to be the bootable-container analogue for podman, which runs application containers. Everywhere one might run podman, one could also consider using bootc.

Relationship with Image Builder (osbuild)

There is a new bootc-image-builder project that is dedicated to the intersection of these two!

Relationship with Kubernetes

Just as podman does not depend on a Kubernetes API server, bootc will also not depend on one.

However, there are also plans for bootc to also understand Kubernetes API types. See configmap/secret support for example.

Perhaps in the future we may actually support some kind of Pod analogue for representing the host state. Or we may define a CRD which can be used inside and outside of Kubernetes.

Relationship with ostree

OSTree provides many things:

  1. a git-like repo for OS data from which you can check out an entire rootfs
  2. a bootloader integration layer
  3. a transport layer for pulling content over HTTP

With bootc, the OSTree transport layer is not used. Instead, content is pulled as OCI containers using skopeo as mentioned above. However, this content is then imported into the local OSTree repo to perform a deployment checkout. The role of OSTree may further shrink in the future, especially as tighter integration with podman and composefs occurs, but it will remain an important part of the bootc stack (in particular the bootloader integration layer and management of deployment roots).

Relationship with rpm-ostree

As mentioned above, bootc uses OSTree as a backing model, and so does rpm-ostree. Hence, when using a container source, rpm-ostree upgrade and bootc upgrade are effectively equivalent; you can use either command.

Differences from rpm-ostree

  • The ostree project never tried to have an opinionated "install" mechanism, but bootc does with bootc install to-filesystem
  • Bootc has additional features such as /usr/lib/bootc/kargs.d and logically bound images.

Client side changes

Currently all functionality for client-side changes such as rpm-ostree install or rpm-ostree initramfs --enable continue to work, because of the shared base.

However, as soon as you mutate the system in this way, bootc upgrade will error out as it will not understand how to upgrade the system. The bootc project currently takes a relatively hard stance that system state should come from a container image.

The way kernel argument work also uses ostree on the backend in both cases, so using e.g. rpm-ostree kargs will also work on a system updating via bootc.

Overall, rpm-ostree is used in several important projects and will continue to be maintained for many years to come.

However, for use cases which want a "pure" image based model, using bootc will be more appealing. bootc also does not e.g. drag in dependencies on libdnf and the RPM stack.

bootc also has the benefit of starting as a pure Rust project; and while it doesn't have an IPC mechanism today, the surface of such an API will be significantly smaller.

Further, bootc does aim to include some of the functionality of zincati.

But all this said: It will be supported to use both bootc and rpm-ostree together; they are not exclusive. For example, bootc status at least will still function even if packages are layered.

Future bootc <-> podman binding

All the above said, it is likely that at some point bootc will switch to hard binding with podman. This will reduce the role of ostree, and hence break compatibility with rpm-ostree. When such work lands, we will still support at least a "one way" transition from an ostree backend. But once this happens there are no plans to teach rpm-ostree to use podman too.

Relationship with Fedora CoreOS (and Silverblue, etc.)

Per above, it is a toplevel goal to support a seamless, transactional update from existing OSTree based systems, which includes these Fedora derivatives.

For Fedora CoreOS specifically, see this tracker issue.

See also OstreeNativeContainerStable.


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

How does the use of OCI artifacts intersect with this effort?

The "bootc compatible" images are OCI container images; they do not rely on the OCI artifact specification or OCI referrers API.

It is foreseeable that users will need to produce "traditional" disk images (i.e. raw disk images, qcow2 disk images, Amazon AMIs, etc.) from the "bootc compatible" container images using additional tools. Therefore, it is reasonable that some users may want to encapsulate those disk images as an OCI artifact for storage and distribution. However, it is not a goal to use bootc to produce these "traditional" disk images nor to facilitate the encapsulation of those disk images as OCI artifacts.


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

Relationship with systemd "particles"

There is an excellent vision blog entry that puts together a coherent picture for how a systemd (and uapi-group.org) oriented Linux based operating system can be put together, and the rationale for doing so.

The "bootc vision" aligns with parts of this, but differs in emphasis and also some important technical details - and some of the emphasis and details have high level ramifications. Simply stated: related but different.

System emphasis

The "particle" proposal mentions that the desktop case is most interesting; the bootc belief is that servers are equally important and interesting. In practice, this is not a real point of differentiation, because the systemd project has done an excellent job in catering to all use cases (desktop, embedded, server) etc.

An important aspect related to this is that the bootc project exists and must interact with many ecosystems, from "systemd-oriented Linux" to Android and Kubernetes. Hence, we would not explicitly compare with just ChromeOS, but also with e.g. Kairos and many others.

Design goals

Many of the toplevel design goals do overall align. It is clear that e.g. Discoverable Disk Images and OCI images align on managing systems in an image-oriented fashion.

A difference on goal 11

Goal 11 states:

Things should not require explicit installation. i.e. every image should be a live image. For installation it should be sufficient to dd an OS image onto disk.

The bootc install approach is explicitly intending to support things such as e.g. static IP addresses provisioned via kernel arguments at install time; it is not a goal for installations to be equivalent to dd. The bootc creator has experience with systems that install this way, and it creates practical problems in nontrivial scenarios such as "Advanced Format" disk drives, etc.

New Goal: An explicit alignment with cloud-native

The bootc project has an explicit goal to to take formats, cues and inspiration from the container and cloud-native ecosystem. More on this in several sections below.

New Goal: Continued explicit support for "unlocked" systems

A strong emphasis of the particle approach is "sealed" systems that chain from Secure Boot. bootc aims to support the same. And in practice, nothing in "particles" strictly requires Secure Boot etc.

However, bootc has a stronger emphasis on continuing to support "unlocked" systems into the foreseeable future in which key (even root level) operating system changes can be that are outside of an explicit signed state and feel equally first class, not just "developer system extensions".

Or stated more simply, it will be explicitly supported to create bootc-based operating systems that boot as e.g. a cloud instance or as desktop machine that defaults to an unlocked state and provides good ergonomics in this scenario for managing user owned state across operating system upgrades too.

Hermetic /usr

One of the biggest differences starts with this. The idea of having the entire operating system self-contained in /usr is a good one. However, there is an immense amount of prior history and details that make this hard to support in many generalized cases.

This tracking issue is a good starting point - it's mostly about /etc (see below).

bootc design: Carve out sub mounts

Instead, the bootc model allows arbitrary directory roots starting from / to be included in the base operating system image.

This first notable difference is rooted in bootc taking a stronger cue from the opencontainers ecosystem (including docker/podman/Kubernetes). There are no restrictions on application container filesystem layout (everything is ephemeral by default, and persistence must be explicit); bootc aims to be closer to this.

There is still alignment: bootc design does strongly encourage operating system state to live underneath /usr - it should be the default place for all operating system executable binaries and default configuration. It should be read-only by default.

/etc

Today, the bootc project uses ostree as a backend, and a key semantic ostree provides for /etc is a "3 way merge".

This has several important differences. First, it means that /etc does get updated by default for unchanged configuration files.

The default proposal for "particle" OSes to deal with "legacy" config files in /etc is to copy them on first OS install (e.g. /usr/share/factory).

This creates serious problems for all the software (for example, OpenSSH) that put config files there; - having the default configuration updated (e.g. for a security issue) for a package manager but not an image based update is not viable.

However a key point of alignment between the two is that we still aim to have /etc exist and be useful! Writing files there, whether from vi or config management tooling must continue to work. Both bootc and systemd "particle" systems should still Feel Like Unix - in contrast to e.g. Android.

At the current time, this is implemented in ostree; as bootc moves towards stronger integration with podman, it is likely that this logic will simply be moved into bootc instead on top of podman. Alternatively perhaps, podman itself may grow some support for specifying this merge semantic for containers.

Other persistent state: /var

Supporting arbitrary toplevel files in / on operating system updates conflicts with a desire to have e.g. /home be persistent by default.

Hence, bootc emphasizes having e.g. /home → /var/home as a default symlink in base images.

Aside from /home and /etc, it is common on most Linux systems to have most persistent state under /var, so this is not a major point of difference otherwise.

Other toplevel files/directories

Even the operating systems have completed "UsrMerge" still have legacy compatibility symlinks required in /, e.g. /bin → /usr/bin. We still need to support shipping these for many cases, and they are an important part of operating system state. Having them not be explicitly managed by OS updates is hence suboptimal.

Related to this, bootc will continue to support operating systems that have not completed UsrMerge.

Discoverable Disk images and booting

The bootc project will not use Discoverable Disk Images. Instead, we orient as strongly around opencontainers/image-spec i.e. OCI/Docker images.

This is the biggest technical difference that strongly influences many other aspects of operating system design and experience.

It is an explicit goal of the bootc project that it should feel as natural as possible for someone familiar with "application containers" from podman/Docker/Kubernetes to take their tools and knowledge and apply that to the base operating system too.

Technical heart: composefs

There is a very strong security rationale behind much of the design proposal of "particles" and DDIs. It is absolutely true today, quoting the blog:

That said, I think [OCI has] relatively weak properties, in particular when it comes to security, since immutability/measurements and similar are not provided. This means, unlike for system extensions and portable services a complete trust chain with attestation and per-app cryptographically protected data is much harder to implement sanely.

The composefs project aims to close this gap, and the bootc project will use it, and has an explicit goal to align with e.g. podman in using it too.

Effectively, everywhere one might use a DDI, bootc will usually support a container image. (However for some things like system configuration files, bootc may aim to instead support e.g. plain ConfigMap files which are signed for example).

System booting

The bootloader

The strong emphasis of the UAPI-group is on UEFI. However, the world is a bit broader than that; the bootc project also will explicitly continue to support:

  • GNU Grub for multiple reasons; among them that unfortunately x86 BIOS systems will not disappear entirely in the next 10 years even.
  • Android Boot - because some hardware manufacturers ship it, and we want to support operating systems that must work on this hardware.
  • zipl because it's how things work on s390x, and there is significant alignment in terms of emphasizing a "unified kernel" style flow.

Boot loader configs

bootc aims to align with the idea of generic bootloader-independent config files where possible; today it uses ostree. For more on this, see ostree and bootloaders.

The kernel and initramfs

There is agreement that in order to achieve integrity, there must be a strong link between the kernel and the first userspace code that executes in the initial RAM disk.

Building on the bootloader statement above: bootc will support UKI, but not require it.

The root filesystem

In the bootc model, the root filesystem defaults to a single physical Linux filesystem (e.g. xfs, ext4, btrfs etc.). It is of course supported to mount other partitions and filesystems; doing so is encouraged even for /var. , where one ends up with some space constraints around the OS /usr partition due to dm-verity.

This is a rather large difference already from particles; the root filesystem contains the operating system too; it is not a separate partition. One thing this helps significantly with is dealing with the "space management" problems that dm-verity introduces (need for a partition to have unused empty space to grow, and also a fixed-size ultimate capacity limit).

Locating the root

bootc does not mandate or emphasize any particular way to locate the root filesystem; parts of the discoverable partitions specification specifically the "root partition" may be used. Or, the root filesystem can be found the traditional way, via a local root= kernel argument.

Another point of contrast from the particle emphasis is that while we encourage encrypting the root filesystem, it is not required. Particularly some use cases in cloud environments perform encryption at the hypervisor level and do not want additional overhead of doing so per virtual machine.

Locating the base container image

Until this point, we have been operating under external constraints; no one is creating a bootloader that directly understands how to start a container image, for example. We've gotten as far as running a Linux userspace in the initial RAM disk, and the physical root filesystem is mounted.

Here, we circle back to composefs. One can think of composefs as effectively a way to manage something like dm-verity, but using files.

What bootc builds on top of that is to target a specific container image rootfs that is part of the "physical" root. Today, this is implemented again using ostree, via the ostree= kernel commandline argument. In the future, it is likely to be a bootc.image. However, integration with other bootloaders (such as Android Boot) require us to interact with externally-specified fixed kernel arguments.

Ultimately, the initramfs will contain logic to find the desired root container, which again is just a set of files stored in the "physical" root filesystem.

Chaining integrity from the initramfs

One can think of composefs as effectively a way to manage something like dm-verity, but supporting multiple ones stored inside a standard Linux filesystem.

For "sealed" systems, the bootc project suggests a default model where there is an "ephemeral key" that binds the UKI (or equivalent) and the real root. For a bit more on this, see ostree and composefs. Effectively, at image build time an "ephemeral" key is generated which signs the composefs digest of the container image. The public half of this key is injected into the UKI, which is itself signed e.g. for Secure Boot.

At boot time, the initramfs will use its embedded public key to verify the composefs digest of the target root - and from there, overlayfs in the Linux kernel combined with fs-verity will continually verify the integrity of all operating system root files we use.

At the current time, there is not one single standardized approach for signing composefs images. Ultimately, a composefs image has a digest, and signing and verification of that digest can be done via any signing tool. For more on this, see this issue.

bootc itself will not mandate one mechanism currently. However, it is very likely that we will ship an optionally-enabled opinionated mechanism that uses basic ed25519 signatures for example.

This is effectively equivalent to the particle approach of embedding a verity root hash into the kernel commandline - it means that the booted Linux kernel will only be capable of mounting that one specific root filesystem. Note that this model is effectively the same as e.g. Fedora uses to sign kernel modules.

However, an "ephemeral key" is not the only valid way to do things; for some operating system creators it may be very desirable to continue to be able to make root OS image changes without changing the UKI (and hence re-signing it). Instead, another valid approach is to simply maintain a persistent public/private keypair. This allows disconnecting the build of userspace and kernel, but also means that there is less strict verification between kernel and userspace (e.g. downgrade attacks become possible).

Chaining integrity to configuration and application containers

composefs is explicitly designed to be useful as a backend for "application" containers (e.g. podman). There is again not one single mechanism for signing and verification; in some use cases, it may be enough to boot the operating system enough to implement "network as source of truth" - for example, the public keys for verification of application containers might be fetched from a remote server. Then before any application containers are run, we dynamically fetch the relevant keys from a server which was trusted.

The bootc project will align with podman in general, and make it easy to implement a mechanism that chains keys stored alongside the operating system into composefs-signed application containers.

Configuration (effectively starting from /etc and the kernel commandline) in a "sealed" system is a complex topic. Many operating system builds will want to disable the default "etc merge" and make /etc always lifecycle bound with the OS: commonly writable but ephemeral.

This topic is covered more in the next section.

Modularity

A goal of "particles" is to add integrity into "general purpose" Linux OSes and distributions - supporting a world where there are a lot of users that simply directly install an OS from an upstream OS such as Debian or Fedora. This has a lot of implications; among them that e.g. the Secure Boot signatures etc. are made by the OS creator, not the user.

A big emphasis for the bootc project in contrast a design where it is normal and expected for many users to derive (via standard container build technology) from the base image produced by the OS upstream.

This is just a difference in emphasis: "particles" can clearly be built fully customized by the end customer, and bootc fully supports booting "stock" images.

But still: the bootc project will again much more strongly push any scenario that desires truly strong integrity towards making and managing custom derived builds.

Extensions and security

In "unlocked" scenarios, the bootc project will continue to support a "traditional Unix" feeling where persistent changes to /etc can be written and maintained. Similarly, it will continue to be supported to have machine-local kernel arguments. There is significant value in migrating "package based" systems to "image based" systems, even if they are still "unsigned" or "unlocked".

The particle model calls for tools like confext that use DDIs. The "backend" of this (managing merged dynamic filesystem trees with overlayfs) and its relationship with systemd units is still relevant, but the bootc approach will again not expose DDIs to the user. Instead, our approach will take cues from the cloud-native world and use e.g. Kubernetes ConfigMap and support signatures on these.

More Modularity: Secondary OS installs

This uses OCI containers, which will work the same as the host.

Developer Mode

This topic heavily diverges between the "unlocked" and "sealed" cases. In the unlocked case, the bootc project aims to still continue to make it feel very "first class" to perform arbitrary machine-local mutations. Instead of managing overlay DDIs, bootc will make it trivial and obvious to use local container builds using any standard container build tooling.

Package managers

In order to ease the transition for users coming from package systems, the bootc project suggests that package managers like apt and dnf etc. learn how to become a frontend for "local" container builds too. In other words, apt|dnf install foo would become shorthand for a container build like:

FROM <localhost>
RUN apt|dnf install foo

Transitioning from unlocked, mutable local state to server-built images

Building on the above, a key point of bootc is to make it easy and obvious how to go from an "unlocked" system with potential unmanaged state towards a system built and managed using standard OCI container image build systems and tooling. For example, there should be a command like apt|dnf print-containerfile. (The problem is more complex than this of course, as we would likely want to capture some changes from /etc - but also some of those changes may include secrets, which are their own sub-topic)

Democratizing Code Signing

Strong alignment here.

Running the OS itself in a container

This is equally obvious to do when the host and the linked container runtime (e.g. podman) again use the same tools.

Parameterizing Kernels

In "unlocked" scenarios (per above) we will continue to use bootloader configuration that is unsigned.

We will not (in contrast to particles) try to strongly support a "partially sealed, general purpose" model. More on this below.

Most cases for "sealed" systems will want to entirely lock the kernel commandline, not even using a bootloader at all and hence there is no mechanism to configure it locally at all. However, as discussed in various venues around UKI, "sealed" systems can become complex to deploy where there is a need for machine (or machine-type) specific kernel arguments:

The bootc project default approach for this is to lean into the container-native world, using derivation to create a machine-independent "base image", then create derived, machine (or machine-class) specific images that are in turn signed.

Updating Images

A big differentiation here is that bootc will reuse container technology for fetching updates. The operating system and application containers will be signed with e.g. sigstore or similar for network fetching. The signature will cover the composefs digest, which enables continuous verification.

Managing storage of container images using composefs is more complex than systemd-sysupdate writing to a partition, but significantly more flexible. For more on this, see upstream composefs.

Kernel in images

The bootc and particle approaches are aligned on storing the kernel binary in /usr/lib/modules/$kver. On the bootc side, a key bit here is that bootc will extract the kernel and initramfs (or just UKI) and put it in the appropriate place - this is implemented as a transactional operation. There are significant details that can vary for how this works (because unlike particles, bootc aims to support non-EFI setups as well), but the high level idea is similar.

Boot Counting + Assessment

This topic relates to the previous one; because of multiple bootloaders, there is not one single approach. The systemd automatic boot assessment is good where it can be used, but we also will support e.g. Android bootloaders.

Picking the Newest Version

Because the storage of images is not just files or partitions, bootc will not expose to the user/administrator a semantic of strvercmp or package-manager oriented versioning semantics. Instead, the implementation of "latest" will be implemented in a more Kubernetes-oriented fashion of having "local" API objects with spec and status. This makes it easy and obvious for higher level management (e.g. cluster) tooling to orchestrate updates in a Kubernetes-style fashion.

Home Directory Management

The bootc project will not do anything with this. We will support systemd-homed where users want it, but in many dedicated servers and managed devices the idea of persistent user "home directories" are more of an anti-pattern.

Partition Setup

The biggest difference again here is that bootc is oriented closer to a single root partition by default that includes the OS, system/app containers and persistent local state all as one unit.

Trust chain

In contrast to particles, the bootc project does not aim to by default emphasize a model of using sysexts from the initramfs because its primary use case occurs when using a "partially sealed" system. And per above (re kernels) it is insufficient for other cases.

Without this in the mix then, the trust chain is simple to describe: the kernel+initramfs are verified by the bootloader, the initramfs contains the key and logic necessary to verify the composefs digest of the root, and the root starts to verify everything else.

File System Choice

As mentioned above, any Linux filesystem is valid for the root. For "sealed" systems using composefs will cover integrity and there is not a distinct need for dm-integrity.

OS Installation vs. OS Instantiation

The bootc project is just less partition-oriented and more towards multiple-composefs-in-root oriented. However the high level goal is shared of making it easy to "re-provision" and keeping the install-time flow as close as possible.

Building Images According to this Model

This is a key point of bootc: we aim for operating systems and distributions to ship their own bootc-compatible base images that can be used as a default derivation source. These images are just OCI images that will follow simple rules (as mentioned above, the kernel is found in /usr/lib/modules/$kver/vmlinuz) for example for the extra state to boot.

However in order to enable "sealed" systems (using signed composefs digests), the container build system will need support for this. But, it is a goal to standardize the composefs metadata needed alongside the OCI, and to support this in the broader container ecosystem of tools (e.g. docker, podman) as well as bootc.

Final words

This document is obviously very heavily inspired by the original blog.

A point of divergence is that a goal of the bootc project is to strongly influence the existing operating systems and distributions and help them migrate their customers into an image-based world - and to make practical compromises in order to aid that goal.

But, the bootc project strongly agrees with the idea of finding common ground (the "50% shared" case). At a practical level, this project will take a hard dependency on systemd and on the container ecosystem, extending bridges where they exist, working on shared standards and approaches between the two.


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

Internals

This section documents bootc's internal architecture for developers working on the project. For user-facing documentation, see the rest of this book.

Code Architecture

CLI Structure

The bootc binary (crates/cli) is a thin wrapper that:

  1. Performs global initialization (signal handlers, mounting filesystems)
  2. Creates a tokio async runtime (single-threaded)
  3. Delegates to bootc_lib::cli::run_from_iter()

The CLI uses clap with derive macros. Each subcommand typically opens the system storage via store::BootedStorage::new_from_env(), performs the operation, and writes status to stdout or a progress fd.

CPU-intensive work is offloaded via tokio::task::spawn_blocking.

Crate Organization

  • bootc (crates/cli): Thin binary entrypoint
  • bootc-lib (crates/lib): Core implementation library
  • ostree-ext (crates/ostree-ext): OCI/ostree bridging, container import/export
  • Supporting crates: Focused utilities (mount, blockdev, kernel cmdline, etc.)

Most functionality lives in bootc-lib, making it testable. The API is internal and not stable for external consumers.

Storage Backends

OSTree Backend (stable, default)

Uses ostree for content-addressed storage. Container images are imported via ostree-ext. Deployments use ostree's native mechanism with Boot Loader Specification (BLS) entries.

Key paths:

  • /sysroot/ostree/repo/ - OSTree repository
  • /sysroot/ostree/deploy/<stateroot>/ - Deployment directories

Composefs Backend

Uses composefs-rs directly, enabling native UKI support and sealed images with fsverity integrity.

Key paths:

  • /sysroot/composefs/ - Composefs repository (EROFS images)
  • /sysroot/state/deploy/<deployment-id>/ - Per-deployment state (deployment-id is the SHA-512 fsverity digest)

Implementation: bootc_composefs module in bootc-lib.

Key Modules

The Store Module

The store module provides the Storage type abstracting both backends. It lazily initializes:

  • OSTree sysroot (ostree::Sysroot)
  • Composefs repository (composefs::Repository<Sha512HashValue>)
  • Container image storage for bound images (podstorage::CStorage)

Deploy Module

Handles deployment lifecycle:

  • Staging new deployments from container images
  • Kernel argument management (bootc_kargs)
  • Three-way merge of /etc configuration
  • Rollback between deployments

Spec Module

Defines core types (see spec module rustdoc). These are ultimately the types that are serialized to bootc status --json and form a key part of the admin experience.

bootc-owned Container Storage

The podstorage module implements bootc's own containers-storage: instance at /sysroot/ostree/bootc/storage/ (symlinked to /usr/lib/bootc/storage/). This supports Logically Bound Images with proper lifecycle management and garbage collection tied to deployments.

Rustdoc API Documentation

The following rustdoc documentation is generated from the source code with --document-private-items to expose internal APIs.

Core crates

Supporting crates

Utility crates

External git crates

These crates are pulled from git and are not published to crates.io (so not on docs.rs).

  • composefs-ctl - composefs-rs entrypoint crate (re-exports composefs, composefs-boot, composefs-oci)

The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.