A template for building custom bootc operating system images based on the lessons from Universal Blue and Bluefin. It is designed to be used manually, but is optimized to be bootstraped by GitHub Copilot. After set up you'll have your own custom Linux.
This template uses the multi-stage build architecture from @projectbluefin/distroless, combining resources from multiple OCI containers for modularity and maintainability. See the Architecture section below for details.
Unlike previous templates, you are not modifying Bluefin and making changes.: You are assembling your own Bluefin in the same exact way that Bluefin, Aurora, and Bluefin LTS are built. This is way more flexible and better for everyone since the image-agnostic and desktop things we love about Bluefin lives in @projectbluefin/common.
Instead, you create your own OS repository based on this template, allowing full customization while leveraging Bluefin's robust build system and shared components.
Be the one who moves, not the one who is moved.
Here are the changes from [Base Image Name]. This image is based on [Bluefin/Bazzite/Aurora/etc] and includes these customizations:
- System packages:
tmuxandgum— tmux is the template's package-manager cache smoke test, and gum provides the interactive prompts used by the default ujust recipes.
- CLI Tools (Homebrew): neovim, helix - [brief explanation]
- GUI Apps (Flatpak): Spotify, Thunderbird - [brief explanation]
- List anything removed from base image
- Any systemd services enabled/disabled
- Desktop environment changes
- Other notable modifications
Last updated: [date]
Replace the placeholders above with your actual customizations whenever you add or remove packages, apps, or configuration. This section is what tells users how your image differs from the base.
This template works best with phased prompts that let Copilot bootstrap your image in three stages.
Use this prompt first to get your fork building:
Bootstrap a new custom OS from @projectbluefin/finpilot. Name it after this repository. Use the `finpilot-onboarding` skill first, then:
1. Rename `finpilot` in the 7 required files
2. Enable GitHub Actions and set RENOVATE_TOKEN (classic PAT with `repo` +
`workflow` scopes, or a fine-grained token with **Dependabot alerts:
Read-only** and **Contents: Read and write**)
3. Configure branch protection for `main` with `validate` as a required status check
4. Enable auto-merge
5. Trigger the first green build on `main`
6. Add the "What Makes this Raptor Different" section to README.md (with placeholders)
Once the first build is green, use this prompt to add packages:
Use the `finpilot-packages` and `finpilot-custom` skills, then:
1. Add one system package to the image in `build/10-build.sh`
2. Add one CLI tool to `custom/brew/default.Brewfile`
3. Add one GUI app to `custom/flatpaks/default.preinstall`
4. Add shortcuts in `custom/ujust/custom-apps.just` to install them
5. Update the README "What Makes this Raptor Different" section with the new entries
6. Run `just build && just build-qcow2 && just run-vm-qcow2` to verify locally
7. Open a PR and merge once `validate` passes
When you are ready for production, use this prompt to harden the setup:
Use the `finpilot-maintain` and `finpilot-ci` skills, then:
1. Verify keyless image signing works: cosign verify --certificate-identity-regexp="https://github.com/USER/REPO/.github/workflows/" --certificate-oidc-issuer="https://token.actions.githubusercontent.com" ghcr.io/USER/REPO:stable
2. Follow the maintenance schedule in the `finpilot-maintain` skill
- Automated builds via GitHub Actions on every commit
- Self-hosted Renovate for automated dependency updates
- Automatic cleanup of old images (90+ days) to keep it tidy
- Pull request workflow - test changes before merging to main
- PRs build and validate before merge
mainbuilds:stable-testing; merging the auto-opened promotion PR tostablepublishes:stable
- Validates your files on pull requests so you never break a build:
- Brewfile, Justfile, ShellCheck, Renovate config, and it'll even check to make sure the flatpak you add exists on FlatHub
- Production Grade Features
- Container signing with keyless OIDC
- Pre-configured Brewfiles for easy package installation and customization
- Includes curated collections: development tools, fonts, CLI utilities. Go nuts.
- Users install packages at runtime with
brew bundle, aliased to premadeujust commands - See custom/brew/README.md for details
- Ship your favorite flatpaks
- Automatically installed on first boot after user setup
- See custom/flatpaks/README.md for details
- User-friendly command shortcuts via
ujust - Pre-configured examples for app installation and system maintenance for you to customize
- See custom/ujust/README.md for details
- Modular numbered scripts (10-, 20-, 30-) run in order
- Example scripts included for third-party repositories and desktop replacement
- Helper functions for safe COPR usage
- See build/README.md for details
Click "Use this template" to create a new repository from this template.
Important: Change finpilot to your repository name in these 7 files:
Containerfile(# Name:comment andARG IMAGE_NAME):# Name: your-repo-nameJustfile(export IMAGE_NAME := env("IMAGE_NAME", ...)):your-repo-nameREADME.md(title):# your-repo-nameartifacthub-repo.yml(repositoryID):repositoryID: your-repo-namecustom/ujust/README.md(bootc switch example):localhost/your-repo-name:stable.github/workflows/clean.yml(packages):packages: your-repo-nameiso/iso.toml(bootc switch URL):ghcr.io/YOUR_USERNAME/your-repo-name:stable
- Go to the "Actions" tab in your repository
- Click "I understand my workflows, go ahead and enable them"
Your first build will start automatically!
Note: Images are signed automatically with keyless OIDC signing — no keys or secrets to configure. See "Image Signing" below for details.
Renovate automatically updates dependencies and GitHub Actions (including workflow files). This template uses a self-hosted Renovate runner via projectbluefin/actions.
One-time setup:
- Go to GitHub → Settings → Developer settings → Personal access tokens → Tokens (classic)
- Click Generate new token (classic)
- Set a note like
renovate-finpilot - Select scopes:
repo(full control) andworkflow(update workflows) - Click Generate token and copy the value
- Go to your repository → Settings → Secrets and variables → Actions
- Add a new secret:
RENOVATE_TOKEN(paste the token value) - Enable Settings → General → Pull Requests → Allow auto-merge so Renovate can merge low-risk updates after checks pass
- Configure branch protection for
main(required for automerge to work):- Go to Settings → Branches → Add rule
- Set Branch name pattern to
main - Enable "Require a pull request before merging"
- Enable "Require status checks to pass before merging"
- Add
validateas a required status check - Enable "Require branches to be up to date before merging" (recommended)
Renovate will run every 6 hours and on config changes. It pins GitHub Actions to SHAs and updates tracked image digests automatically.
Repositories created with Use this template are independent repositories.
Renovate keeps pinned dependencies current, but it does not copy arbitrary
changes from finpilot's Containerfile, build scripts, or workflows.
For a template improvement or build-system change, file a scoped finpilot issue instead of merging unrelated histories:
- Select "Opt in to a clanker working on my issue" when creating your own
issue to send it directly to
3-clanker-queue. - Maintainers can move any accepted issue to
3-clanker-queue. - A Hive-connected agent opens a focused pull request; humans review and merge it.
Review and port structural changes into your custom image deliberately through a pull request. This preserves your image-specific changes while sharing improvements with every future finpilot user.
Choose your base image in Containerfile (the FROM line):
FROM quay.io/fedora-ostree-desktops/silverblue:44@sha256:...Finpilot layers on top of Fedora Silverblue, not Bluefin. Bluefin's desktop
configuration is provided by @projectbluefin/common earlier in the build.
Add your packages in build/10-build.sh:
dnf5 install -y package-nameCustomize your apps:
- Add Brewfiles in
custom/brew/(guide) - Add Flatpaks in
custom/flatpaks/(guide) - Add ujust commands in
custom/ujust/(guide)
All changes should be made via pull requests:
- Open a pull request on GitHub with the change you want.
- The PR will automatically trigger:
- Build validation
- Brewfile, Flatpak, Justfile, and shellcheck validation
- Test image build
- Once checks pass, merge the PR
- Merging to
mainpublishes a:stable-testingimage; the promotion PR it opens publishes:stablewhen merged
The template uses a two-branch release model:
| Branch | Image tag | Audience |
|---|---|---|
main |
:stable-testing (+ :testing) |
Testers and release candidates |
stable |
:stable |
Production systems |
When stable differs from main, the promote-main-to-stable workflow opens a squash promotion PR automatically, enables auto-merge, and runs a release gate that verifies image signatures on :testing. Direct pushes to stable are not part of the workflow; hotfixes made there are merged back into main by sync-stable-to-main.
For the automated promotion PR to open, your repository needs:
- An organization-owned repo with a
maintainersteam — the workflow requests review from<owner>/maintainerswhen creating the PR. Personal-account forks can replace.github/workflows/promote-main-to-stable.ymlwith a local version that skips reviewer requests. - Branch protection on
stable: 0 required approvals means fully automatic promotion; 1 approval means review, then auto-merge. - The release gate is advisory by default — make the promote workflow a required check on
stableif arelease/blockedresult should block merging.
Test the candidate from main first:
sudo bootc switch --transport registry ghcr.io/your-username/your-repo-name:stable-testing
sudo systemctl rebootAfter merging the promotion PR, deploy production:
sudo bootc switch --transport registry ghcr.io/your-username/your-repo-name:stable
sudo systemctl rebootImages are signed automatically with keyless OIDC signing via Cosign and GitHub Actions. No manual key generation, cosign.key, or cosign.pub files are required — the signature is created using GitHub's OIDC token via Fulcio during each build.
- Verify image authenticity and integrity
- Prevent tampering and supply chain attacks
- Required for some enterprise/security-focused deployments
- Industry best practice for production images
- Required for promotion: the
main → stablerelease gate verifies signatures on:testingand blocks promotion of unsigned images
cosign verify \
--certificate-identity-regexp="https://github.com/your-username/your-repo-name/.github/workflows/" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
ghcr.io/your-username/your-repo-name:stableTo disable, comment out the Sign and publish step in .github/workflows/build-image.yml. Be aware that unsigned images will fail the promotion release gate, so main → stable promotions will report release/blocked until signing is re-enabled.
Ready to take your custom OS to production? Enable these features for enhanced security, reliability, and performance:
-
Verify Image Signing
- Provides cryptographic verification of your images
- Prevents tampering and ensures authenticity
- Uses keyless OIDC signing via GitHub Actions — no keys or secrets required
- Verify it works with the
cosign verifycommand in the "Image Signing" section above
-
Enable Image Rechunking (Recommended)
- Optimizes bootc image layers for better update performance
- Improves download resumability with evenly sized layers
- Set
ENABLE_RECHUNKING: "true"in.github/workflows/build-image.yml - Uses OCI-native chunkah;
/usr/libexec/bootc-base-imagectlis not required - Status: Not enabled by default (optional optimization)
The old rechunking recipe used /usr/libexec/bootc-base-imagectl, which is absent from many Universal Blue images. Do not copy that recipe or install a legacy rechunker: its layer format is not a safe migration path to the current implementation.
Finpilot instead uses the OCI-native bootc-build/chunka action. The action runs chunkah from a pinned container and replaces the locally built image before the existing tag and push steps. The default Fedora Silverblue-based finpilot image is RPM-based, so chunkah can discover components from its RPM database without bootc-base-imagectl.
To enable it, change the workflow environment value:
env:
ENABLE_RECHUNKING: "true"
RECHUNK_MAX_LAYERS: "128"Rechunking runs only for publish builds, not pull requests. It requires additional runner time and temporary storage. Keep ENABLE_RECHUNKING set to "false" if those costs are more important than smaller OTA deltas.
Custom base images: This switch is supported for the template's default RPM-based image. BuildStream-produced images strip the component xattrs chunkah needs and require an xattr-manifest; changing to one of those images is not a one-line setup. See the action's xattr-manifest input before replacing the default base.
Optional package cadence: Basic rechunking does not require package cadence data. Advanced deployments can run bootc-build/apply-pkg-intervals before rechunking and maintain files/pkg-intervals.tsv with the reusable package-cadence workflow. That workflow requires a repository GitHub App ID and private key, so configure it separately rather than treating it as part of basic enablement.
References:
Your workflow will:
- Sign all images using keyless OIDC signing
- Provide cryptographic proof of authenticity via SLSA build provenance attestation
- Homebrew/Brewfiles - Runtime package management
- Flatpak Preinstall - GUI application setup
- ujust Commands - User convenience commands
- Build Scripts - Build-time customization
This template follows the multi-stage build architecture from @projectbluefin/distroless, as documented in the Bluefin Contributing Guide.
Stage 1: Context (ctx) - Combines resources from multiple sources:
- Local build scripts (
/build) - Local custom files (
/custom) - @projectbluefin/common - Desktop configuration shared with Aurora (includes branding/artwork content)
- @ublue-os/brew - Homebrew integration
Stage 2: Base Image - Default options:
quay.io/fedora-ostree-desktops/silverblue:44(Fedora-based GNOME desktop, default)quay.io/centos-bootc/centos-bootc:stream10(CentOS-based alternative)
- Modularity: Compose your image from reusable OCI containers
- Maintainability: Update shared components independently
- Reproducibility: Renovate automatically updates OCI tags to SHA digests
- Consistency: Share components across Bluefin, Aurora, and custom images
The template imports files from these OCI containers at build time:
COPY --from=ghcr.io/projectbluefin/common:latest /system_files /oci/common
COPY --from=ghcr.io/ublue-os/brew:latest /system_files /oci/brewYour build scripts can access these files at:
/ctx/oci/common/- Shared desktop configuration (branding/artwork content lives insidecommon)/ctx/oci/brew/- Homebrew integration files
Note: Renovate automatically updates :latest tags to SHA digests for reproducible builds.
Test your changes before pushing:
just build # Build container image
just build-qcow2 # Build VM disk image
just run-vm-qcow2 # Test in browser-based VMThis template provides security features for production use:
- Image signing with keyless OIDC cosign for cryptographic verification
- Automated security updates via Renovate
- Build provenance tracking
Signing and Renovate run automatically; see the "Love Your Image? Let's Go to Production" section above for optional production hardening like rechunking.
Flatpaks are installed on first boot via flatpak-preinstall.service, not during bootc switch. Ensure:
- Internet is available on first boot
flatpak-preinstall.servicecompletes (systemctl status flatpak-preinstall.service)- Wait until the service finishes before checking for flatpaks
The adw-gtk3-dark runtime is not available on Flathub. These warnings are cosmetic and do not prevent other flatpaks from installing. To suppress, remove adw-gtk3-dark from your flatpak list in custom/flatpaks/.
Homebrew is pre-staged at build time (tarball + systemd services). The brew-setup.service extracts it on first boot. If you don't see brew, verify brew-setup.service ran (systemctl status brew-setup.service) and that your Containerfile includes the Brew integration.