Adding New Images
This guide covers two scenarios:
- Adding a new image - Creating an entirely new image (e.g., adding
nginxfor the first time) - Adding a versioned image - Adding a new version of an existing image (e.g.,
go-1-25whengoalready exists)
Choose the appropriate section below based on your use case.
Adding Versioned Language Images
When adding a new version of an existing language runtime or SDK (e.g., go-1-25, nodejs-24, dotnet-sdk-10-0), follow this streamlined workflow.
1. Check Package Availability
Versioned packages may be available in different repositories:
- Hummingbird repository - Custom packages built by the Hummingbird project (e.g.,
golang1.25,nodejs24,dotnet-sdk-10.0) - Fedora repositories - Standard Fedora packages (e.g.,
golang,python3)
The project uses two distros:
rawhide- Uses only Fedora Rawhide packages (fedora-44.repo)hummingbird- Uses Fedora 43 + Hummingbird packages (fedora-43.repo+hummingbird.repo)
To check if a package is available in Hummingbird:
# Check the Hummingbird RPMs repository
ls ../rpms/rpms/ | grep <package-name>
Important: If the package is only available in the Hummingbird repository, add distros: [hummingbird] to properties.yml (see step 3).
2. Create Image Directory and Copy Files
Follow the naming pattern <language>-<version> with dashes (e.g., go-1-25, nodejs-24, dotnet-sdk-10-0):
# Create directory
mkdir images/go-1-25
# Copy templates from the base image (not from generic templates)
cp images/go/Containerfile.j2 images/go-1-25/
cp images/go/tests-container.yml images/go-1-25/
cp images/go/report.yml images/go-1-25/
Do not copy README.md.j2 - the README is typically shared across versions and generated automatically.
3. Configure properties.yml
Create images/go-1-25/properties.yml with the versioned package:
---
main_package: golang1.25
repository: go
# endoflife.date: parallel at major.minor (Docker Hub + Wolfi)
stream: "1.25"
distros:
- hummingbird # Only if package is unavailable in Fedora
user: root
tags:
# NOTE: Do NOT include 'latest' or major-version tags for older versions
# Only the newest version (e.g., go-1-26) should have those tags
- value: '{{ package_major_minor_version("golang1.25") }}'
label: io.hummingbird-project.major-minor-version
- value: '{{ package_version("golang1.25") }}'
label: org.opencontainers.image.version
rpm_packages:
all:
- coreutils-single
- git-core
- glibc-devel
- golang1.25
- make
Key differences from a new image:
- Set
main_packageto the versioned package name - Set
repositoryto match the base image family (for registry organization) - Set
streamto the version this image tracks (e.g.,"1.25"for go-1-25), with a YAML comment above it documenting the reasoning (copy from an existing sibling image) - Add
distros: [hummingbird]if the package is only in the Hummingbird repository - Tag strategy for multiple versions:
- Newest version only (e.g.,
go-1-26): Includelatestandmajor-versiontags - Older versions (e.g.,
go-1-25): Include onlymajor-minor-versionandversiontags - This prevents tag conflicts where multiple images would claim the same major version tag
- Newest version only (e.g.,
4. Update Containerfile Template
Edit images/go-1-25/Containerfile.j2 to reference the versioned package in any environment variables or version-specific commands:
ENV GOPATH=/go \
GOTOOLCHAIN=local \
- GOLANG_VERSION={{ package_version('golang') }} \
+ GOLANG_VERSION={{ package_version('golang1.25') }} \
PATH=/go/bin:/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
5. Update Comparison Images
Edit images/go-1-25/report.yml to compare against version-specific upstream images:
compare:
- pull: docker.io/golang:1.25
url: https://hub.docker.com/_/golang
- pull: cgr.dev/chainguard/go:latest-dev
url: https://images.chainguard.dev/directory/image/go/overview
6. Tag Strategy for Multiple Versions
When multiple versions of the same language exist (e.g., both go-1-25 and go-1-26), coordinate tags to avoid conflicts:
Newest version (go-1-26):
tags:
- value: latest # Points to newest
- value: '{{ package_major_version("golang1.26") }}' # e.g., "1"
label: io.hummingbird-project.major-version
- value: '{{ package_major_minor_version("golang1.26") }}' # e.g., "1.26"
label: io.hummingbird-project.major-minor-version
- value: '{{ package_version("golang1.26") }}' # Full version
label: org.opencontainers.image.version
Older versions (go-1-25):
tags:
# NO 'latest' tag
# NO major-version tag (would conflict with go-1-26's "1" tag)
- value: '{{ package_major_minor_version("golang1.25") }}' # e.g., "1.25"
label: io.hummingbird-project.major-minor-version
- value: '{{ package_version("golang1.25") }}' # Full version
label: org.opencontainers.image.version
Why: This ensures users can reference:
quay.io/hummingbird/go:latest→ go-1-26quay.io/hummingbird/go:1→ go-1-26 (latest 1.x)quay.io/hummingbird/go:1.25→ go-1-25 (specific minor version)quay.io/hummingbird/go:1.26→ go-1-26 (specific minor version)
When to update: If you add go-1-27, update go-1-26 to remove its latest and major-version tags, and add them to go-1-27.
7. Generate Files and Test
# Generate distro directories, lockfiles, VERSION, TAGS, etc.
make
# Build the image
ci/build_images.sh go-1-25/hummingbird/default
# Run tests
ci/run_tests_container.sh go-1-25/hummingbird/default
# Run linters
make check
Common Issues
Error: No match for argument: golang1.25
The package is not available in the configured distro repositories. Add distros: [hummingbird] to properties.yml and remove any generated rawhide/ directories:
rm -rf images/go-1-25/rawhide
Adding Completely New Images
Follow these steps to add an entirely new container image to the project.
1. Create Image Directory
Create a directory for the new image:
mkdir images/your-service
2. Copy Base Templates
Copy the template files:
cp images/properties.yml images/your-service/properties.yml
cp images/Containerfile.j2 images/your-service/Containerfile.j2
3. Configure Image Properties
Edit images/your-service/properties.yml to configure:
- Variants: Define variants (default:
[default, builder]) - Packages: List RPM packages needed in
rpm_packages.all - Main package: Set
main_packagefor version labeling - Repository and stream: Set
repositoryandstream(both required). Add a YAML comment abovestreamdocumenting the reasoning (see Choosing a Stream Value) - Tags: Configure image tags
Example minimal configuration:
---
rpm_packages:
all:
- coreutils-single
- your-main-package
main_package: your-main-package
repository: your-service
# <reasoning for stream choice> -- see "Choosing a Stream Value" section
stream: "latest"
tags:
- value: latest
See the Image Configuration Reference for complete
properties.yml options.
4. Customize Containerfile Template
Edit images/your-service/Containerfile.j2:
- Add service-specific configuration
- Configure exposed ports, volumes, and entrypoint
- Add
{{ set_user() }}at the end of the template to set the container user
The template automatically includes packages from properties.yml via the
{{ main_packages_arg() }} macro.
The {{ set_user() }} macro sets the USER directive based on the user: field in
properties.yml (which defaults to default if not specified).
Multi-Architecture Support
When Containerfiles or build scripts need to reference the target architecture (e.g., downloading architecture-specific binaries), use the correct naming convention for each context.
| Docker | Linux | |
|---|---|---|
| Intel | amd64 |
x86_64 |
| ARM | arm64 |
aarch64 |
| Containerfile | $TARGETARCH ARG variable |
$(arch) output |
- In Containerfiles or Go builds: Use the
$TARGETARCHARG variable provided by buildah (values:amd64,arm64) - In shell scripts or RPM builds: Use the arch or uname -m commands (values:
x86_64,aarch64)
5. Add Integration Tests
Create test files in images/your-service/:
tests-container.yml- Container tests (Podman/Docker)tests-k8s.yml- K8s tests
Container test example:
---
version-check:
command: |
test_engine_run --rm "${TEST_IMAGE}" your-service --version
K8s test example:
---
run-as-pod:
command: |
name="test-pod-${TEST_RUN_ID}"
kubectl run "${name}" --image="${TEST_IMAGE}" --restart=Never --labels="${TEST_RUN_LABEL}"
kubectl wait --for=jsonpath='{.status.phase}'=Succeeded "pod/${name}" --timeout=60s
See the Testing Guide for how to write and run tests.
6. Add Image Documentation
Create images/your-service/README.md.j2 with a high-level description, usage instructions, and
compatibility notes. The template generates multiple README files for different target audiences
(defined in images/variables.yml under readme_targets).
Important notes:
- The
readme_heading()macro automatically generates the heading with the correct product name - The
readme_description()macro renders the image description fromdescriptioninproperties.yml; use it after the heading instead of hardcoding introductory text - Use
{{ readme_targets[target].registry }}for registry references (not hardcodedquay.io/hummingbird) - The
readme_standard_sections()macro generates sections for tags, compatibility, verification, and vulnerabilities
Example template:
{{ readme_heading() }}
{{ readme_description() }}
## Usage
podman run -d -p 8080:8080 {{ readme_targets[target].registry }}/your-service:latest
{{ readme_standard_sections(variants, tags) }}
For more details on README templates, see the README Generation System section in .claude/CLAUDE.md.
7. Follow Standard Workflow
Follow the standard contribution workflow from the Development Workflow guide:
- Generate files (
make) - Build locally (
ci/build_images.sh your-service) - Test locally (
ci/run_tests_container.shandci/run_tests_k8s.sh) - Run linters (
make check) - Commit and push changes
- Open a merge request
- Wait for CI pipeline
8. Deploy Konflux Resources and Trigger Testing
On the merge request pipeline, click the play button next to the deploy_resources_to_konflux job
to deploy the Konflux resources for the new image. After the job completes, add a /retest comment
to the merge request to trigger the CI pipeline for the newly added image.
Choosing a Stream Value
Every image must have a stream field in properties.yml. The stream is a version series
identity (e.g., "24", "3.11", "10.1", "latest") that is emitted as the
io.hummingbird-project.stream label on the container image. It enables the image catalog to
provide consistent structured data about multi-stream repositories.
Why Stream Matters
The image catalog must provide consistent structured data across time, including for images published before a second stream exists. Labels on published images cannot be changed retroactively. If a repository gains a new stream, all previously published images must already carry the correct version-based stream value — otherwise the catalog has inconsistent data that requires fragile repository-specific fixups.
Decision Process
flowchart TD
Start["New image: choose stream value"] --> Q1{"Does endoflife.date show\nmultiple concurrently\nsupported branches?"}
Q1 -->|Yes| Assess["Assess ecosystem granularity:\nupstream project, Docker Hub,\nDebian/Ubuntu, Wolfi/Chainguard,\nFedora, endoflife.date"]
Assess --> Version["Use version-based stream\n(match ecosystem granularity)"]
Q1 -->|No| Q2{"Does the software have a\nmeaningful version series\nidentity?"}
Q2 -->|Yes| Current["Use version-based stream\n(current major version)"]
Q2 -->|No| Latest["stream: latest"]
Version --> Doc["Document reasoning\nas YAML comment"]
Current --> Doc
Latest --> Doc
Document the reasoning for the stream choice as a YAML comment above the stream field in
properties.yml. This ensures future maintainers understand why a particular granularity was
chosen:
# endoflife.date: parallel at major.minor (2.8, 3.0, 3.2)
stream: "3.0"
# rolling release with date-based versions, no version branches
stream: "latest"
Step 1: Does endoflife.date show multiple concurrently supported branches?
Check endoflife.date for the upstream project. It provides structured data on upstream release branches, security support timelines, and versioning schemes for most projects. If it shows multiple concurrently supported branches, use a version-based stream at the granularity of those branches. If the product is not on endoflife.date, that is itself evidence that parallel version lifecycle management is not a concern for that software — proceed to step 2.
The stream granularity must match the level at which upstream maintains parallel security-supported branches. Check:
- Upstream project: At what version granularity are parallel branches maintained?
- Docker Hub official images: Are there parallel version tags? At what granularity?
- Debian/Ubuntu: Are there parallel versioned packages?
- Wolfi/Chainguard: Are there versioned package variants?
- Fedora: Are there parallel versioned packages? Single-version availability is not evidence against versioning, but multi-version availability is strong evidence for it.
Important: the image’s directory name is not evidence for granularity. The directory may use a coarser version or no version at all. The stream must reflect upstream’s actual branch structure.
Similarly, the purpose of upstream’s branches (LTS vs stable, stable vs mainline, STS vs LTS) is irrelevant to granularity. If they are parallel branches receiving security updates, the stream granularity must match.
Match the upstream project’s version naming convention. If the project and ecosystem consistently
identify branches using a format that includes a minor component (e.g., .NET uses 8.0, 9.0 —
never just 8 or 9), use that format for the stream value. This applies even when
endoflife.date abbreviates to just the major number.
Step 2: Does the software have a meaningful version series identity?
If the software uses numbered versioning where the major (or major.minor) number identifies a
version series, use a version-based stream at the current major version. The risk is asymmetric:
using a version-based stream when a new major version never ships is harmless (the label is still
correct), while using "latest" when a new version does ship breaks catalog consistency.
Note: if endoflife.date lists minor versions (e.g., memcached 1.4, 1.5, 1.6) but each immediately supersedes the previous with no overlap in support periods, the minor number is a sequential release counter, not a branch identity. Use the major version as the stream.
Reserve "latest" only for images where no meaningful version series identity exists. This is a
narrow criterion with two cases:
- Rolling/date-based releases with no version series (e.g., minio uses
RELEASE.2024-01-18...) - Base images that track the distro rather than independent software (e.g., core-runtime)
Step 3: For unversioned images, track the current version
For images without a version in their directory name or version constraints, the stream value
reflects the version currently shipped and must be updated when the upstream version changes.
This is analogous to version_constraints — an explicit declaration that is updated periodically.
Field Reference
See the Image Configuration Reference for the
stream field definition and examples.
Next Steps
- Testing Guide - How to run and write tests
- Image Configuration Reference - Complete
properties.ymlreference - FIPS Variant Guide - How to add FIPS variants to images
- Image Pipeline - How the complete pipeline works