BuildKit’s OCI image export feature lets you bypass the Docker daemon entirely when pushing images, directly writing the OCI image manifest and layers to a specified output.
Here’s a look at how it works and why you’d use it.
package main
import (
"context"
"fmt"
"os"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/images"
"github.com/containerd/containerd/leases"
"github.com/containerd/containerd/remotes/docker"
"github.com/moby/buildkit/client"
"github.com/moby/buildkit/session"
"github.com/moby/buildkit/session/contenttarget"
"github.com/moby/buildkit/util/progress/progressui"
)
func main() {
ctx := context.Background()
// Assume 'c' is an initialized buildkit.Client
// Assume 'src' is a buildkit.SolveOpt with your build context and target
// e.g., src := buildkit.SolveOpt{...}
// Example: Exporting an image to a local OCI layout directory
outputPath := "./my-oci-layout"
if err := os.MkdirAll(outputPath, 0755); err != nil {
// handle error
}
// Create a session manager for the export
sm, err := session.NewManager(context.Background())
if err != nil {
// handle error
}
// Register the content target exporter
exp, err := contenttarget.NewExporter(outputPath)
if err != nil {
// handle error
}
sm.Register(contenttarget.ExporterKind, exp)
// Define the export options
exportOpts := []client.ExportOpt{
client.WithExporter(contenttarget.ExporterKind),
client.WithSession(sm.NewSession(ctx, "buildkit-export-session", nil)),
}
// Solve and export the image
// This is a simplified representation; actual solve might involve more complex options
// For a real build, you'd define your build target (e.g., "dockerfile.v1")
// and potentially specify the platform.
// result, err := c.Solve(ctx, nil, src, exportOpts...)
// if err != nil {
// // handle error
// }
fmt.Printf("Image exported to OCI layout at: %s\n", outputPath)
// To push to a remote registry directly without a daemon:
// remoteOpts := []client.ExportOpt{
// client.WithRemote(docker.NewClient(), "my-registry/my-image:latest"),
// }
// result, err := c.Solve(ctx, nil, src, remoteOpts...)
// if err != nil {
// // handle error
// }
// fmt.Println("Image pushed to remote registry.")
}
The core problem BuildKit’s OCI export solves is decoupling image building from the Docker daemon’s image store and registry interaction. Traditionally, docker build and docker push rely on the daemon to manage image layers, tag them, and communicate with registries. This can be a bottleneck, introduce state management complexities, and limit where you can build and push images (e.g., in environments without a full Docker daemon).
BuildKit, when configured for OCI export, acts as a standalone builder. It generates the OCI image configuration (manifest, config blob) and the actual layer tarballs. Instead of telling the Docker daemon "store this image," you tell BuildKit "write this image to this OCI layout directory" or "push this image to this remote registry endpoint."
Internally, BuildKit uses its content store to manage the immutable blobs (layers and config). When you request an OCI export, it serializes the image into the OCI layout format (a directory structure with index.json, oci-layout, and blobs/sha256/...) or directly streams the manifest and blobs to a remote registry using the OCI Distribution Specification. The contenttarget exporter handles the local OCI layout, while the remote exporter (often using containerd’s remote interface) handles registry pushes.
The primary levers you control are:
- The Build Definition: This is your
Dockerfile, or more generally, theclient.SolveOptyou pass to BuildKit. It dictates what gets built. - The Export Destination: This is specified via
client.ExportOpt. You choose betweenclient.WithExporter(contenttarget.ExporterKind)for a local OCI layout, orclient.WithRemote(remote.Resolver, "ref")for pushing to a registry. - The Session: BuildKit uses sessions to manage communication between the client and the builder. For exports, you often need to set up specific exporters within the session, like
contenttarget.
The most surprising thing about BuildKit’s OCI export is how seamlessly it integrates with the broader containerd ecosystem. BuildKit itself doesn’t store images in a daemon-like way; it produces OCI-compliant artifacts. These artifacts can then be consumed by any OCI-compliant runtime or registry, not just the Docker daemon. This means you can use BuildKit to generate images that are then loaded into containerd, Podman, or even used directly by Kubernetes via CRI-compatible runtimes, all without a Docker daemon in the loop.
The next logical step after exporting to a local OCI layout is to import that layout into a container runtime like containerd or Podman.