Skip to content

Latest commit

 

History

History
617 lines (465 loc) · 19.6 KB

File metadata and controls

617 lines (465 loc) · 19.6 KB

Lab 2: Building Your Custom OS with BlueBuild

Overview

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.

Time Required

75-90 minutes

Learning Objectives

  • 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

Understanding Bluefin and BlueBuild

What is Bluefin?

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

What is BlueBuild?

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

The Build Process

recipe.yml (your config)
        ↓
GitHub Actions (build automation)
        ↓
Container Build (using Fedora tooling)
        ↓
Custom OS Image (bootable ISO + OCI image)
        ↓
Your Computer (atomic updates)

Step 1: Create Your BlueBuild Repository

1.1 Use the BlueBuild Template

  1. Go to https://github.com/blue-build/template
  2. Click "Use this template" → "Create a new repository"
  3. 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)
  4. Click "Create repository"

1.2 Clone Your Repository

git clone https://github.com/YOUR_USERNAME/bluefin-custom.git
cd bluefin-custom

1.3 Explore the Template Structure

ls -la

You 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

Step 2: Set up 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.private

Otherwise run through these steps:

  1. Open your repository’s Settings on https://www.github.com
  2. From the sidebar, pick “Secrets and Variables” and “Actions”.
  3. Create a new repository secret called SIGNING_SECRET and 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.

Step 3: Understanding recipe.yml

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/

3.1 Basic Structure

---
# 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

3.2 Key Concepts

Step 4: Customize Your Recipe

Let's create a simple but functional custom image.

4.1 Edit recipe.yml

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:
        - wireshark

Your recipes directory structure should now look like this:

recipes
├── features
│   └── wireshark.yml
└── recipe.yml

4.2 Understanding This Recipe

  • 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

Step 5: Add Configuration Files

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.

5.1 Create a Message of the Day

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                       ║
║                                                          ║
╚══════════════════════════════════════════════════════════╝
EOF

Note: Replace YOUR_USERNAME with your actual GitHub username.

Step 6: Configure GitHub Actions

The template includes a GitHub Actions workflow which should just work, but let's verify it's configured correctly.

6.1 Review the Build Workflow

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

6.2 What This Does

  • 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.

Step 7: Commit and Push

7.1 Review Your Changes

git status

You should see:

  • Modified: recipes/recipe.yml and cosign.pub
  • New files: files/system/etc/motd and recipes/features/wireshark.yml

7.2 Commit Your Changes

git add .
git commit -m "Add custom Bluefin configuration

- Add Wireshark package
- Add MOTD banner
"

7.3 Push to GitHub

git push

Step 8: Monitor the Build

8.1 Watch GitHub Actions

  1. Go to your repository on GitHub: https://github.com/YOUR_USERNAME/bluefin-custom
  2. Click the "Actions" tab
  3. You should see a workflow run in progress
  4. Click on the workflow run to see details

Expected time: 10-20 minutes for the first build.

8.2 Check Build Progress

You can watch the logs in real-time. Look for:

Building image...
Installing packages: wireshark...
Copying files...
Generating ISO...
Build complete!

Step 9: Debugging Build Failures

If your build fails, don't panic! Here's how to debug:

9.1 Common Issues

Package Not Found

Error: No match for argument: wireshark

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

9.2 Debugging Process

  1. Read the Error: Click on the failed step in GitHub Actions
  2. Find the Root Cause: Scroll to the first error message
  3. Fix Locally: Make the fix in your local repository
  4. Test YAML: Validate your YAML syntax
  5. Commit and Push: Push the fix to trigger a new build
  6. Retry: Watch the new build

9.4 Manual Testing (Advanced)

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.yml

This is optional and requires Docker/Podman installed locally.

Step 10: Build artifacts

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-bluefin

Understanding What You Built

Let's break down what you've accomplished:

The Recipe

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

The Build Process

GitHub Actions automatically:

  1. Checks out your code
  2. Builds a container image
  3. Publishes to container registry

The Result

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

Key Takeaways

  • 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

Going Further (Optional)

Want to try more? Here are ideas:

Add More Packages

install:
  - wireshark
  - vim
  - htop
  - git-delta
  - bat

Add a Custom Script

See: 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 here

Reference in recipe.yml:

- type: script
  scripts:
    - setup.sh

Add Flatpaks

- type: default-flatpaks
  notify: true
  system:
    install:
      - org.mozilla.firefox
      - com.visualstudio.code

Troubleshooting Reference

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

Verification Checklist

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

Next Steps

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