DEV Community

Cover image for Podman on GitLab CI: Fast, Efficient Container Builds — No DinD Required
Serge Matveenko
Serge Matveenko

Posted on

Podman on GitLab CI: Fast, Efficient Container Builds — No DinD Required

If you’re still relying on Docker-in-Docker (DinD) for container builds in GitLab CI, there’s a cleaner, faster way: Podman. Combined with GitLab’s cache, Podman lets you use RUN --mount=type=cache just like on your local machine — without a privileged Docker service. This approach gives you rootless, reproducible builds that efficiently reuse dependency caches across pipelines.

Inspired by my tiny repo lig/coredns-zoner, this guide generalizes the setup for popular languages and package managers. We’ll also note one caveat about GitLab.com’s cache implementation and how a self-managed GitLab can provide even faster caching.


Why Use Podman in CI?

  • No Docker-in-Docker service: Build inside a single container image (like quay.io/podman/stable) — no privileged daemon required.
  • Rootless operation: Works seamlessly with GitLab’s container executor.
  • Native caching: Podman/Buildah natively support RUN --mount=type=cache, letting you reuse dependency caches across builds.

How RUN --mount=type=cache Speeds Up Builds

When you use RUN --mount=type=cache,target=/some/dir in your Containerfile, the specified directory persists across builds. This avoids redownloading dependencies and refetching packages every run.

Common examples:

  • Go: /go/pkg/mod
  • Rust: /cargo/registry, /cargo/git
  • Node: /root/.npm or /usr/local/share/.cache/yarn
  • OS packages: /var/cache/apk, /var/cache/apt

These caches are available only during the RUN step — they’re not stored in the image itself, just reused between builds to save time.


A Real-World Example

In lig/coredns-zoner, Podman is used with cache mounts directly in the Containerfile:

FROM alpine/git AS clone
WORKDIR /src
RUN --mount=type=cache,target=/src/.git \
    git init -b master && \
    git remote add origin https://github.com/coredns/coredns.git || \
    git remote set-url origin https://github.com/coredns/coredns.git && \
    git fetch --tags --prune --auto-gc && \
    git switch -d $(git describe --tags --no-abbrev origin/HEAD) > /dev/null && \
    git reset --hard

FROM golang:1-alpine AS build
WORKDIR /src
COPY --from=clone /src /src
RUN --mount=type=cache,target=/go/pkg/mod \
    --mount=source=plugin.cfg,target=/src/plugin.cfg,relabel=shared \
    go generate && go build

FROM alpine AS app
ENTRYPOINT ["/coredns"]
COPY --from=build /src/coredns /coredns
Enter fullscreen mode Exit fullscreen mode

The key part: Git and Go caches are mounted for reuse between builds — no extra scripting or hacks.

The project’s .gitlab-ci.yml runs Podman builds without DinD, caching Buildah’s temporary data inside the project workspace:

stages:
  - build

build:
  stage: build
  image: quay.io/podman/stable:latest
  variables:
    TMPDIR: ${CI_PROJECT_DIR}/.local/tmp
  before_script:
    - mkdir -p ${CI_PROJECT_DIR}/.local/tmp
    - echo "$CI_REGISTRY_PASSWORD" | podman login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
  script:
    - podman build --pull --cache-from "${CI_REGISTRY_IMAGE}" -t "${CI_REGISTRY_IMAGE}" .
    - podman push "${CI_REGISTRY_IMAGE}"
  cache:
    paths:
      - .local/tmp/buildah-cache-0
Enter fullscreen mode Exit fullscreen mode

Making Cache Persistent Between Pipelines

There are two complementary layers of caching:

  1. GitLab CI file cache
    A directory (e.g., TMPDIR) is cached inside the project workspace, persisting data used by RUN --mount=type=cache between jobs and pipelines.

  2. Registry-based image cache
    Podman/Buildah can also store cached layers in the container registry. Use both --layers and --cache-to/--cache-from flags to enable it:

   podman build \
     --layers \
     --cache-to=type=image,ref="$CI_REGISTRY_IMAGE:build-cache" \
     --cache-from=type=image,ref="$CI_REGISTRY_IMAGE:build-cache" \
     -t "$CI_REGISTRY_IMAGE" .
Enter fullscreen mode Exit fullscreen mode

Using both methods — file cache for dependency directories and registry cache for build layers — yields the best speed gains.


Example Containerfile Patterns

Go modules

FROM golang:1.22-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod go mod download
COPY . .
RUN --mount=type=cache,target=/go/pkg/mod CGO_ENABLED=0 go build -o /out/app ./cmd/app
Enter fullscreen mode Exit fullscreen mode

Rust (cargo)

FROM rust:1-alpine AS build
WORKDIR /src
ENV CARGO_HOME=/cargo
RUN mkdir -p /cargo/git /cargo/registry
COPY Cargo.toml Cargo.lock ./
RUN --mount=type=cache,target=/cargo/registry --mount=type=cache,target=/cargo/git cargo fetch
COPY . .
RUN --mount=type=cache,target=/cargo/registry --mount=type=cache,target=/cargo/git cargo build --release
Enter fullscreen mode Exit fullscreen mode

Node (npm)

FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm npm ci --prefer-offline
COPY . .
RUN --mount=type=cache,target=/root/.npm npm run build
Enter fullscreen mode Exit fullscreen mode

Alpine packages (apk)

FROM alpine:3
RUN --mount=type=cache,target=/var/cache/apk apk add --no-cache build-base git
Enter fullscreen mode Exit fullscreen mode

⚙️ Cache mounts exist only during the RUN step. If you need artifacts created in a cached directory, copy them elsewhere within the same RUN.


Practical Notes Before Setting Up CI

  • TMPDIR** controls Buildah’s cache location**. Any RUN --mount=type=cache targets are automatically stored there — no Containerfile changes are required.
  • Keep ***TMPDIR***** inside your project workspace** so GitLab can cache it between jobs.
  • Enable layer caching by using --layers with --cache-to/--cache-from for a portable, registry-backed build cache.

Generic .gitlab-ci.yml Example

stages: [build]

variables:
  IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
  CACHE_IMAGE: $CI_REGISTRY_IMAGE:build-cache
  TMPDIR: ${CI_PROJECT_DIR}/.ci/tmp

build:
  image: quay.io/podman/stable:latest
  stage: build
  before_script:
    - mkdir -p "$TMPDIR"
    - echo "$CI_REGISTRY_PASSWORD" | podman login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
  script:
    - podman build --pull --layers \
        --cache-from=type=image,ref="$CACHE_IMAGE" \
        --cache-to=type=image,ref="$CACHE_IMAGE" \
        -t "$IMAGE" .
    - podman tag "$IMAGE" "$CI_REGISTRY_IMAGE:latest"
    - podman push "$IMAGE"
    - podman push "$CI_REGISTRY_IMAGE:latest"
  cache:
    paths:
      - .ci/tmp/
Enter fullscreen mode Exit fullscreen mode

Key Advantages

  • No Docker-in-Docker: Podman runs natively within a single container, eliminating DinD complexity.
  • Local-like caching: The same RUN --mount=type=cache behavior you use locally now works seamlessly in CI.
  • Registry cache support: Combine GitLab cache with Podman’s registry cache for best performance.

Caveat: GitLab.com Cache Behavior

On GitLab.com’s shared runners, caches are zipped and uploaded to S3 after each job and restored before the next. While reliable, this process can add extra time for large caches (compression and transfer overhead).

A Faster Option for Self-Managed GitLab

In self-managed GitLab, you can mount your S3 storage via FUSE (or another network filesystem). This avoids compression entirely, making caches behave like live directories and dramatically speeding up builds — especially with large dependency sets.


✅ Setup Checklist

Use this checklist to ensure your Podman + GitLab CI caching setup works efficiently:

  1. Prepare the environment
  • Use a CI image that includes podman (e.g., quay.io/podman/stable).
  • Ensure your GitLab runner supports the container executor (no privileged DinD needed).
  1. Set up workspace caching
  • Define TMPDIR=${CI_PROJECT_DIR}/.ci/tmp in your CI variables.
  • Create the directory in before_script with mkdir -p "$TMPDIR".
  • Add it to cache: paths: in .gitlab-ci.yml so the contents persist across jobs.
  1. Configure your Containerfile
  • Use RUN --mount=type=cache for dependency-heavy directories (e.g., /go/pkg/mod, /root/.npm, /cargo/registry).
  • You don’t need to adapt the file structure — Podman automatically caches those directories under TMPDIR.
  1. Optimize layer caching
  • Build with --layers enabled.
  • Add --cache-to and --cache-from pointing to your container registry (e.g., $CI_REGISTRY_IMAGE:build-cache).
  1. Authenticate and push
  • Log into your registry with podman login in the before_script.
  • Push both the image and the cached layers to your registry.
  1. Verify caching efficiency
  • Check build logs for reuse of cached layers and cached mounts.
  • Ensure cache restoration reduces dependency fetch or install times.
  1. Understand caching trade-offs
  • GitLab.com runners zip and upload caches to S3, which may slow down large caches.
  • In self-managed GitLab, prefer a FUSE-mounted S3 bucket or local cache directory for near-instant caching.

Wrap-Up

Using Podman with GitLab CI delivers faster, cleaner, and rootless container builds — no Docker daemon required. By combining file-based caching (via GitLab CI) and registry-based caching (via --cache-to/--cache-from), you can achieve near-local build performance.

Top comments (0)