- 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
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:
build-rootfs- Generate a container root filesystem from RPM packagesrechunk- Split container images into reproducible, chunked layerslist- 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.dconfigurationtarget- 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:
- Repository Setup: Runs
dnf repolistto refresh repository metadata - Manifest Processing: Loads YAML manifest from
/usr/share/doc/bootc-base-imagectl/manifests/ - Override Generation: Creates temporary JSON with user overrides
- OSTree Compose: Executes
rpm-ostree compose rootfswith the manifest - Post-processing: Fixes permissions and runs
bootc container lint - 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:
- Input Processing: Reads the source container image
- Layer Analysis: Analyzes RPM database to determine optimal layer splits
- Chunking: Splits content into separate, reproducible layers
- Timestamp Canonicalization: Sets all timestamps to zero for reproducibility
- 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:
- Manifest Discovery: Scans
/usr/share/doc/bootc-base-imagectl/manifests/for.yamlfiles - Filtering: Excludes
.hidden.yamlfiles and symbolic links - Metadata Extraction: Uses
rpm-ostree compose tree --print-onlyto read manifest metadata - 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
.yamlfiles (excluding.hidden.yaml) - Override Support: JSON overrides for user customizations
2. Package Management
- Repository Handling: Uses
/etc/yum.repos.dconfiguration 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 enginednf- Package manager for repository operationsostree- OSTree repository managementbootc- 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 lintto 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