Sub-Module 2.1: Building with Hummingbird on OpenShift

Sub-Module Overview

Duration: ~15 minutes
Prerequisites: Environment setup complete (see Appendix B)
Learning Objectives:

  • Verify on-cluster Quay registry credentials for pushing built images

  • Understand the pre-installed buildah ClusterBuildStrategy

  • Build a Quarkus application from Git using Shipwright and a multi-stage Containerfile

  • Monitor build execution and verify pushed image

  • Deploy the Hummingbird-based image to OpenShift

Introduction

In Module 1, you built Quarkus applications locally using Podman with multi-stage Containerfiles: a full builder image for compilation and a minimal Hummingbird runtime for production. In this sub-module, you’ll bring that same pattern to OpenShift using Shipwright Builds.

Shipwright provides a Kubernetes-native build framework built on Tekton. Instead of running podman build on a developer workstation, you define a Build resource pointing to a Git repository and a ClusterBuildStrategy, then trigger a BuildRun. The Shipwright operator handles the rest — cloning the source, running the build, and pushing the image to the registry.

The pre-installed buildah ClusterBuildStrategy (shipped with the Builds for Red Hat OpenShift operator) works with any standard Containerfile, making it the natural choice for the multi-stage Hummingbird pattern you already know.

Project setup

Switch to your hummingbird project on the OpenShift Cluster:

oc project {user}-hummingbird-builds

Registry Configuration

The on-cluster Quay registry and registry-credentials secret should already be configured from Appendix B: Quay Setup. Verify the secret exists and is linked:

oc get secret registry-credentials -n {user}-hummingbird-builds
oc describe sa pipeline -n {user}-hummingbird-builds | grep registry-credentials
Expected output:
NAME                   TYPE                             DATA   AGE
registry-credentials   kubernetes.io/dockerconfigjson   1      ...

Mountable secrets:   registry-credentials

If the secret does not exist, return to Appendix B: Quay Setup and complete Steps 5-6 to create the secret and link it to the pipeline ServiceAccount.

Verify Pre-Installed BuildStrategy

Step 2: List Available ClusterBuildStrategies

The Builds for Red Hat OpenShift operator ships with several pre-installed strategies. Verify they are available:

oc get clusterbuildstrategy
Expected output:
NAME                  AGE
buildah               ...
buildpacks            ...
buildpacks-extender   ...
source-to-image       ...

The buildah strategy is the one we’ll use. It builds from a Containerfile (or Dockerfile) using Red Hat’s ubi9/buildah image and pushes the result to the configured registry.

Step 3: Inspect the buildah Strategy

oc get clusterbuildstrategy buildah -o jsonpath='{.spec.parameters[*].name}' | tr ' ' '\n'
Expected output:
build-args
registries-block
registries-insecure
registries-search
dockerfile
storage-driver
target
Key parameters:
  • dockerfile: Path to the Containerfile (default: Dockerfile). We’ll override this to Containerfile.

  • build-args: Pass ARG values to the Containerfile (e.g., override builder/runtime images).

  • registries-insecure: Mark registries that don’t have valid TLS (needed for the on-cluster Quay with self-signed certs).

Why use the pre-installed strategy?

The buildah strategy ships with the operator and is maintained by Red Hat. It runs as root (required for container builds), handles registry auth via Tekton credential injection, and supports all standard Containerfile features. No custom strategy creation needed.

Create Build Resource

Step 4: Create Build

Create a Build resource pointing to the sample Quarkus application with a Hummingbird multi-stage Containerfile:

cat << 'EOF' | oc apply -f -
apiVersion: shipwright.io/v1beta1
kind: Build
metadata:
  name: sample-quarkus-app
  namespace: {user}-hummingbird-builds
spec:
  source:
    type: Git
    git:
      url: https://github.com/tosin2013/sample-quarkus-hummingbird
      revision: main
  strategy:
    name: buildah
    kind: ClusterBuildStrategy
  paramValues:
  - name: dockerfile
    value: "Containerfile"
  output:
    image: {quay_hostname}/{quay_user}/sample-quarkus:latest
    credentials:
      name: registry-credentials
EOF
Expected output:
build.shipwright.io/sample-quarkus-app created
What this Build defines:
  • Source: Git repository containing a Quarkus REST application with a multi-stage Containerfile

  • Strategy: Pre-installed buildah ClusterBuildStrategy

  • Containerfile: Multi-stage build using quay.io/hummingbird-hatchling/openjdk:21-builder (Stage 1) and quay.io/hummingbird-hatchling/openjdk:21-runtime (Stage 2)

  • Output: Push to {quay_hostname}/{quay_user}/sample-quarkus:latest

  • Credentials: Use registry-credentials secret for registry push

The Containerfile in the repository follows the same multi-stage pattern from Module 1:

  • Stage 1 (Builder): Uses hummingbird-hatchling/openjdk:21-builder which includes JDK 21, bash, and dnf. The Maven wrapper (./mvnw) downloads Maven and compiles the Quarkus application.

  • Stage 2 (Runtime): Uses hummingbird-hatchling/openjdk:21-runtime which is distroless — no shell, no package manager. Only the compiled JAR and runtime dependencies are copied from Stage 1.

  • The final image runs as non-root (UID 65532, the Hummingbird default).

Execute BuildRun

Step 5: Trigger a BuildRun

Execute the build by creating a BuildRun resource:

cat << 'EOF' | oc apply -f -
apiVersion: shipwright.io/v1beta1
kind: BuildRun
metadata:
  name: sample-quarkus-app-run-1
  namespace: {user}-hummingbird-builds
spec:
  build:
    name: sample-quarkus-app
EOF
Expected output:
buildrun.shipwright.io/sample-quarkus-app-run-1 created

Step 6: Monitor BuildRun Progress

Watch the build execution:

oc get buildrun sample-quarkus-app-run-1 -n {user}-hummingbird-builds -w
Expected output:
NAME                         SUCCEEDED   REASON      STARTTIME   COMPLETIONTIME
sample-quarkus-app-run-1     Unknown     Pending     2s
sample-quarkus-app-run-1     Unknown     Running     15s
sample-quarkus-app-run-1     True        Succeeded   5m          5s

Press Ctrl+C once you see Succeeded.

Typical build times for the Quarkus application:

  • First build: 4-6 minutes (pulls Hummingbird builder/runtime images, downloads Maven and dependencies)

  • Subsequent builds: 2-3 minutes (image layers cached, Maven dependencies may be re-downloaded)

The first build is slower because the Hummingbird builder image and all Maven dependencies must be pulled. This is normal.

View Build Logs

Step 7: View Detailed Build Logs

Check detailed build logs:

BUILDRUN_POD=$(oc get buildrun sample-quarkus-app-run-1 -n {user}-hummingbird-builds -o jsonpath='{.status.taskRunName}')-pod

oc logs $BUILDRUN_POD -n {user}-hummingbird-builds --all-containers
Expected log excerpts:
[INFO] Building image ...
STEP 1/11: FROM quay.io/hummingbird-hatchling/openjdk:21-builder AS builder
...
STEP 4/11: RUN ./mvnw package -DskipTests -Dmaven.repo.local=/build/.m2
...
[INFO] [io.quarkus.deployment.QuarkusAugmentor] Quarkus augmentation completed in ...ms
[INFO] BUILD SUCCESS
...
STEP 6/11: FROM quay.io/hummingbird-hatchling/openjdk:21-runtime
...
STEP 10/11: USER 65532
STEP 11/11: ENTRYPOINT ["java", "-jar", "quarkus-run.jar"]
...
[INFO] Pushing image ...

Key build phases visible in the logs:

  • Stage 1: The Hummingbird builder image is pulled, Maven downloads dependencies, and Quarkus compiles the application.

  • Stage 2: The distroless Hummingbird runtime image is pulled, and only the compiled artifacts are copied in.

  • Push: The final image (containing only the JRE runtime + your compiled app) is pushed to the on-cluster Quay registry.

This is the same multi-stage pattern from Module 1, now running as an automated Shipwright build on OpenShift.

Verify Built Image

Step 8: Get Image Digest

Confirm the image was pushed successfully:

oc get buildrun sample-quarkus-app-run-1 -n {user}-hummingbird-builds -o jsonpath='{.status.output.digest}'
echo ""
Expected output:
sha256:abc123def456789...

Verify the image was pushed to Quay via the Docker v2 registry API:

TOKEN=$(curl -sk "{quay_url}/v2/auth?service={quay_hostname}&scope=repository:{quay_user}/sample-quarkus:pull" \
  -u "{quay_user}:{quay_password}" | python3 -c 'import json,sys; print(json.load(sys.stdin)["token"])')
curl -sk -H "Authorization: Bearer $TOKEN" \
  "{quay_url}/v2/{quay_user}/sample-quarkus/tags/list" | python3 -m json.tool
Expected output:
{
    "name": "{user}/sample-quarkus",
    "tags": [
        "latest"
    ]
}
The Quay repository is created as private by default. The v2 registry API is used here because it supports standard Docker token authentication without requiring an OAuth application token.

Step 9: Inspect Image with skopeo

Use skopeo to examine the built image and confirm it uses the Hummingbird runtime:

skopeo inspect docker://{quay_hostname}/{quay_user}/sample-quarkus:latest --username {quay_user} --password {quay_password} 2>/dev/null \
  | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'Architecture: {d.get(\"Architecture\")}')
print(f'OS: {d.get(\"Os\")}')
print(f'Layers: {len(d.get(\"Layers\",[]))}')
labels = d.get('Labels',{})
for k,v in sorted(labels.items()):
    if 'hummingbird' in k.lower():
        print(f'Label {k}: {v}')
"

The image labels from the Hummingbird base image (e.g., io.hummingbird-project.*) are inherited by the final image. This provides built-in provenance showing which Hummingbird runtime was used.

Step 9b: Verify in Quay Console

Open the Quay console and navigate to Repositoriesworkshopuser/sample-quarkus. You should see the latest tag with a successful security scan:

Quay Repository Tags
Figure 1. Quay repository showing the pushed sample-quarkus:latest image

Click the security scan result to view the Clair vulnerability report. A Hummingbird-based image should show zero vulnerabilities:

Clair Zero CVE Scan
Figure 2. Clair security scan: zero CVEs detected in the Hummingbird runtime image

Deploy Built Image

Step 10: Deploy to OpenShift

Deploy your Hummingbird-built application. First, link the registry credentials to the default ServiceAccount so pods can pull the image from the private Quay repository:

oc secrets link default registry-credentials --for=pull -n {user}-hummingbird-builds

Now create the deployment, service, and route:

oc create deployment sample-quarkus \
  --image={quay_hostname}/{quay_user}/sample-quarkus:latest \
  -n {user}-hummingbird-builds

oc expose deployment sample-quarkus --port=8080 -n {user}-hummingbird-builds

oc expose service sample-quarkus -n {user}-hummingbird-builds

The oc secrets link default registry-credentials --for=pull step is required because Quay creates repositories as private by default (CREATE_PRIVATE_REPO_ON_PUSH: true). Without linking the pull secret to the default ServiceAccount, pods will fail with ImagePullBackOff.

Step 11: Test the Application

ROUTE=$(oc get route sample-quarkus -n {user}-hummingbird-builds -o jsonpath='{.spec.host}')
echo "Application URL: http://$ROUTE/hello"
curl -s http://$ROUTE/hello | python3 -m json.tool
Expected output:
Application URL: http://sample-quarkus-hummingbird-builds.apps.<cluster-domain>/hello
{
    "message": "Hello from Hummingbird!",
    "runtime": "Quarkus + JDK 21.0.10"
}

You can also open the URL in your browser by navigating to http://<route>/hello:

Quarkus Hello Endpoint
Figure 3. Browser showing the Hummingbird Quarkus /hello endpoint response

Also verify the Quarkus health endpoint:

curl -s http://$ROUTE/q/health | python3 -m json.tool
Expected output:
{
    "status": "UP",
    "checks": [...]
}

If you get a timeout, the application may still be starting. Check pod status:

oc get pods -l app=sample-quarkus -n {user}-hummingbird-builds
oc logs -l app=sample-quarkus -n {user}-hummingbird-builds
OpenShift Pods
Figure 4. OpenShift console: hummingbird-builds pods showing the completed build and running application

The route created by oc expose service uses HTTP (not HTTPS). Access the application at http:// — using https:// will return a 503 error.

The root path / returns "Resource not found" which is expected for Quarkus REST applications. Use the /hello endpoint to verify the app is working.

Verify Hummingbird Runtime

Step 12: Check Image in Quay Console

Log into Red Hat Quay in the tab on the right hand side.

As a reminder your credentials for Quay are:

  • Username: {quay_user}

  • Password: {quay_password}

Find your container image clicking on the {quay_user} organization, and then clicking on the sample-quarkus image. Then on the image switch to the Tags tab to examine the available tags and their CVE scan results.

Look at your container image and check the results of the Quay Security Scanner for CVE counts. You can click on None detected to open up a detailed security report.

Hummingbird Runtime Advantages:

The image uses quay.io/hummingbird-hatchling/openjdk:21-runtime — a distroless image containing only the JRE and essential libraries. Compared to a full registry.access.redhat.com/ubi9/openjdk-21 image:

  • Dramatically fewer packages — no shell, no package manager, no build tools

  • Near-zero CVE baseline — fewer packages means fewer vulnerabilities

  • Non-root by default — runs as UID 65532

  • Content-based layers — uses chunkah for efficient pull deduplication

Standard vulnerability scanners (including Quay/Clair) may not yet fully support Hummingbird’s Fedora-based packages. The Hummingbird project provides custom scanning tools (syft-hummingbird.sh) for accurate results. See the Hummingbird repository for details.

Summary

Congratulations! You’ve built and deployed your first Hummingbird application on OpenShift using Shipwright!

What You’ve Accomplished

✅ Verified on-cluster Quay registry credentials linked to pipeline ServiceAccount
✅ Inspected the pre-installed buildah ClusterBuildStrategy
✅ Created a Build resource pointing to a Quarkus application Git repository
✅ Executed a BuildRun and monitored progress
✅ Viewed detailed build logs showing the multi-stage Containerfile in action
✅ Verified the image was pushed to Quay with the correct digest
✅ Inspected the image with skopeo to confirm Hummingbird labels
✅ Deployed the Hummingbird-built image to OpenShift
✅ Tested the running Quarkus application via Route

Key Takeaways

Module 1 Pattern on OpenShift:

  • The same multi-stage Containerfile pattern (builder + Hummingbird runtime) works seamlessly with Shipwright

  • No need to learn a new build system — the buildah strategy builds standard Containerfiles

  • The Hummingbird distroless runtime provides a near-zero CVE baseline

Developer Experience:

  • Simple YAML to trigger builds from Git

  • Pre-installed strategy — no custom build infrastructure needed

  • Builds run securely within the cluster

  • Quarkus health endpoints work out of the box for Kubernetes probes

Platform Engineering:

  • ClusterBuildStrategy enforces organizational build standards

  • All teams use Hummingbird runtimes automatically

  • Registry credentials managed centrally via Kubernetes secrets

Next Sub-Module

Ready for advanced strategies? Proceed to:

Create a ClusterBuildStrategy with language auto-detection, conditional Hummingbird runtime selection, and integration with internal registries.

Troubleshooting

Issue: BuildRun fails with registry push error

oc get secret registry-credentials -n {user}-hummingbird-builds
oc describe sa pipeline -n {user}-hummingbird-builds | grep registry-credentials

# Recreate secret if needed (use your on-cluster Quay route)
oc delete secret registry-credentials -n {user}-hummingbird-builds
oc create secret docker-registry registry-credentials \
  --docker-server="{quay_hostname}" \
  --docker-username={quay_user} \
  --docker-password={quay_password} \
  -n {user}-hummingbird-builds
oc secrets link pipeline registry-credentials -n {user}-hummingbird-builds

Issue: BuildRun stuck in Pending

oc get pods -n openshift-pipelines
oc get events -n {user}-hummingbird-builds --sort-by=.metadata.creationTimestamp
oc describe buildrun sample-quarkus-app-run-1 -n {user}-hummingbird-builds

Issue: BuildRun fails with BLOB_UPLOAD_INVALID

This typically indicates a problem with the NooBaa object storage backend used by Quay:

oc get backingstore -n openshift-storage
oc get noobaa -n openshift-storage -o jsonpath='{.items[0].status.phase}'

# If backing store shows Rejected/ALL_NODES_OFFLINE, wait for recovery
# Once NooBaa returns to Ready/OPTIMAL, delete the failed BuildRun and retry
oc delete buildrun sample-quarkus-app-run-1 -n {user}-hummingbird-builds
# Then re-create the BuildRun (Step 5)

Issue: Maven build fails with "permission denied" on mvnw

The Maven wrapper script must be executable in the Git repository. If you see this error, ensure the repo has the correct permissions:

# On the machine where the repo is managed:
chmod +x mvnw
git add mvnw
git commit -m "Fix mvnw permissions"
git push

Issue: Deployment pod stuck in ImagePullBackOff

The on-cluster Quay registry creates repositories as private by default. The default ServiceAccount needs pull credentials:

oc secrets link default registry-credentials --for=pull -n {user}-hummingbird-builds
oc delete pod -l app=sample-quarkus -n {user}-hummingbird-builds

Issue: Application not responding

oc get pods -l app=sample-quarkus -n {user}-hummingbird-builds
oc logs -l app=sample-quarkus -n {user}-hummingbird-builds
oc get route sample-quarkus -n {user}-hummingbird-builds