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
RUNsteps 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=cachepersists package-manager caches across builds;RUN --mount=type=secretinjects 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
| Aspect | Classic docker build | Rootless BuildKit |
|---|---|---|
| Privileged daemon | Required (dockerd as root) | None |
| Host root needed | Yes (effectively) | No |
| Docker socket mount | Common in CI | Not needed |
| Parallel build steps | Limited | Native (DAG) |
| Registry cache export | Add-on | First-class |
| Multi-tenant safety | Poor | Strong |
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:buildcacheA 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: 1000This 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/subgidentries for the unprivileged user. Managed Kubernetes nodes usually have this; bare hosts may need configuration. fuse-overlayfsperformance. 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, neverARG/ENVfor 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).