Buildah: Build OCI Container Image không cần Docker Daemon — Giải pháp rootless cho CI/CD

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

2 giờ sáng. Pipeline GitLab CI của team báo đỏ. Log hiện ra dòng quen thuộc mà mình đã ghét từ lâu:

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

Thật ra đây không phải lần đầu mình gặp cái lỗi này. Hồi deploy microservices cho dự án e-commerce, pipeline build image cứ fail mỗi khi job chạy trong isolated container vì không mount được Docker socket từ host. Lúc đó fix tạm bằng cách mount /var/run/docker.sock vào container CI — nó chạy được, nhưng về sau mới biết đó là quyết định tệ nhất trong tháng về mặt bảo mật.

Tại sao Docker Daemon là vấn đề trong môi trường CI/CD?

Khi dùng docker build trong pipeline, có hai vấn đề cốt lõi:

  • Docker cần daemon chạy với quyền root — trong môi trường container hóa như Kubernetes hay GitLab shared runners, không phải lúc nào cũng có Docker daemon sẵn sàng trên host.
  • Mount Docker socket là rủi ro bảo mật nghiêm trọng — bất kỳ process nào truy cập được /var/run/docker.sock đều có thể leo thang đặc quyền lên root của host.

Giải pháp phổ biến là Docker-in-Docker (DinD) — chạy một Docker daemon khác ngay bên trong container CI. Nghe hay, nhưng DinD yêu cầu --privileged mode, vốn tắt gần hết các tính năng bảo mật của container. Với môi trường production, đây là điều tối kị.

Cần một công cụ khác.

Buildah là gì và tại sao nó giải quyết được vấn đề này?

Buildah là tool build OCI (Open Container Initiative) image do Red Hat phát triển, thuộc bộ công cụ container không daemon gồm Buildah + Podman + Skopeo. Điểm mấu chốt:

  • Không cần Docker daemon — hoàn toàn standalone
  • Hỗ trợ rootless build — chạy với user thường, không cần sudo
  • Output là OCI image chuẩn — tương thích hoàn toàn với Docker, Kubernetes, Podman
  • Đọc được DockerfileContainerfile như bình thường

Cài đặt Buildah trên Linux

RHEL / CentOS / Fedora

sudo dnf install -y buildah

Ubuntu / Debian

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

Kiểm tra cài đặt:

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

Để dùng rootless build (không cần sudo), cần cấu hình user namespace mapping:

# Thay "youruser" bằng username thực của bạn
echo "youruser:100000:65536" | sudo tee -a /etc/subuid
echo "youruser:100000:65536" | sudo tee -a /etc/subgid

Các cách build image với Buildah

Cách 1: Dùng Containerfile (đơn giản, quen thuộc)

Tạo file Containerfile — cú pháp hoàn toàn giống Dockerfile:

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

Build:

# Build từ Containerfile trong thư mục hiện tại
buildah build -t my-node-app:latest .

# Hoặc chỉ định file rõ ràng
buildah build -f Containerfile -t my-node-app:latest .

Xong. Không daemon, không root.

Cách 2: Build từng bước bằng shell script (scripted build)

Đây là điểm mạnh độc đáo của Buildah. Bạn có thể kiểm soát từng layer bằng shell script thuần:

#!/bin/bash
set -e

# Tạo container working từ base image
CONTAINER=$(buildah from node:20-alpine)

# Mount filesystem của container ra host
MOUNTPOINT=$(buildah mount $CONTAINER)

# Copy files vào container — dùng cp/rsync của host, không cần RUN layer
mkdir -p $MOUNTPOINT/app
cp -r ./src $MOUNTPOINT/app/
cp package*.json $MOUNTPOINT/app/

# Chạy lệnh bên trong container
buildah run $CONTAINER -- sh -c "cd /app && npm ci --only=production"

# Set metadata cho image
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 và commit thành image cuối
buildah unmount $CONTAINER
buildah commit $CONTAINER my-node-app:v1.0.0

# Dọn dẹp container tạm
buildah rm $CONTAINER

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

Cách này cho phép dùng mọi công cụ của host (rsync, sed, jq…) để xử lý file trước khi đóng gói, mà không tốn thêm RUN layer nào — kết quả là image nhỏ hơn và build nhanh hơn.

Rootless Build — Chạy không cần quyền root

Đây là lý do chính mình chuyển toàn bộ CI/CD sang Buildah. Chạy với user thường:

# Không cần sudo
whoami       # output: ci-runner
buildah build -t myapp:latest .
# Build thành công — zero root privilege

Ngay cả khi pipeline bị compromise, attacker chỉ có quyền của user ci-runner, không leo thang được lên root host. Đây là thứ DinD không bao giờ đảm bảo được.

Tích hợp Buildah vào CI/CD Pipeline

GitLab CI

stages:
  - build
  - push

build-image:
  stage: build
  image: quay.io/buildah/stable:latest
  variables:
    STORAGE_DRIVER: vfs        # Dùng vfs trong môi trường container lồng nhau
    BUILDAH_FORMAT: docker     # Output format tương thích Docker registry
  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"

Không --privileged, không mount Docker socket, không DinD. Job chạy sạch trong image Buildah chính thức của Red Hat.

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 }}

Cách tốt nhất: Buildah + Podman cho workflow hoàn chỉnh

Sau khi chuyển CI/CD của team từ Docker-in-Docker sang Buildah, workflow ổn định nhất mình đúc kết được là:

  1. Build image: Buildah (rootless, không daemon)
  2. Test container: Podman để chạy container kiểm tra (podman run thay docker run — cũng không cần daemon)
  3. Push lên registry: Buildah push hoặc Skopeo copy
# Bước 1: Build
buildah build -t myapp:test .

# Bước 2: Test (Podman cũng rootless, không daemon)
podman run --rm myapp:test npm test

# Bước 3: Tag và push lên production registry
buildah tag myapp:test registry.example.com/myapp:latest
buildah push registry.example.com/myapp:latest

# Xem danh sách image đã build
buildah images

# Dọn dẹp image cũ
buildah rmi myapp:test

Bộ ba Buildah + Podman + Skopeo thay thế hoàn toàn Docker trong môi trường server mà không cần daemon chạy ngầm, không cần root, và quan trọng nhất — không có /var/run/docker.sock nào để bị khai thác.

Nhìn lại cái đêm e-commerce ngồi debug memory leak trong container, phần lớn thời gian mất đi là do môi trường build không ổn định vì DinD tạo ra quá nhiều layer caching conflict. Chuyển sang Buildah, pipeline sạch hơn, reproducible hơn, và team security không còn nhắc nhở mỗi sprint review nữa.

Nếu bạn đang vận hành CI/CD trên Kubernetes, GitLab shared runners, hay bất kỳ môi trường nào mà Docker daemon là vấn đề — Buildah là câu trả lời thực dụng nhất hiện tại.

Share: