Buildah is a daemonless, rootless command-line tool for building OCI-compliant container images — without requiring a running Docker daemon or root privileges. On RHEL 9, Buildah ships as a first-class tool alongside Podman, and the two integrate tightly: images built with Buildah appear immediately in Podman’s local image store. Buildah’s fine-grained API lets you build images layer by layer using shell commands or compile them from a standard Dockerfile. This guide covers installing Buildah on RHEL 9, building images from scratch and from a Dockerfile, and pushing images to a registry.

Prerequisites

  • RHEL 9 system with sudo privileges
  • Podman already installed (recommended, though not strictly required)
  • A container registry account (Docker Hub, Quay.io, or a private registry) for pushing images
  • Familiarity with basic container concepts (images, layers, registries)

Step 1 — Install Buildah

# Install Buildah (and Podman if not already present)
sudo dnf install -y buildah podman

# Verify both tools are available
buildah --version
podman --version

# Enable user namespaces for rootless operation (usually enabled by default on RHEL 9)
cat /proc/sys/user/max_user_namespaces
# Should be 15000 or higher; if 0, enable it:
# sudo sysctl -w user.max_user_namespaces=15000

Step 2 — Build an Image from Scratch Using Buildah Commands

# Start a new build container from the ubi9-minimal base image
CONTAINER=$(buildah from registry.access.redhat.com/ubi9/ubi9-minimal)
echo "Working container: $CONTAINER"

# Run a command inside the build container (installs nginx)
buildah run $CONTAINER -- microdnf install -y nginx && microdnf clean all

# Copy a custom config file from the host into the container
echo "server { listen 80; root /usr/share/nginx/html; }" > /tmp/nginx.conf
buildah copy $CONTAINER /tmp/nginx.conf /etc/nginx/conf.d/default.conf

# Set default environment variables
buildah config --env APP_VERSION=1.0 $CONTAINER

# Set the port the container exposes
buildah config --port 80 $CONTAINER

# Set the default command to run when the container starts
buildah config --cmd "/usr/sbin/nginx -g 'daemon off;'" $CONTAINER

# Add OCI labels (good practice for CI/CD traceability)
buildah config --label maintainer="[email protected]" $CONTAINER
buildah config --label version="1.0" $CONTAINER

# Commit the working container to a named image
buildah commit $CONTAINER my-nginx:1.0

# Verify the image is available in both Buildah and Podman
buildah images
podman images

Step 3 — Build an Image from a Dockerfile

# Create a simple project directory
mkdir ~/buildah-app && cd ~/buildah-app

cat > Dockerfile < app.py <<'EOF'
from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello():
    return "Hello from Buildah on RHEL 9!"

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)
EOF

# Build the image from the Dockerfile using buildah bud (Build Using Dockerfile)
buildah bud -t my-flask-app:1.0 .

# Run the finished image with Podman to test it
podman run -d -p 5000:5000 --name flask-test my-flask-app:1.0
curl http://localhost:5000
podman rm -f flask-test

Step 4 — Push an Image to a Container Registry

# Log in to your registry (Docker Hub example)
buildah login docker.io
# Enter username and password when prompted

# Tag the image with the full registry path
buildah tag my-flask-app:1.0 docker.io/YOUR_DOCKERHUB_USER/my-flask-app:1.0

# Push to the registry
buildah push docker.io/YOUR_DOCKERHUB_USER/my-flask-app:1.0

# Push to a private registry (e.g., Quay.io or Nexus)
buildah login quay.io
buildah push my-flask-app:1.0 quay.io/YOUR_ORG/my-flask-app:1.0

# Push as an OCI tarball for air-gapped environments
buildah push my-flask-app:1.0 oci-archive:/tmp/my-flask-app.tar

Step 5 — Using Buildah in a CI/CD Pipeline Without Privileged Containers

# One of Buildah's main CI/CD advantages: it does not need a Docker socket
# or --privileged flag when user namespaces are available.

# Example Jenkinsfile pipeline stage (runs as non-root agent):
# stage('Build Image') {
#   steps {
#     sh 'buildah bud -t my-app:${BUILD_NUMBER} .'
#     sh 'buildah push my-app:${BUILD_NUMBER} quay.io/myorg/my-app:${BUILD_NUMBER}'
#   }
# }

# Example GitLab CI job (using a RHEL 9 runner):
# build-image:
#   script:
#     - buildah bud -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
#     - buildah push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA

# Compare: docker build requires a mounted socket (security risk in CI)
# docker build -t my-app .  <-- needs /var/run/docker.sock or --privileged

# List and clean up build containers to free disk space
buildah containers
buildah rm --all
buildah rmi --prune

Conclusion

Buildah gives RHEL 9 teams a secure, daemonless path to building OCI container images that integrates naturally with Podman’s rootless runtime. By eliminating the Docker daemon dependency, Buildah allows CI/CD agents to build and push images without elevated privileges or access to a Docker socket — a meaningful security improvement in shared build environments. Whether you work from a Dockerfile with buildah bud or build images layer by layer with fine-grained buildah run and buildah copy commands, the resulting images are fully compatible with any OCI-compliant runtime.

Next steps: How to Install Nexus Repository Manager on RHEL 9, Getting Started with Podman Compose on RHEL 9, and How to Configure a Rootless Podman systemd Service on RHEL 9.