Build Linux kernels with stock docker build. No local compiler, source
checkout, or custom client is required.
kernelbuild-buildkit is a custom BuildKit
LLB frontend. It reads a small Kernelfile, resolves the source and toolchain,
and runs kbuild over a persistent object tree. An identical build is a full
cache hit. A config change re-runs one vertex, where kbuild recompiles only the
affected objects.
Status: pre-1.0. The Kernelfile format and Go API may change between minor releases until v1.0.
- Docker with BuildKit enabled. The frontend path is tested with Docker 29.7.2 and BuildKit 0.32.2 on native Linux/amd64 and Docker Desktop/arm64.
- Network access to GHCR, kernel.org, and the base image's package mirrors.
- Several gigabytes of free Docker storage. Allow about 3 GB per persisted kernel object tree, plus image layers and exported artifacts.
The default target is x86_64. Building arm64 kernels requires a ready cross-toolchain image; the worker itself may be amd64 or arm64. Go is needed only for the client and development workflows.
-
Create a
Kernelfile:#syntax=ghcr.io/emirb/kernelbuild-buildkit KERNEL 6.18.20 CONFIG kernel.config SHA256 a1415e257075c2fadf070f44bbb029469efbde5b6cf07d1433fe72207acff03c EPOCH 1785542400 TARGETS vmlinux image config -
Create a file named
kernel.configin the same directory as theKernelfile; theCONFIGline above names it. A fragment is enough because the build runsolddefconfig, so this one line is a valid config:CONFIG_OVERLAY_FS=yThe directory now holds exactly two files:
. ├── Kernelfile └── kernel.config -
Build from that directory:
docker build -f Kernelfile --output type=local,dest=out . -
Verify the result:
file out/vmlinux out/bzImage grep '^CONFIG_OVERLAY_FS=y$' out/config
out/vmlinux is the uncompressed ELF, out/bzImage is the x86 boot image, and
out/config is the resolved config. Repeating the build should report every
vertex as cached.
The complete example is in examples/. The bare frontend
reference tracks latest; use a release tag or digest in a committed
Kernelfile.
The artifacts boot directly in
Firecracker and
Cloud Hypervisor.
Firecracker normally takes vmlinux; Cloud Hypervisor accepts a PVH-enabled
vmlinux or bzImage. Both use the arm64 PE Image for arm64 guests.
Start from a VMM-maintained guest config rather than kbuild defaults:
curl -fsSLo kernel.config \
https://raw.githubusercontent.com/firecracker-microvm/firecracker/main/resources/guest_configs/microvm-kernel-ci-x86_64-6.1.config
docker build -f Kernelfile --output type=local,dest=out .Append local overrides below the downloaded config; later lines win through
olddefconfig. Cloud Hypervisor needs CONFIG_PVH=y. The minimal tested boot
floor is testdata/boot.config.
An empty config builds but does not make a useful microVM guest: initramfs and serial-console support are disabled by default, so it can boot to silence.
Kernelfile + config + patches
│
▼
BuildKit LLB frontend
│
▼
pinned base → kbuild-step → artifacts
↕
persistent object tree
The frontend resolves a tagged base image to a digest before generating the
graph. The compile vertex starts a static kbuild-step binary as plain argv.
Fetching, extraction, patching, config validation, seed transfer, and artifact
packing are implemented in Go; make is its only child process.
Values from the Kernelfile are validated before they reach the graph.
Extraction is confined with os.Root, and the
frontend does not interpolate input into shell commands. Kernel Makefiles can
still execute shells, so a config or patch must be treated as code. See
SECURITY.md for the trust boundary and deployment guidance.
Lines are KEY VALUE. Whitespace separates fields, # starts a comment, and
key order does not matter. Unknown and duplicate keys are errors.
The #syntax= directive is required for docker build. Individual keys are
optional because the frontend supplies defaults. The selected config file is
still required in the build context; omitting CONFIG selects kernel.config.
| Key | Required? | Value / default |
|---|---|---|
KERNEL |
No | kernel.org version; 6.18.20 |
SOURCE_URL |
No | derived from KERNEL; supports .tar.gz, .tar.xz, and .tar.zst |
SHA256 |
No | pinned for the default source; cleared when the source changes |
EPOCH |
No | SOURCE_DATE_EPOCH; 1785542400 |
CONFIG |
No | config filename in the context; kernel.config |
EXPECT |
No | post-olddefconfig assertions; disabled |
BASE_MAKE |
No | in-tree config targets applied before CONFIG; none |
TARGETS |
No | arch default; accepts vmlinux, image, modules, config, kconfigs |
ARCH |
No | x86_64 |
CROSS_COMPILE |
No | derived from ARCH |
TOOLCHAIN |
No | apt; use ready for a preinstalled toolchain |
PATCHES |
No | off |
BASE |
No | digest-pinned Ubuntu 24.04 image |
PROXY_CA |
No | CA certificate filename in the context; none |
image exports bzImage on x86_64 and Image on arm64. modules exports a
stripped modules.tar.zst. config and kconfigs do not compile a kernel.
See Kernelfile reference for target behavior, expectations, base configs, source pinning, Docker flags, and frontend image verification.
The build has three cache layers:
- BuildKit vertex cache. An identical build is a full hit. The cache can be exported to a registry or S3-compatible store for fresh workers.
- Persistent object tree. A config change re-enters the compile vertex but reuses kbuild's dependency state and compiled objects.
- Remote object-tree seed. BuildKit cache exporters do not include cache mounts. A trusted seeder can publish the object tree to S3-compatible storage so a cold worker can hydrate it before compiling.
The persisted tree is keyed by kernel version, architecture, toolchain image, cross prefix, and patched state. A source or patch-content mismatch discards the tree and rebuilds it. Seed publication is forced without replacing the warm cache mount.
No ccache or sccache is involved. See Operations and design for cache identity, remote seeding, concurrency, garbage collection, proxies, and toolchains.
Linux 6.18.20, 4 vCPU worker, 26 August 2026:
| Scenario | Wall | Objects compiled |
|---|---|---|
| Cold build, including vertex-cache export | 329s | 1740 |
| Identical rebuild | 1s | 0 |
| One config option changed, warm worker | 16s | 8 |
| Fresh worker, same config, S3 vertex hit | 2s | 0 |
| Fresh worker, changed config, 283 MB seed hydrate | 49s | 0–19 |
| Seed publication | +19s | 0 |
On a 16 vCPU / 32 GB worker, the same suite measured 84s cold, 0.4s for an identical request, and 11s for a one-option change. Client-side graph generation and marshaling measured 22µs per solve on Apple M5.
Every release is gated on the same path a user takes: the exact image about to
be published builds a kernel from testdata/boot.config
with stock docker build on a cold 4 vCPU runner and boots it in QEMU. For
v0.1.0
that was 80s for the full build and 1.5s to reach userspace. That config is
minimal, so it is not comparable to the table above.
These numbers are workload and worker dependent. The important invariants are zero compilation on a full hit, a small object delta for a local config change, and zero or near-zero compilation after seed hydration.
SHA256 pins the source bytes. EPOCH fixes the build timestamp. A committed
frontend digest and a TOOLCHAIN ready image pinned by digest fix the remaining
build inputs.
The default apt mode favors a one-command first build. Its Ubuntu base image is pinned, but the archive can publish newer compiler and binutils packages, so it is only best-effort reproducible across time. Use a complete, digest-pinned toolchain image for byte reproducibility.
The published frontend image is multi-architecture, signed with keyless cosign, and carries SLSA provenance and an SPDX SBOM. Verification commands are in the Kernelfile reference.
- Builds sharing one kernel, architecture, toolchain, and patch state serialize on a locked object-tree mount.
- BuildKit garbage collection can evict the tree. Size the daemon for roughly 3 GB per active tree or configure a remote seed.
- arm64 kernels are cross-compiled in linux/amd64 build steps. On an arm64 worker, those steps run under emulation.
- One invocation produces one target architecture, not a multi-platform result.
- The frontend applies in-tree
BASE_MAKEtargets and one config fragment. It does not implement arbitrary fragment merging or policy.
- Kernelfile reference
- Client and Go API
- Operations and design
- Development, testing, and releases
- Security model
Use GitHub issues for
bugs and usage questions. Report vulnerabilities privately as described in
SECURITY.md.
MIT