Also at Deasil Works · txn2 · Plexara
Profiles GitHub · X · LinkedIn
Theme Light · Auto · Dark
Professional notes by Craig Johnston
long-form, short-form, working drafts · since 2008
VOL. XIX · MMXXVI
126 NOTES IN PRINT
FOLIO CXXVI 2026-07-19 · 6 MIN · SHORT-FORM

Build Images Inside Your Cluster, Without the Docker Socket

Rootless container builds in CI with BuildKit, and where supply-chain guardrails belong

Diagram · folio cxxvi
flowchart LR
  PUSH["git push / tag"] --> CI["Forgejo Actions"]
  CI -->|buildctl| BK["BuildKit<br>rootless"]
  BK --> SIGN["scan, SBOM, cosign sign"]
  SIGN --> REG["registry"]

The Forgejo pipelines from the last post build container images, and how they do it matters more than it looks. The obvious way, the way a lot of CI still does it, is to mount the host’s Docker socket into the build job. That one shortcut hands the job root on the node, and it is exactly the kind of thing you do not want running on a cluster that holds your platform’s data. I build images rootless, inside the cluster, with BuildKit, and use the build stage for the supply-chain guardrails that belong there.

This series rebuilds my 2020 Apress book, Advanced Platform Development with Kubernetes, for 2026. The approach behind it comes from building and running data platforms in production for more than twenty years.

§Why Not the Docker Socket

Building an image traditionally meant talking to a Docker daemon, and in a CI job running on Kubernetes the tempting way to get one is to mount /var/run/docker.sock from the node into the job. Understand what that does: the Docker socket is full control of the Docker daemon, which runs as root, so a job with the socket can run any container it likes on the node, as root, with the host’s filesystem in reach. A build script, or anything that compromises it, now owns the node. On a cluster running your databases and secrets, that is an unacceptable blast radius for the convenience of a docker build.

The 2020-era answer to this was Kaniko, which built images inside a container without a daemon. Kaniko worked, and Google has since archived it. The current answers are BuildKit and Buildah, both of which build OCI images without a privileged daemon and without the socket. They produce the same OCI images your runtime already runs, with none of the host access.

§BuildKit, Rootless

BuildKit is the modern build engine, and its relevant property here is that it builds rootless: it does not need root on the node, and it does not need the Docker socket. You run it as a build backend the CI connects to, and the client, buildctl, drives the build. Because it runs as an ordinary unprivileged workload, a compromised build cannot reach past its own pod.

Buildah is the equally good alternative, Red Hat’s daemonless image builder, which builds from a Dockerfile or its own scripting and is a fine pick especially if you are in that ecosystem. The choice between them is preference; what matters is that neither needs the socket. I reach for BuildKit by default because it is the most widely used and integrates cleanly with the existing build tooling.

§Build in the Pipeline

A Forgejo Actions job builds and pushes an image with BuildKit, authenticating to the platform’s own registry. The job runs rootless, builds the image, and pushes it, with no daemon and no host access anywhere in the flow.

# .forgejo/workflows/build.yaml
jobs:
  build:
    runs-on: docker
    steps:
      - uses: actions/checkout@v4
      - name: Build and push with BuildKit
        run: |
          buildctl build \
            --frontend dockerfile.v0 \
            --local context=. \
            --local dockerfile=. \
            --output type=image,name=reg.git.apk8s.dev/platform/app:${GIT_TAG},push=true

The image lands in the Forgejo registry, built inside the cluster, by a job that never had more privilege than it needed. That is the same minimal-privilege posture the platform takes everywhere, applied to the place it is most often violated.

§The Build Is Where Guardrails Belong

The build stage is where the image is made, and it is the right place to prove the image is sound before it ships. On a platform increasingly built by AI agents, that proof matters more. Three checks belong here, and they are cheap to add once the pipeline exists. Scan the image for known vulnerabilities, with a scanner like Trivy, and fail the build if it finds something serious. Generate a software bill of materials, an SBOM, recording exactly what is in the image. And sign the image with cosign, so a deploy can later verify it was built by your pipeline and not substituted.

This is the supply-chain side of the discipline I have written about in the context of signing and SBOMs and the broader practice of holding AI-generated code to standards it would not hold itself to. The build stage is the gate: code goes in, and a scanned, inventoried, signed artifact comes out, or the build fails. An agent can write the code and run the pipeline, and the pipeline is what makes sure what it produced is verifiable.

§Operating Builds

Cache to stay fast. BuildKit’s layer caching makes repeated builds fast by reusing unchanged layers, and it can export and import cache to the registry so the cache survives across ephemeral CI runs. A build system without caching rebuilds the world every time; with it, only what changed is rebuilt.

Authenticate without leaking. The build pushes to the registry using credentials supplied as a mounted secret, not baked into an image or a script. The registry is the Forgejo one you own, so the whole build-and-push loop stays inside the platform.

Sign and inventory by default. Make the scan, SBOM, and signature part of every build, not a special case, so that every image on the platform is verifiable by construction. The cost is a few seconds per build; the value is knowing what you are running.

§When Something Is Wrong

The build fails with a permissions error. Rootless builds need a couple of kernel features and the right security context; a build that cannot set up its namespaces is usually missing the rootless configuration the BuildKit deployment expects, not a problem with the Dockerfile.

The push is rejected. Registry authentication. Confirm the credentials are mounted and valid and that the image name matches the Forgejo registry path; a rejected push is almost always auth or a name mismatch.

Builds are slow every time. No cache. Configure BuildKit to import and export its cache to the registry so layers persist between runs; without it, every CI run starts cold.

The signature step fails. The cosign key or its access. Keep the signing key as a secret the pipeline can read, and confirm the keyless or key-based signing is configured for how your registry and policy expect it.

§What You Have

Container images built inside the cluster, rootless, with no Docker socket and no host access, by a pipeline that scans, inventories, and signs every image it produces. It is the safe way to do the thing CI does constantly, and it closes the security hole that the convenient way leaves wide open. The platform builds its own images and can prove what they are.

The pipeline produces verified images; something has to deploy them. Next I close the loop with GitOps, where git is the source of truth and a controller keeps the cluster matching it, the deployment half the 2020 book never covered.

← back to all notes