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
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
Making Cache Persistent Between Pipelines
There are two complementary layers of caching:
GitLab CI file cache
A directory (e.g.,TMPDIR
) is cached inside the project workspace, persisting data used byRUN --mount=type=cache
between jobs and pipelines.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" .
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
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
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
Alpine packages (apk)
FROM alpine:3
RUN --mount=type=cache,target=/var/cache/apk apk add --no-cache build-base git
⚙️ Cache mounts exist only during the
RUN
step. If you need artifacts created in a cached directory, copy them elsewhere within the sameRUN
.
Practical Notes Before Setting Up CI
-
TMPDIR
** controls Buildah’s cache location**. AnyRUN --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/
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:
- 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).
- Set up workspace caching
- Define
TMPDIR=${CI_PROJECT_DIR}/.ci/tmp
in your CI variables. - Create the directory in
before_script
withmkdir -p "$TMPDIR"
. - Add it to
cache: paths:
in.gitlab-ci.yml
so the contents persist across jobs.
- 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
.
- 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
).
- Authenticate and push
- Log into your registry with
podman login
in thebefore_script
. - Push both the image and the cached layers to your registry.
- Verify caching efficiency
- Check build logs for reuse of cached layers and cached mounts.
- Ensure cache restoration reduces dependency fetch or install times.
- 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)