How to Build a Custom Fedora Atomic Image with BlueBuild or ostree-native-containers

Build a custom Fedora Atomic image by layering a container image on top of a Fedora base using ostree-native-containers and committing it to an OSTree repository.

The scenario

You installed Fedora Silverblue and realized the default image lacks the proprietary GPU driver or the specific database server you need. You ran sudo dnf install and got a transaction error because the root filesystem is read-only. You need a custom base image that bakes those packages in before deployment. Building a custom Atomic image solves this by creating a versioned, bootable OS tree that already contains your required software.

How the immutable root actually works

Atomic desktops use OSTree to manage the operating system as a single, versioned tree. Think of it like a Git repository for your entire root filesystem. Every update creates a new commit. You boot into one commit, and if it breaks, you switch to the previous one. The system is immutable by design. You cannot install packages directly on a running Atomic system using traditional package managers. Instead, you build a container image that contains your desired packages, then use OSTree to convert that container into a bootable OS tree.

The container acts as a blueprint. OSTree extracts the filesystem layers, applies them to a local repository, and creates a deployable ref. The deployment process does not modify your running system. It writes a new tree to the disk and updates the bootloader configuration. You reboot into the new tree. If something fails, the bootloader still points to the old tree. Rollback is a matter of selecting the previous boot entry.

Run ostree admin status before you make any changes. Knowing your current boot position prevents accidental data loss.

Build the container blueprint

You need a container image that contains the exact packages and configuration files you want in your custom OS. The container does not run as a service. It only exists to package the filesystem layers correctly. You will use podman to build it, and you must apply specific OSTree labels so the ostree CLI recognizes the image as a valid OS blueprint.

Here is how to write a Containerfile that prepares the filesystem for OSTree consumption.

FROM fedora:latest
# Start with the standard Fedora base. This pulls the latest stable release.

RUN dnf install -y kernel-modules-extra \
    && dnf clean all
# Install your required packages. Clean the cache to keep the image small.

RUN mkdir -p /etc/ostree-release
# Create the directory OSTree expects for release metadata.

RUN echo "Fedora" > /etc/ostree-release/fedora-release
# Write a minimal release identifier. OSTree uses this to validate the tree.

LABEL org.opencontainers.image.ref.name="fedora-atomic"
# Tell OSTree this container represents a base OS, not an application.

LABEL io.containerized.data.application="fedora-atomic"
# Mark the container as an OSTree-compatible OS image.

LABEL version="40.0"
# Set a version string. This becomes the commit metadata in the repo.

Build the container image locally. The build process caches layers, so subsequent builds only recompile the steps that changed.

podman build -t my-custom-os:latest .
# Build the container from the current directory. Tag it for easy reference.

podman images | grep my-custom-os
# Verify the image exists locally before passing it to OSTree.

The container now holds your custom packages and the required labels. You have not touched the host filesystem yet. The image lives in the container storage directory.

Convert the container to an OSTree ref next. The conversion step is where the blueprint becomes a bootable tree.

Convert the container to an OSTree ref

OSTree needs a local repository to store the new commit. The repository holds the compressed content and the metadata tree. You will point OSTree at the container image, extract the layers, and commit them as a new ref.

Here is how to initialize the repository and pull the container into it.

ostree init --repo=/var/lib/containers/storage/ostree/repo/fedora-atomic
# Create the OSTree repository directory if it does not exist yet.

ostree container pull --repo=/var/lib/containers/storage/ostree/repo/fedora-atomic \
  --name=my-custom-os \
  my-custom-os:latest
# Pull the local container image into the OSTree repo. This extracts layers.

The ostree container pull command reads the container manifest, downloads the filesystem layers, and stores them as OSTree content. It creates a ref named my-custom-os inside the repository. The ref points to a commit hash that represents your custom OS tree.

You can now deploy this ref to a target system. The deployment command writes the tree to the disk and updates the bootloader.

ostree admin deploy --repo=/var/lib/containers/storage/ostree/repo/fedora-atomic \
  my-custom-os
# Deploy the new ref to the current system. This creates a new boot entry.

ostree admin status
# Show the current boot position and available deployments.

Reboot before you debug. Half the time the symptom is gone once the new tree is active.

Verify the deployment

After the reboot, you need to confirm that the system booted into your custom tree and that the packages are actually present. The bootloader should show two entries: the original Fedora Atomic image and your custom my-custom-os deployment.

Here is how to check the active tree and verify package presence.

rpm-ostree status
# Show the current deployment, commit hash, and installed packages.

rpm-ostree pkg list
# List all packages currently active in the running tree.

journalctl -xeu systemd-ostree-seeded.service
# Check the seed service logs. It confirms OSTree successfully mounted the tree.

The rpm-ostree status output will display the commit ID and the ref name. If you see my-custom-os under the Deployments section, the tree is active. The pkg list command will show the packages you baked into the container. If a package is missing, the container build step failed to install it, or the package was removed during layer compression.

Run rpm-ostree status first. Read the actual commit hash before guessing which tree you are on.

Common pitfalls and error messages

Building custom Atomic images introduces a few specific failure modes. Most of them stem from container labeling issues, SELinux context mismatches, or confusing the ostree CLI with rpm-ostree.

The ostree container pull command will refuse to proceed and print Error: Container image does not contain OSTree metadata. This happens when the container lacks the org.opencontainers.image.ref.name label or the /etc/ostree-release directory. OSTree cannot distinguish between a random application container and a valid OS blueprint without those markers. Add the labels to your Containerfile and rebuild.

If you see [FAILED] Failed to start systemd-ostree-seeded.service during boot, your repository path or ref name contains a typo. The seed service cannot mount the tree because the commit hash does not exist in the repo. Check the ref name in ostree repo list-refs and verify the path matches exactly.

SELinux denials appear when you copy custom configuration files into the container without preserving contexts. The OSTree tree expects specific labels for /etc/ and /usr/. If you see avc: denied { read } for pid=... comm="sshd" name="sshd_config" dev="dm-0" ino=... scontext=system_u:system_r:sshd_t:s0 tcontext=system_u:object_r:etc_t:s0, your container build did not set the correct file contexts. Use semanage fcontext and restorecon inside the Containerfile, or rely on the base image's default labeling.

Config files in /etc/ are user-modified. Files in /usr/lib/ ship with the package. Edit /etc/. Never edit /usr/lib/. The OSTree tree treats /usr/ as read-only content. Changes there will be overwritten on the next deployment.

Trust the package manager. Manual file edits drift, snapshots stay.

Choose your build tool

Use BlueBuild when you want a declarative YAML workflow that handles dependency resolution and cross-architecture builds automatically. Use ostree-native-containers when you need full control over the container layering process and want to integrate directly with existing CI pipelines. Use osbuild when you are building images for bare metal, cloud providers, or virtualization platforms that require specific disk formats. Use standard Fedora Atomic when you only need the default desktop environment and occasional containerized applications.

Snapshot the system before the upgrade. Future-you will thank you.

Where to go next