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
sudoprivileges - 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.