In this lab, you'll create your first custom operating system image using BlueBuild. You'll define your OS as code, add custom packages, include configuration files, and use GitHub Actions to build a bootable ISO.
75-90 minutes
- Understand Bluefin and BlueBuild architecture
- Create a BlueBuild repository from template
- Write a recipe.yml to define your OS
- Add RPM packages and configuration files
- Set up automated builds with GitHub Actions
- Debug build failures
- Download and verify your custom ISO
Bluefin is a custom Fedora Atomic Desktop image that provides:
- Immutable OS: Base system is read-only and atomic
- Container-native: Built for cloud-native development
- Developer-focused: Includes modern dev tools and workflows
- Automatic updates: Images updated automatically from upstream
BlueBuild is a tool for creating custom Bluefin images:
- Define your OS in YAML files
- Build custom images based on existing ones (like Bluefin)
- Automated builds via GitHub Actions
- Distribute as bootable ISOs or container images
recipe.yml (your config)
↓
GitHub Actions (build automation)
↓
Container Build (using Fedora tooling)
↓
Custom OS Image (bootable ISO + OCI image)
↓
Your Computer (atomic updates)
- Go to https://github.com/blue-build/template
- Click "Use this template" → "Create a new repository"
- Configure your repository:
- Owner: Your GitHub username
- Repository name:
bluefin-custom(or your preferred name) - Description: "My custom Bluefin image"
- Visibility: Public (required for free GitHub Actions)
- Click "Create repository"
git clone https://github.com/YOUR_USERNAME/bluefin-custom.git
cd bluefin-customls -laYou should see:
├── .github/
│ └── workflows/
│ └── build.yml # GitHub Actions build workflow
├── config/
│ ├── files/ # Static files to include in image
│ └── scripts/ # Scripts to run during build
├── recipes/
│ └── recipe.yml # Your OS definition
├── README.md
└── cosign.pub # For image signing
You will need to set up signing before you can proceed with the build process - otherwise your GitHub Actions will fail later! The full guide can be found here: https://blue-build.org/how-to/cosign/
However for this workshop, once you have cloned your repository, you should be able to run the following in the repo root to complete the process:
cosign generate-key-pair
# Do NOT put in a password when it asks you to, just press enter.Now add the private key to your GitHub repository. If you have the gh command line tool configured, you can just run:
gh secret set SIGNING_SECRET < cosign.key # or cosign.privateOtherwise run through these steps:
- Open your repository’s Settings on https://www.github.com
- From the sidebar, pick “Secrets and Variables” and “Actions”.
- Create a new repository secret called
SIGNING_SECRETand for its value copy the contents of your cosign.key file.
IMPORTANT cosign.key is excluded from the repository using the default .gitignore file - although we're creating a copy of it by virtue of creating the Secret in GitHub, you should also securely back this up in case you ever need to recreate this image or build from scratch with the same public key.
Open recipes/recipe.yml in your text editor. This file defines your entire OS.
More details on recipe.yml can be found here: https://blue-build.org/reference/recipe/
---
# yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json
# image will be published to ghcr.io/<user>/<name>
name: template
# description will be included in the image's metadata
description: This is my personal OS image.
# the base image to build on top of (FROM) and the version tag to use
base-image: ghcr.io/ublue-os/silverblue-main
image-version: 42 # latest is also supported if you want new updates ASAP
# module configuration, executed in order
# you can include multiple instances of the same module
modules:
- type: files
files:
- source: system
destination: / # copies files/system/* (* means everything inside it) into your image's root folder /
- type: dnf
repos:
copr:
- atim/starship
install:
packages:
- micro
- starship
remove:
packages:
# example: removing firefox (in favor of the flatpak)
# "firefox" is the main package, "firefox-langpacks" is a dependency
- firefox
- firefox-langpacks # also remove firefox dependency (not required for all packages, this is a special case)
- type: default-flatpaks
configurations:
- notify: true # Send notification after install/uninstall is finished (true/false)
scope: system
# If no repo information is specified, Flathub will be used by default
install: # system flatpaks we want all users to have and not remove
- org.mozilla.firefox
- org.gnome.Loupe
- scope: user # Also add Flathub user repo, but no user packages
- type: signing # this sets up the proper policy & signing files for signed images to work fully- Base Image: What you're building on top of (Bluefin in our case)
- Modules: Building blocks that customize your image
- dnf: Manages RPM packages included in the build (see: https://blue-build.org/reference/modules/dnf/)
- files: Static configuration files to include (see: https://blue-build.org/reference/modules/files/)
- default-flatpaks: Flatpaks to install on the build (see: https://blue-build.org/reference/modules/default-flatpaks/)
Let's create a simple but functional custom image.
Replace the contents of recipes/recipe.yml with:
---
# yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json
# image will be published to ghcr.io/<user>/<name>
name: my-custom-bluefin
# description will be included in the image's metadata
description: This is my personal OS image.
# the base image to build on top of (FROM) and the version tag to use
base-image: ghcr.io/ublue-os/bluefin
image-version: stable # latest is also supported if you want new updates ASAP
# module configuration, executed in order
# you can include multiple instances of the same module
modules:
- from-file: features/wireshark.yml
- type: files
files:
- source: system
destination: / # copies files/system/* (* means everything inside it) into your image's root folder /
- type: dnf
repos:
copr:
- atim/starship
install:
packages:
- micro
- starship
- gopass # We'll need this for lab 4
- age # We'll need this for lab 4
- chezmoi # We'll need this for lab 5
remove:
packages:
# example: removing firefox (in favor of the flatpak)
# "firefox" is the main package, "firefox-langpacks" is a dependency
- firefox
- firefox-langpacks # also remove firefox dependency (not required for all packages, this is a special case)
- type: default-flatpaks
configurations:
- notify: true # Send notification after install/uninstall is finished (true/false)
scope: system
# If no repo information is specified, Flathub will be used by default
install: # system flatpaks we want all users to have and not remove
- org.mozilla.firefox
- org.gnome.Loupe
- scope: user # Also add Flathub user repo, but no user packages
- type: signing # this sets up the proper policy & signing files for signed images to work fully
Next create a directory called features under the recipes/ directory, containing the following which will add wireshark to our build. Notice how we've included this file in the above recipe using the from-file: directive.
A quick note on why we'd do this. When you customize your Bluefin based OS, you want to take a measured approach. You don't want to install every package under the sun as you'll just create a bloated image. However certain packages like Wireshark cannot capture packets unless they are installed in an RPM as part of the build. There is a Flatpak version of Wireshark, but it can only be used to view capture files.
You can install RPM's in Bluefin at runtime using the rpm-ostree command, but this is considered an anti-pattern (for more details, see https://lwn.net/Articles/954059/). In the user space, the ideal solutions for application management are either to use Flatpaks, or to make use of toolbox or distrobox to run your preferred tools. However there will always be certain tools which require OS level permissions or drivers that it is advantageous to build into your base image.
Part of the anti-pattern is also that the whole goal of doing this is not having to perform a whole load of customizations when you install your OS afresh.
Back to the build, create the recipes/features/wireshark.yml file to install the wireshark package:
# yaml-language-server: $schema=https://schema.blue-build.org/module-stage-list-v1.json
modules:
- type: dnf
install:
packages:
- wiresharkYour recipes directory structure should now look like this:
recipes
├── features
│ └── wireshark.yml
└── recipe.yml
- Base: We start with the standard Bluefin image (current stable release)
- RPM Install: We add Wireshark package by including a new YAML file
- Files: We include a custom file (we'll create this next)
- Signing: Enables image signing for verification
We know from the base recipe.yml file that we already have the construct we need to copy files across during the build:
- type: files
files:
- source: system
destination: / # copies files/system/* (* means everything inside it) into your image's root folder /As a result, we can drop whatever files we want copied over into <REPO_ROOT>/files/system/. Let's create a custom MOTD file as an example to demonstrate this.
Create files/system/etc/motd:
cat > files/system/etc/motd << 'EOF'
╔══════════════════════════════════════════════════════════╗
║ ║
║ WORKSTATION AS CODE - CFGMGMTCAMP 2026 ║
║ ║
║ This system is defined entirely as code. ║
║ Configuration: https://github.com/YOUR_USERNAME/ ║
║ ║
║ To make changes: ║
║ 1. Edit recipe.yml in your Git repository ║
║ 2. Push to GitHub ║
║ 3. GitHub Actions builds new image ║
║ 4. System updates automatically ║
║ ║
╚══════════════════════════════════════════════════════════╝
EOFNote: Replace YOUR_USERNAME with your actual GitHub username.
The template includes a GitHub Actions workflow which should just work, but let's verify it's configured correctly.
Open .github/workflows/build.yml. This file defines the automated build process.
Key parts:
name: bluebuild
on:
schedule:
- cron:
"00 06 * * *" # build at 06:00 UTC every day
# (20 minutes after last ublue images start building)
push:
paths-ignore: # don't rebuild if only documentation has changed
- "**.md"
pull_request:
workflow_dispatch: # allow manually triggering builds
concurrency:
# only run one build at a time
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
cancel-in-progress: true
jobs:
bluebuild:
name: Build Custom Image
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write
strategy:
fail-fast: false # stop GH from cancelling all matrix builds if one fails
matrix:
recipe:
# !! Add your recipes here
- recipe.yml # <-- You can have multiple recipes
steps:
# the build is fully handled by the reusable github action
- name: Build Custom Image
uses: blue-build/github-action@v1.10
with:
recipe: ${{ matrix.recipe }}
cosign_private_key: ${{ secrets.SIGNING_SECRET }}
registry_token: ${{ github.token }}
pr_event_number: ${{ github.event.number }}
# enabled by default, disable if your image is small and you want faster builds
maximize_build_space: true- Trigger (on:): Runs every day at 6am, on every push to main, PRs, or manual trigger
- Environment: Runs on GitHub-hosted Ubuntu runners
- Permissions: Needs package write access to publish images
- Blue-build Action: Uses the official BlueBuild action to build your image
- recipe: You can specify multiple recipes - for example you can have a customized base image, and a customized image with the nVidia drivers.
You don't need to modify this file for basic usage.
git statusYou should see:
- Modified:
recipes/recipe.ymlandcosign.pub - New files:
files/system/etc/motdandrecipes/features/wireshark.yml
git add .
git commit -m "Add custom Bluefin configuration
- Add Wireshark package
- Add MOTD banner
"git push- Go to your repository on GitHub:
https://github.com/YOUR_USERNAME/bluefin-custom - Click the "Actions" tab
- You should see a workflow run in progress
- Click on the workflow run to see details
Expected time: 10-20 minutes for the first build.
You can watch the logs in real-time. Look for:
Building image...
Installing packages: wireshark...
Copying files...
Generating ISO...
Build complete!
If your build fails, don't panic! Here's how to debug:
Package Not Found
Error: No match for argument: wireshark
- The package name might be wrong
- Check Fedora package search: https://packages.fedoraproject.org/
YAML Syntax Error
Error: Invalid YAML syntax
- Check indentation (use spaces, not tabs)
- Verify all colons and dashes are correct
- Use a YAML validator: https://www.yamllint.com/
File Not Found
Error: files/system/etc/motd: No such file or directory
- Verify the file exists in your repository
- Check the path in recipe.yml matches the actual file location
- Make sure you committed and pushed the file
- Read the Error: Click on the failed step in GitHub Actions
- Find the Root Cause: Scroll to the first error message
- Fix Locally: Make the fix in your local repository
- Test YAML: Validate your YAML syntax
- Commit and Push: Push the fix to trigger a new build
- Retry: Watch the new build
You can test the build locally with Docker:
# Install BlueBuild CLI
# See: https://blue-build.org/learn/getting-started/#installation
# Build locally
bluebuild build recipes/recipe.ymlThis is optional and requires Docker/Podman installed locally.
The build process does not generate an ISO file - hence one of the pre-requisites for this lab was to download one of the existing builds of Bluefin Linux. However the build does produce an OCI image, which is published to GitHub Container Registry:
ghcr.io/YOUR_USERNAME/my-custom-bluefin:latest
This is what your system will use for automatic updates.
Also note that the image name is taken from here in recipes/recipe.yml:
---
# yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json
# image will be published to ghcr.io/<user>/<name>
name: my-custom-bluefinLet's break down what you've accomplished:
Your recipe.yml is a declarative definition of your OS:
- Declarative: "My system should have Wireshark" not "Install Wireshark"
- Version Controlled: Every change is tracked in Git
- Reproducible: Anyone can build the exact same image
GitHub Actions automatically:
- Checks out your code
- Builds a container image
- Publishes to container registry
You now have:
- A custom Bluefin Linux image with your packages
- Automatic builds on every Git push (and daily at 6am which will automatically pick up updates from the upstream image)
- A container image for atomic updates
- BlueBuild lets you define your OS as code in YAML
- GitHub Actions automates the build process
- rpm-ostree manages packages on atomic systems
- Configuration files can be included directly in the image
- Builds are reproducible and version-controlled
- Debugging is straightforward with GitHub Actions logs
Want to try more? Here are ideas:
install:
- wireshark
- vim
- htop
- git-delta
- batSee: https://blue-build.org/reference/modules/script/
Create files/scripts/setup.sh:
#!/bin/bash
# Run during image build
echo "Running custom setup..."
# Add your logic hereReference in recipe.yml:
- type: script
scripts:
- setup.sh- type: default-flatpaks
notify: true
system:
install:
- org.mozilla.firefox
- com.visualstudio.code| Issue | Solution |
|---|---|
| Build timeout | Reduce number of packages or use a faster runner |
| Package not found | Check package name at packages.fedoraproject.org |
| YAML error | Validate YAML syntax, check indentation |
| File not copied | Verify file path and that it's committed to Git |
Before moving to the next lab, verify:
- Repository created from BlueBuild template
- recipe.yml edited with Wireshark and custom files
- Configuration files created in
files/ - Changes committed and pushed to GitHub
- GitHub Actions build completed successfully
- Container image published to GitHub Container Registry
- You understand how to debug build failures
Congratulations! You've built your first custom OS image. In the next lab, we'll install it in a VM and boot from it to see our customizations in action.
Previous: Lab 1: Introduction and Concepts Next: Lab 3: Installing and Running Your Custom Image