bootc-docs/bootc-base-imagectl.md
robojerk bd4c3e746f
Some checks failed
Test bootc Documentation / test-base-image (push) Failing after 38s
Test bootc Documentation / test-documentation (push) Failing after 24s
Enhance bootc-base-imagectl documentation with technical details
- Add comprehensive technical implementation details based on actual Python source
- Document all command-line options and usage patterns
- Add detailed examples for build-rootfs, rechunk, and list commands
- Include practical Containerfile examples for different use cases
- Document external dependencies and error handling
- Add security considerations and cleanup procedures
- Provide cross-build examples and advanced workflows

Based on analysis of the actual bootc-base-imagectl Python script from
https://gitlab.com/fedora/bootc/base-images/-/raw/main/bootc-base-imagectl
2025-09-15 14:44:47 -07:00

13 KiB

bootc-base-imagectl

A core premise of the bootc model is that rich control over Linux system customization can be accomplished with a "default" container build:

FROM <base image>
RUN ...

As of recently, it is possible to e.g. swap the kernel and other fundamental components as part of default derivation.

However, some use cases want even more control - for example, as an organization deploying a bootc system, I may want to ensure the base image version carries a set of packages at exactly specific versions (perhaps defined by a lockfile, or an rpm-md repository). There are many tools which manage snapshots of yum (rpm-md) repositories.

There are currently issues where it won't quite work to e.g. dnf -y upgrade selinux-policy-targeted.

The /usr/libexec/bootc-base-imagectl tool which is included in the base image is designed to enable building a root filesystem in ostree-container format from a set of RPMs controlled by the user.

Architecture Overview

The bootc-base-imagectl tool is a Python script that provides three main operations:

  1. build-rootfs - Generate a container root filesystem from RPM packages
  2. rechunk - Split container images into reproducible, chunked layers
  3. list - Enumerate available build configurations

The tool uses rpm-ostree under the hood to compose root filesystems from RPM packages, with support for custom manifests, package locking, and reproducible builds.

Understanding the base image content

Most, but not all content from the base image comes from RPMs. There is some additional non-RPM content, as well as postprocessing that operates on the filesystem root. At the current time the implementation of the base image build uses rpm-ostree, but this is considered an implementation detail subject to change.

Using bootc-base-imagectl build-rootfs

The core operation is bootc-base-imagectl build-rootfs.

Basic Usage

bootc-base-imagectl build-rootfs [OPTIONS] <source_root> <target>

Required Arguments:

  • source_root - Path to source root with /etc/yum.repos.d configuration
  • target - Path where the root filesystem will be generated (must not exist)

Advanced Options

bootc-base-imagectl build-rootfs \
  --manifest=minimal \
  --install=package1 --install=package2 \
  --add-dir=/path/to/overlay1 --add-dir=/path/to/overlay2 \
  --lock=package-1.0.0-1.x86_64 \
  --no-docs \
  --sysusers \
  --cachedir=/var/cache/dnf \
  --repo=fedora --repo=updates \
  /source/root /target/rootfs

Key Options:

  • --manifest - Select build configuration (default: "default")
  • --install - Add additional packages to install
  • --add-dir - Copy directory contents as overlay layers
  • --lock - Lock packages to specific versions (NEVRA or NEVR format)
  • --no-docs - Skip documentation packages
  • --sysusers - Use systemd-sysusers instead of hardcoded passwd/group
  • --cachedir - Cache repository metadata and RPMs
  • --repo - Enable specific repositories only
  • --reinject - Copy build configurations into target

Technical Implementation

The tool performs these steps:

  1. Repository Setup: Runs dnf repolist to refresh repository metadata
  2. Manifest Processing: Loads YAML manifest from /usr/share/doc/bootc-base-imagectl/manifests/
  3. Override Generation: Creates temporary JSON with user overrides
  4. OSTree Compose: Executes rpm-ostree compose rootfs with the manifest
  5. Post-processing: Fixes permissions and runs bootc container lint
  6. Cleanup: Removes temporary files and repositories

Using bootc-base-imagectl rechunk

This operation is strongly related to build-rootfs but is also orthogonal; it can be used on a "regular" container build as well.

Basic Usage

bootc-base-imagectl rechunk [OPTIONS] <from_image> <to_image>

Required Arguments:

  • from_image - Source image in container storage (e.g., quay.io/exampleos:build)
  • to_image - Output image in container storage (e.g., quay.io/exampleos:latest)

Advanced Options

bootc-base-imagectl rechunk \
  --max-layers=10 \
  quay.io/exampleos:build \
  quay.io/exampleos:latest

Options:

  • --max-layers - Configure maximum number of output layers

Container Usage

This command assumes it will be run as a container image, and defaults to wanting write access to the container storage.

podman run --rm --privileged \
  -v /var/lib/containers:/var/lib/containers \
  quay.io/fedora/fedora-bootc:rawhide \
  bootc-base-imagectl rechunk \
  quay.io/exampleos/exampleos:build \
  quay.io/exampleos/exampleos:latest

Technical Implementation

The rechunk operation uses rpm-ostree experimental compose build-chunked-oci:

  1. Input Processing: Reads the source container image
  2. Layer Analysis: Analyzes RPM database to determine optimal layer splits
  3. Chunking: Splits content into separate, reproducible layers
  4. Timestamp Canonicalization: Sets all timestamps to zero for reproducibility
  5. Output Generation: Creates new container image with chunked layers

Rationale

When performing a complex container derivation, there are several issues:

Replaced duplicate content

When e.g. upgrading or replacing the kernel or other large packages as part of a container build (without squashing all layers) then the old replaced content will still be present.

Removed content still present

Similarly, RUN dnf -y remove etc. will still retain that removed content in prior layers.

Timestamp drift

By default, many tools will use the current timestamp when writing files. rpm will do this (unless SOURCE_DATE_EPOCH is set), and other tools like cp and curl will as well.

This means that every build of the image will produce a new tar stream (with new timestamps) - that will get pushed to a registry and downloaded by clients, even if the content didn't actually change.

What rechunk does: split reproducible chunked images

The bootc-base-imagectl rechunk command fixes all of these issues by taking an input container, operates on its final merged filesystem tree (hence removed/overridden files are handled), and then splits it up (currently based on the RPM database) into separate layers (tarballs).

Further, because bootc uses OSTree today, and OSTree canonializes all timestamps to zero on the client side, this tool does that at build time.

Using bootc-base-imagectl list

The list command enumerates available build configurations that can be selected by passing --manifest to build-rootfs.

Usage

bootc-base-imagectl list

Output Format

minimal: Minimal bootc base image
default: Standard bootc base image with common packages
server: Server-focused bootc base image
---

Technical Implementation

The list command:

  1. Manifest Discovery: Scans /usr/share/doc/bootc-base-imagectl/manifests/ for .yaml files
  2. Filtering: Excludes .hidden.yaml files and symbolic links
  3. Metadata Extraction: Uses rpm-ostree compose tree --print-only to read manifest metadata
  4. Summary Display: Shows manifest name and description from metadata

Technical Implementation Details

Core Architecture

The bootc-base-imagectl tool is implemented as a Python 3 script that orchestrates rpm-ostree operations. The tool provides a high-level interface for:

  • Manifest Management: Loading and processing YAML treefiles
  • Package Resolution: Handling RPM dependencies and version locking
  • Overlay Support: Managing additional content layers
  • Reproducible Builds: Ensuring consistent output across builds

Key Components

1. Manifest System

  • Location: /usr/share/doc/bootc-base-imagectl/manifests/
  • Format: YAML treefiles compatible with rpm-ostree
  • Discovery: Automatic scanning for .yaml files (excluding .hidden.yaml)
  • Override Support: JSON overrides for user customizations

2. Package Management

  • Repository Handling: Uses /etc/yum.repos.d configuration from source root
  • Version Locking: Supports NEVRA (Name-Epoch-Version-Release-Architecture) format
  • Dependency Resolution: Leverages rpm-ostree's advanced dependency solver
  • Caching: Optional repository metadata and RPM caching

3. Overlay System

  • Directory Overlays: Copy additional content as OSTree overlay layers
  • Temporary Repositories: Create temporary OSTree repos for overlay content
  • Content Integration: Merge overlay content into final rootfs

4. Build Process

# Simplified build flow
def run_build_rootfs(args):
    # 1. Repository refresh
    subprocess.check_call(['dnf', 'repolist'], stdout=subprocess.DEVNULL)
    
    # 2. Manifest processing
    manifest_path = find_manifest(args.manifest)
    
    # 3. Override generation
    if user_overrides:
        create_temp_override_manifest(overrides)
    
    # 4. OSTree compose
    subprocess.run(['rpm-ostree', 'compose', 'rootfs', ...])
    
    # 5. Post-processing
    fix_permissions(target)
    subprocess.run(['bootc', 'container', 'lint', f'--rootfs={target}'])

External Dependencies

The tool requires these external commands:

  • rpm-ostree - Core OSTree composition engine
  • dnf - Package manager for repository operations
  • ostree - OSTree repository management
  • bootc - Container validation and linting

Error Handling

The implementation includes comprehensive error handling:

  • Subprocess Failures: Captures and reports command execution errors
  • File Operations: Handles temporary file creation and cleanup
  • Repository Issues: Manages OSTree repository lifecycle
  • Validation: Runs bootc container lint to ensure output validity

Security Considerations

  • Temporary Files: All temporary files are created with secure permissions
  • Repository Isolation: Uses separate temporary OSTree repositories for overlays
  • Content Validation: Validates all input manifests and configurations
  • Cleanup: Ensures all temporary resources are properly cleaned up

Cross builds and the builder image

The build tooling is designed to support "cross builds"; the repository root could e.g. be CentOS Stream 10, while the builder root is Fedora or RHEL, etc.

In other words, one given base image can be used as a "builder" to produce another using different RPMs.

Practical Examples

Example 1: Generate a new image using CentOS Stream 10 content from RHEL

FROM quay.io/centos/centos:stream10 as repos

FROM registry.redhat.io/rhel10/rhel-bootc:10 as builder
RUN --mount=type=bind,from=repos,src=/,dst=/repos,rw \
    /usr/libexec/bootc-base-imagectl build-rootfs \
    --manifest=minimal \
    /repos /target-rootfs

# This container image uses the "artifact pattern"; it has some
# basic configuration we expect to apply to multiple container images.
FROM quay.io/exampleos/baseconfig@sha256:.... as baseconfig

FROM scratch
COPY --from=builder /target-rootfs/ /
# Now we make other arbitrary changes. Copy our systemd units and
# other tweaks from the baseconfig container image.
COPY --from=baseconfig /usr/ /usr/
RUN <<EORUN
set -xeuo pipefail
# Install critical components
dnf -y install linux-firmware NetworkManager cloud-init cowsay
dnf clean all
bootc container lint
EORUN
LABEL containers.bootc 1
ENV container=oci
STOPSIGNAL SIGRTMIN+3
CMD ["/sbin/init"]

Example 2: Custom package selection with version locking

FROM quay.io/fedora/fedora-bootc:rawhide as builder

# Create custom repository configuration
RUN mkdir -p /custom-repos/etc/yum.repos.d
COPY custom-repos.repo /custom-repos/etc/yum.repos.d/

# Build rootfs with specific packages and versions
RUN /usr/libexec/bootc-base-imagectl build-rootfs \
    --manifest=minimal \
    --install=kernel \
    --install=systemd \
    --lock=kernel-6.5.0-1.fc39.x86_64 \
    --lock=systemd-253-1.fc39.x86_64 \
    --cachedir=/var/cache/dnf \
    /custom-repos /target-rootfs

FROM scratch
COPY --from=builder /target-rootfs/ /
LABEL containers.bootc 1
ENV container=oci
STOPSIGNAL SIGRTMIN+3
CMD ["/sbin/init"]

Example 3: Adding custom overlay content

FROM quay.io/fedora/fedora-bootc:rawhide as builder

# Prepare custom overlay content
RUN mkdir -p /overlay/usr/local/bin
RUN echo '#!/bin/bash\necho "Hello from custom overlay!"' > /overlay/usr/local/bin/custom-script
RUN chmod +x /overlay/usr/local/bin/custom-script

# Build rootfs with overlay
RUN /usr/libexec/bootc-base-imagectl build-rootfs \
    --manifest=default \
    --add-dir=/overlay \
    --install=cowsay \
    / /target-rootfs

FROM scratch
COPY --from=builder /target-rootfs/ /
LABEL containers.bootc 1
ENV container=oci
STOPSIGNAL SIGRTMIN+3
CMD ["/sbin/init"]

Example 4: Reproducible build with rechunking

# Build stage
FROM quay.io/fedora/fedora-bootc:rawhide as builder
RUN /usr/libexec/bootc-base-imagectl build-rootfs \
    --manifest=minimal \
    --install=kernel \
    --install=systemd \
    / /target-rootfs

# Create intermediate image
FROM scratch as intermediate
COPY --from=builder /target-rootfs/ /
LABEL containers.bootc 1
ENV container=oci
STOPSIGNAL SIGRTMIN+3
CMD ["/sbin/init"]

# Rechunk for reproducibility
FROM quay.io/fedora/fedora-bootc:rawhide as rechunker
COPY --from=intermediate / /
RUN /usr/libexec/bootc-base-imagectl rechunk \
    --max-layers=5 \
    localhost/intermediate:latest \
    localhost/final:latest

# Final image
FROM localhost/final:latest