Back to Blog
Architecture11 min read2026-07-09

Rootless Container Builds with BuildKit Explained

Why building images as root via the Docker socket is a security liability, and how rootless BuildKit produces OCI images with no daemon, no privileged socket, and no host root.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

The Docker socket is the problem

For years, the default way to build a container image inside CI was to mount the host's Docker socket (/var/run/docker.sock) into the build job. It works, and it is a gaping hole. Anyone who can talk to that socket can run a container as root on the host, mount the host filesystem, and own the machine. "Build a container" quietly became "hand out host root."

The classic Docker build flow also relies on a long-running, root daemon (dockerd). The daemon is a single privileged process that does everything: pull, build, run, network. In a multi-tenant build farm that is exactly the wrong shape.

Rootless BuildKit fixes both problems: no daemon, no socket, no host root.

What BuildKit is

BuildKit is the modern build engine that powers docker build under the hood (via Buildx), but it can also run completely standalone. Its design goals are the reasons people migrate to it:

  • Concurrent, content-addressed build graph. BuildKit models a build as a DAG (LLB — low-level build) and executes independent steps in parallel. Unrelated RUN steps run concurrently instead of serially.
  • Aggressive, correct caching. Layer cache is content-addressed and can be exported to a registry, so a fresh CI runner can pull the cache and skip unchanged work.
  • Cache mounts and secrets. RUN --mount=type=cache persists package-manager caches across builds; RUN --mount=type=secret injects credentials without baking them into a layer.
  • OCI output. It produces standard OCI images consumable by any compliant registry/runtime.

What "rootless" means here

Rootless means the entire build runs as an unprivileged user — no root on the host, no setuid helpers required at runtime. It leans on a few Linux features:

  • User namespaces map an unprivileged host user to "root" *inside* the build container. UID 0 in the build is not UID 0 on the host.
  • fuse-overlayfs (or native rootless overlay on newer kernels) provides a layered filesystem without needing real root to mount overlayfs.
  • slirp4netns / network namespaces give the build network access without privileged networking.

The upshot: even if a build step is malicious, it is confined to an unprivileged namespace. It cannot trivially reach the host.

Daemon vs daemonless

AspectClassic docker buildRootless BuildKit
Privileged daemonRequired (dockerd as root)None
Host root neededYes (effectively)No
Docker socket mountCommon in CINot needed
Parallel build stepsLimitedNative (DAG)
Registry cache exportAdd-onFirst-class
Multi-tenant safetyPoorStrong

A concrete rootless build

The simplest path is buildctl talking to a buildkitd running rootless, or the all-in-one buildkit-rootless image. Here's a standalone build that exports an image and reuses a registry cache:

buildctl build \
  --frontend=dockerfile.v0 \
  --local context=. \
  --local dockerfile=. \
  --output type=image,name=registry.example.com/app:latest,push=true \
  --export-cache type=registry,ref=registry.example.com/app:buildcache,mode=max \
  --import-cache type=registry,ref=registry.example.com/app:buildcache

A Dockerfile that takes advantage of BuildKit features:

# syntax=docker/dockerfile:1
FROM node:20-slim AS build
WORKDIR /app
COPY package*.json ./
# Cache mount: node_modules download cache survives across builds
RUN --mount=type=cache,target=/root/.npm npm ci
COPY . .
RUN npm run build

FROM gcr.io/distroless/nodejs20
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
CMD ["dist/server.js"]

The --mount=type=cache line is the one people fall in love with: dependency installs go from minutes to seconds on warm cache, and nothing sensitive lands in a layer.

Running it in Kubernetes

The cleanest pattern for a build platform is *ephemeral build pods*: spin up a Job, build the image, push it, tear the Job down. Because BuildKit is rootless, those pods don't need privileged security contexts or a mounted host socket.

apiVersion: batch/v1
kind: Job
metadata:
  name: build-app
spec:
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: buildkit
          image: moby/buildkit:rootless
          args:
            - build
            - --frontend=dockerfile.v0
            - --local=context=/workspace
            - --local=dockerfile=/workspace
            - --output=type=image,name=registry/app:latest,push=true
          securityContext:
            # No privileged; rootless needs only these
            seccompProfile:
              type: Unconfined
            runAsUser: 1000

This is essentially how PandaStack builds images: rootless BuildKit runs in ephemeral Kubernetes Job pods, pushes the resulting OCI image to Google Artifact Registry, and a Helm release deploys it. There is no host Docker socket anywhere in the pipeline. For a multi-tenant platform that property is non-negotiable — one tenant's build must never be able to touch the host or another tenant.

Gotchas worth knowing

  • Kernel + subuid/subgid setup. Rootless needs a reasonably modern kernel and /etc/subuid / /etc/subgid entries for the unprivileged user. Managed Kubernetes nodes usually have this; bare hosts may need configuration.
  • fuse-overlayfs performance. On older kernels FUSE-based overlay is slower than native overlay. Newer kernels (5.11+) support rootless native overlay, which is faster.
  • Cache strategy matters most. The single biggest build-time win is a good registry cache. Without --import-cache, every fresh runner rebuilds from scratch.
  • Secrets. Use --mount=type=secret, never ARG/ENV for credentials — build args end up in image history.

When to adopt it

  • You build images in CI and currently mount the Docker socket — stop, switch.
  • You run a build farm or platform serving untrusted users.
  • You want parallel builds and registry-backed caching.

If you're a solo dev building locally on a machine you fully trust, plain docker build (which already uses BuildKit by default now) is fine. The rootless story pays off the moment builds run on shared infrastructure.

References

  • [BuildKit GitHub repository](https://github.com/moby/buildkit)
  • [BuildKit rootless mode docs](https://github.com/moby/buildkit/blob/master/docs/rootless.md)
  • [Dockerfile frontend reference (mounts, secrets)](https://docs.docker.com/reference/dockerfile/)
  • [OCI Image Format Specification](https://github.com/opencontainers/image-spec)
  • [Linux user namespaces man page](https://man7.org/linux/man-pages/man7/user_namespaces.7.html)

---

Don't want to operate a rootless build farm? PandaStack does it for you — connect a Git repo and it builds with rootless BuildKit, pushes to a registry, and deploys via Helm automatically. Start free at [dashboard.pandastack.io](https://dashboard.pandastack.io).

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Architecture

Browse all Architecture articles →

See also