Atomic updates, rollbacks, and rebasing in Bazzite

1. The Immutable OS Model

In Bazzite, /usr is a read-only bind-mount derived from an ostree commit. The running filesystem is never modified in place. Instead, rpm-ostree assembles a new deployment directory on disk, and a reboot atomically pivots to it. At any point two deployments exist: the currently booted one and either a staged update or the previous rollback target.

/etc is a three-way merge between the base image's /usr/etc, the prior deployment's /etc, and any user edits. /var is shared across all deployments and is never replaced by an update.

Path Managed by Persists across updates
/usrostree / imageNo — replaced atomically
/etcthree-way mergeUser edits preserved
/varuser / systemYes — fully persistent
/homesymlink → /var/homeYes
/ostreeostree object storeManaged by ostree
Note

Writing to /usr at runtime fails with a permission error. Changes to system files require either layering an RPM or overriding a file via rpm-ostree override replace.

2. Applying Updates

Automatic updates

Bazzite ships with ublue-update, a wrapper that calls rpm-ostree upgrade on a schedule. By default it runs on login and periodically in the background via a systemd timer. Staged updates are downloaded and prepared in the background; they take effect on the next reboot.

# Check whether a staged update is waiting
rpm-ostree status

# Force a check and stage an update right now
rpm-ostree upgrade

# Stage without rebooting, then reboot when convenient
rpm-ostree upgrade --reboot    # reboots immediately after staging
systemctl reboot               # or reboot manually later

Reading rpm-ostree status

rpm-ostree status lists every deployment tracked by ostree on this machine. The entry marked is the currently booted deployment. Any entry marked staged will become the default on next boot.

rpm-ostree status

State: idle
Deployments:
● ostree-image-signed:docker://ghcr.io/ublue-os/bazzite:stable
                  Version: 40.20240901.0 (2024-09-01T12:00:00Z)
                   Commit: a3f8c2...
              LayeredPackages: steam-devices

  ostree-image-signed:docker://ghcr.io/ublue-os/bazzite:stable
                  Version: 40.20240825.0 (2024-08-25T08:00:00Z)
                   Commit: 7e91d4...

Two deployments are shown. The older one below the active entry is the rollback target. Layered packages installed by the user appear under LayeredPackages in the deployment that contains them.

Tip

Pass -v to see the full list of packages in each deployment, including base image contents and all overrides.

3. Rolling Back a Deployment

Because Bazzite keeps the previous deployment on disk, recovering from a broken update requires no reinstall. The rollback target is the deployment immediately below the active one in rpm-ostree status output.

Rolling back from a running system

rpm-ostree rollback
# Marks the previous deployment as default; takes effect on next boot

systemctl reboot

This does not delete the current deployment. After the reboot, the deployment you just came from becomes the new rollback target. You can run rpm-ostree rollback again to bounce back to it.

Rolling back from the GRUB menu

If the system is unbootable, hold Shift (BIOS) or press Esc during boot (UEFI) to reach the GRUB menu. ostree generates one entry per deployment:

  1. Select Bazzite — previous (or the second ostree entry) in GRUB.
  2. Boot into the older deployment.
  3. Once logged in, run rpm-ostree rollback to make it the permanent default.
  4. Reboot normally.
Warning

ostree only retains two deployments by default. Staging a new update after a rollback will discard the deployment you just rolled back from — there is no undo at that point without pinning (see §4) or fetching the old image digest manually.

Cleaning up old deployments manually

# List by index (0 = booted, 1 = rollback)
rpm-ostree status

# Remove a specific deployment by its index
sudo ostree admin undeploy 1

# Remove all non-booted, unpinned deployments
rpm-ostree cleanup -p   # pending (staged)
rpm-ostree cleanup -r   # rollback
rpm-ostree cleanup -b   # base (refspec cache in /var/cache/rpm-ostree)

4. Pinning Deployments

Pinning prevents ostree from garbage-collecting a deployment when a new one is staged. This is useful before a risky rebase or when you need to preserve a known-good state beyond the default two-deployment window.

# Pin deployment at index 0 (currently booted)
sudo ostree admin pin 0

# Pin index 1 (rollback target)
sudo ostree admin pin 1

# List pinned deployments — look for "Pinned: yes"
rpm-ostree status

# Unpin by index
sudo ostree admin pin --unpin 0
Note

Pinned deployments still consume disk space in /ostree/deploy. Each deployment is roughly the delta size of the image; for Bazzite this is typically 3–6 GB per slot.

5. Rebasing to a Different Image

Rebasing switches the image source tracked by rpm-ostree. You use this to: change Bazzite variant (e.g. AMD → NVIDIA), switch to a different Universal Blue image altogether, or track a different branch (stable, testing, unstable).

Rebase syntax

rpm-ostree rebase ostree-image-signed:docker://ghcr.io/ublue-os/<IMAGE>:<TAG>

Common rebase targets

Variant Image reference
Bazzite (AMD/Intel) ghcr.io/ublue-os/bazzite:stable
Bazzite NVIDIA ghcr.io/ublue-os/bazzite-nvidia:stable
Bazzite GNOME ghcr.io/ublue-os/bazzite-gnome:stable
Bazzite GNOME NVIDIA ghcr.io/ublue-os/bazzite-gnome-nvidia:stable
Bazzite Deck (Steam Deck UI) ghcr.io/ublue-os/bazzite-deck:stable
Testing branch ghcr.io/ublue-os/bazzite:testing
Unstable branch ghcr.io/ublue-os/bazzite:unstable

Full rebase example — AMD to NVIDIA

# 1. Pull the new image and stage the rebase
rpm-ostree rebase \
  ostree-image-signed:docker://ghcr.io/ublue-os/bazzite-nvidia:stable

# 2. Reboot into the rebased deployment
systemctl reboot

# 3. Verify the active image after reboot
rpm-ostree status
Warning

Layered packages carry over after a rebase, but packages that don't exist in the new image's repos will cause the rebase to fail at apply time. Remove incompatible layers first with rpm-ostree uninstall <pkg>.

Rebasing to a specific image digest

To reproduce a precise build (e.g. for bisecting regressions), rebase to a digest rather than a floating tag:

# Get the digest of the currently booted image
rpm-ostree status --json \
  | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['deployments'][0]['container-image-reference'])"

# Rebase to a specific digest
rpm-ostree rebase \
  ostree-image-signed:docker://ghcr.io/ublue-os/bazzite@sha256:<DIGEST>

Switching back

A rebase is just another deployment. Roll back with rpm-ostree rollback the same way as any other update. The pre-rebase deployment remains on disk until a new update or rpm-ostree cleanup evicts it.

Note

After a rebase, the rollback target is the pre-rebase deployment on the original image/branch. Applying an update on the new branch will discard it — pin it first if you might need it.

6. Layered Packages and Update Interaction

Packages installed via rpm-ostree install are layered on top of the base image in a derived ostree commit. They are re-applied on every update, meaning the update process fetches the new base image, re-resolves layered packages against it, and assembles a new commit. This makes updates with many layered packages slower.

# Install a package as a layer
rpm-ostree install vim

# Remove a layered package
rpm-ostree uninstall vim

# List currently installed layers
rpm-ostree status   # see LayeredPackages field

# Override a base package with a different version
rpm-ostree override replace /path/to/local.rpm

# Remove an override
rpm-ostree override reset package-name
Tip

Prefer Flatpaks, Homebrew (brew), or containers (distrobox) for user software instead of layering. Layering increases deploy time and creates additional failure modes during rebases.

Danger

rpm-ostree override remove drops a package from the base image entirely. Removing a dep of the init system or display server will likely produce an unbootable deployment. Test in a VM or ensure you can boot the rollback from GRUB before doing this on bare metal.

7. Quick Reference

Goal Command
Check deployment state rpm-ostree status
Stage an update rpm-ostree upgrade
Stage update and reboot rpm-ostree upgrade --reboot
Roll back (needs reboot) rpm-ostree rollback
Pin a deployment sudo ostree admin pin <index>
Unpin a deployment sudo ostree admin pin --unpin <index>
Rebase to another image rpm-ostree rebase ostree-image-signed:docker://<ref>
Remove staged update rpm-ostree cleanup -p
Remove rollback slot rpm-ostree cleanup -r
Layer a package rpm-ostree install <pkg>
Remove a layer rpm-ostree uninstall <pkg>
Check ostree object store sudo ostree fsck
Note

All rpm-ostree commands that modify deployments operate on a staged pending state. No change takes effect until the next reboot, and every staged change is visible in rpm-ostree status before you commit to it by rebooting.