Bootc Image Resolution and Upgrade Behavior#
Bootc resolves which image to pull during upgrades from the ostree origin file — set at install time or updated by bootc switch — not from build-time metadata. Two separate sources inform the system about its image identity:
| Source | Written | Authority |
|---|---|---|
/usr/share/ublue-os/image-info.json | Build time | Display/tooling (image name, channel label, signing indicator) |
ostree origin file (via bootc status) | Install / bootc switch | Upgrade target resolution |
Understanding this split is essential when debugging upgrade behavior, channel display mismatches, or migration issues.
Build-Time Metadata: image-info.json#
/usr/share/ublue-os/image-info.json is baked into the OCI image at build time and never updated at runtime. It is frozen the moment the image is built.
Dakota writes it in elements/oci/os-release.bst with IMAGE_REF hardcoded as ostree-image-signed:docker://ghcr.io/projectbluefin/dakota .
Bluefin LTS writes it in build_scripts/90-image-info.sh, constructing IMAGE_REF from ${IMAGE_VENDOR}/${IMAGE_NAME} environment variables at build time.
Both produce the same JSON shape :
{
"image-name": "<IMAGE_NAME>",
"image-ref": "ostree-image-signed:docker://ghcr.io/<vendor>/<name>",
"image-flavor": "main",
"image-vendor": "<vendor>",
"image-tag": "stable/testing"
}
The image-ref field encodes the base OCI registry path without a tag — it's a signing/transport hint, not the full upgrade target reference. The image-tag field (stable, testing, latest) is what tooling uses to determine channel.
Consumers of image-info.json:
| Consumer | Fields Used | Purpose |
|---|---|---|
| bctl System screen | image-name, image-tag, image-ref | Image row, Channel label, signing indicator |
ublue-image-info.sh | image-name, image-tag | Shell prompt / fastfetch display |
| Migration services | image-tag (indirectly) | Variant detection |
bootc itself does not read image-info.json during upgrades.
Runtime: bootc status and Upgrade Target Resolution#
Bootc stores the tracked image reference in the ostree origin file. This is set at install time (from the podman run ... bootc install invocation, or overridden with --target-imgref) and updated whenever bootc switch <image-ref> is run .
bootc upgrade always queries the container image source stored in the origin to check for updates — it does not consult image-info.json. The key bootc status --json fields for understanding resolution:
| JSON Path | Meaning |
|---|---|
.status.booted.image.image.image | Full image ref bootc currently tracks (registry + tag) |
.status.booted.image.imageDigest | Digest of the currently-booted image |
.status.staged | Non-null if a new deployment is staged for next boot |
The tag component of the tracked ref (e.g., :stable, :testing) determines which tag is pulled on the next bootc upgrade. Changing channel = changing the tag via bootc switch.
Upgrade Mechanics#
bootc upgrade without flags always fetches from the image source :
- If the staged deployment already matches the latest available, it is unlocked for application.
- If a newer version exists, the staged deployment is replaced.
--download-onlystages the new deployment without queuing it for application on reboot.--from-downloadedapplies an already-staged deployment without re-querying the image source — useful for applying a specific known-good version.
Check the effective upgrade target at any time:
sudo bootc status --verbose
The Image: line in the output shows the full registry reference bootc will use for the next upgrade.
Post-Switch Source Divergence#
After running bootc switch but before rebooting into the new image, the two sources are out of sync :
image-info.jsonstill reflects the old build-time tag — the booted image has not changed.bootc status .status.booted.image.image.imagereflects the previously-booted image (not yet the switch target).- The staged deployment holds the new image ref, visible in
.status.staged.
bctl reads channel from image-info.json for the Image card and from bootc status booted ref for the Updates card — using different sources — meaning these can show different channels until the next reboot .
The canonical rule: image-info.json is authoritative for identity display; the ostree origin (read via bootc status) is authoritative for upgrade targeting.
This divergence is expected and by design. The system is consistent after reboot.
Migration: Reading bootc status to Determine Switch Target#
Automated migration services (e.g., bluefin-lts-migration.service) use bootc status --format=json — not image-info.json — to determine the current booted image before deciding which target to switch to . This illustrates the correct pattern for any tooling that needs to act on the actual running image ref.
Execution flow :
- Check for a stamp file (e.g.,
/etc/bluefin-lts-migrated) — exit early if already migrated. - Parse
bootc status --format=jsonto read the current booted image ref. - Map the detected variant to the correct target image.
- Run
bootc switch --enforce-container-sigpolicy <new-image>— staged, non-destructive until reboot. - On success, write the stamp file and disable the retry timer.
--enforce-container-sigpolicy is the recommended flag for all programmatic switch operations — it verifies image signatures and replaces the deprecated --target-no-signature-verification flag .
Key References#
| Resource | Link |
|---|---|
| bootc upgrade docs | bootc-dev.github.io/bootc/upgrades.html |
| bootc JSON schema | bootc-dev.github.io/bootc/host-v1.schema.json |
Dakota os-release.bst | |
Bluefin LTS 90-image-info.sh | |
| bctl System Status | |
| Bootc Switching and Deployment |