Buildah: Build OCI Container Images Without Docker Daemon — The Rootless Solution for CI/CD

Docker tutorial - IT technology blog
Docker tutorial - IT technology blog

2 AM. The team’s GitLab CI pipeline is showing red. The logs display that familiar line I’ve grown to hate:

Cannot connect to the Docker daemon at unix:///var/run/docker.sock.
Is the docker daemon running?

This wasn’t my first encounter with this error. Back when deploying microservices for an e-commerce project, the image build pipeline kept failing whenever a job ran inside an isolated container because it couldn’t mount the Docker socket from the host. The quick fix was to mount /var/run/docker.sock into the CI container — it worked, but I later learned that was the worst security decision I made that month.

Why Docker Daemon Is a Problem in CI/CD Environments

When using docker build in a pipeline, there are two core problems:

  • Docker requires a daemon running as root — in containerized environments like Kubernetes or GitLab shared runners, a Docker daemon isn’t always available on the host.
  • Mounting the Docker socket is a serious security risk — any process with access to /var/run/docker.sock can escalate privileges to root on the host.

The popular workaround is Docker-in-Docker (DinD) — running a separate Docker daemon inside the CI container. It sounds clever, but DinD requires --privileged mode, which effectively disables most of the container’s security features. In a production environment, this is a hard no.

A different tool is needed.

What Is Buildah and Why Does It Solve This Problem?

Buildah is an OCI (Open Container Initiative) image build tool developed by Red Hat, part of the daemonless container toolkit that includes Buildah + Podman + Skopeo. The key advantages:

  • No Docker daemon required — completely standalone
  • Supports rootless builds — runs as a regular user, no sudo needed
  • Outputs standard OCI images — fully compatible with Docker, Kubernetes, and Podman
  • Reads Dockerfile and Containerfile natively

Installing Buildah on Linux

RHEL / CentOS / Fedora

sudo dnf install -y buildah

Ubuntu / Debian

sudo apt-get update
sudo apt-get install -y buildah

Verify the installation:

buildah version
# buildah version 1.35.0 (image-spec 1.1.0, runtime-spec 1.2.0)

To use rootless builds (no sudo required), configure the user namespace mapping:

# Replace "youruser" with your actual username
echo "youruser:100000:65536" | sudo tee -a /etc/subuid
echo "youruser:100000:65536" | sudo tee -a /etc/subgid

Ways to Build Images with Buildah

Option 1: Using a Containerfile (simple and familiar)

Create a Containerfile — the syntax is identical to a Dockerfile:

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]

Build:

# Build from the Containerfile in the current directory
buildah build -t my-node-app:latest .

# Or specify the file explicitly
buildah build -f Containerfile -t my-node-app:latest .

Done. No daemon, no root.

Option 2: Step-by-step build using a shell script (scripted build)

This is Buildah’s unique strength. You can control every layer using a plain shell script:

#!/bin/bash
set -e

# Create a working container from the base image
CONTAINER=$(buildah from node:20-alpine)

# Mount the container's filesystem to the host
MOUNTPOINT=$(buildah mount $CONTAINER)

# Copy files into the container — using the host's cp/rsync, no extra RUN layer needed
mkdir -p $MOUNTPOINT/app
cp -r ./src $MOUNTPOINT/app/
cp package*.json $MOUNTPOINT/app/

# Run commands inside the container
buildah run $CONTAINER -- sh -c "cd /app && npm ci --only=production"

# Set image metadata
buildah config --cmd '["node", "/app/server.js"]' $CONTAINER
buildah config --port 3000 $CONTAINER
buildah config --label version=1.0.0 $CONTAINER
buildah config --env NODE_ENV=production $CONTAINER

# Unmount and commit as the final image
buildah unmount $CONTAINER
buildah commit $CONTAINER my-node-app:v1.0.0

# Clean up the temporary container
buildah rm $CONTAINER

echo "Build complete: my-node-app:v1.0.0"

This approach lets you use any host-side tool (rsync, sed, jq…) to process files before packaging, without adding extra RUN layers — resulting in smaller images and faster builds.

Rootless Build — Running Without Root Privileges

This is the primary reason I migrated our entire CI/CD stack to Buildah. Running as a regular user:

# No sudo needed
whoami       # output: ci-runner
buildah build -t myapp:latest .
# Build succeeded — zero root privilege

Even if the pipeline is compromised, an attacker only gets the permissions of the ci-runner user — no path to escalate to root on the host. This is something DinD can never guarantee.

Integrating Buildah into CI/CD Pipelines

GitLab CI

stages:
  - build
  - push

build-image:
  stage: build
  image: quay.io/buildah/stable:latest
  variables:
    STORAGE_DRIVER: vfs        # Use vfs in nested container environments
    BUILDAH_FORMAT: docker     # Output format compatible with Docker registries
  script:
    - buildah login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
    - buildah build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
    - buildah push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

No --privileged, no Docker socket mount, no DinD. The job runs cleanly inside Red Hat’s official Buildah image.

GitHub Actions

name: Build with Buildah

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install Buildah
        run: sudo apt-get install -y buildah

      - name: Build image
        run: |
          buildah build -t ghcr.io/${{ github.repository }}:${{ github.sha }} .

      - name: Push image
        run: |
          echo "${{ secrets.GITHUB_TOKEN }}" | buildah login ghcr.io -u ${{ github.actor }} --password-stdin
          buildah push ghcr.io/${{ github.repository }}:${{ github.sha }}

The Best Approach: Buildah + Podman for a Complete Workflow

After migrating the team’s CI/CD from Docker-in-Docker to Buildah, the most stable workflow I’ve settled on is:

  1. Build the image: Buildah (rootless, no daemon)
  2. Test the container: Podman to run and verify the container (podman run instead of docker run — also daemonless)
  3. Push to registry: Buildah push or Skopeo copy
# Step 1: Build
buildah build -t myapp:test .

# Step 2: Test (Podman is also rootless and daemonless)
podman run --rm myapp:test npm test

# Step 3: Tag and push to the production registry
buildah tag myapp:test registry.example.com/myapp:latest
buildah push registry.example.com/myapp:latest

# List built images
buildah images

# Remove old images
buildah rmi myapp:test

The Buildah + Podman + Skopeo trio fully replaces Docker in a server environment — no background daemon, no root access required, and most importantly — no /var/run/docker.sock to be exploited.

Looking back at that e-commerce late-night debugging session, most of the lost time came from an unstable build environment caused by DinD generating too many layer caching conflicts. After switching to Buildah, the pipeline became cleaner, more reproducible, and the security team stopped bringing it up every sprint review.

If you’re running CI/CD on Kubernetes, GitLab shared runners, or any environment where the Docker daemon is a pain point — Buildah is the most practical answer available right now.

Share: