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 |
|---|---|---|
/usr | ostree / image | No — replaced atomically |
/etc | three-way merge | User edits preserved |
/var | user / system | Yes — fully persistent |
/home | symlink → /var/home | Yes |
/ostree | ostree object store | Managed by ostree |
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.
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
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.
Pass -v to see the full list of packages in each deployment, including base image contents and all overrides.
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.
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.
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:
rpm-ostree rollback to make it the permanent default.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.
# 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)
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
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.
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).
rpm-ostree rebase ostree-image-signed:docker://ghcr.io/ublue-os/<IMAGE>:<TAG>
| 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 |
# 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
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>.
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>
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.
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.
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
Prefer Flatpaks, Homebrew (brew), or containers (distrobox) for
user software instead of layering. Layering increases deploy time and creates additional
failure modes during rebases.
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.
| 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 |
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.