Scratch Image Builder
A program that builds minimal OCI container images entirely in Go, without Docker. It takes a pre-compiled static Go binary, packages it into a scratch-based image by constructing tar layers and OCI config, then pushes the result to a container registry using go-containerregistry. The resulting image is typically 5-15 MB with no OS, shell, or package manager.
package main
import (
"archive/tar"
"bytes"
"compress/gzip"
"fmt"
"log"
"os"
"time"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/mutate"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/google/go-containerregistry/pkg/v1/tarball"
)
func main() {
// Read a pre-compiled static Go binary
binaryPath := os.Args[1]
destRef := os.Args[2] // e.g., "ghcr.io/myorg/myapp:v1.0.0"
binary, err := os.ReadFile(binaryPath)
if err != nil {
log.Fatalf("reading binary: %v", err)
}
// Create the layer
layer, err := createBinaryLayer("/app/server", binary)
if err != nil {
log.Fatalf("creating layer: %v", err)
}
// Build the image
img, err := assembleImage(layer)
if err != nil {
log.Fatalf("assembling image: %v", err)
}
// Push to registry
ref, err := name.ParseReference(destRef)
if err != nil {
log.Fatalf("parsing reference: %v", err)
}
if err := remote.Write(ref, img,
remote.WithAuthFromKeychain(authn.DefaultKeychain),
); err != nil {
log.Fatalf("pushing image: %v", err)
}
digest, _ := img.Digest()
fmt.Printf("Pushed %s@%s\n", destRef, digest)
}
func createBinaryLayer(path string, content []byte) (v1.Layer, error) {
var buf bytes.Buffer
gw := gzip.NewWriter(&buf)
tw := tar.NewWriter(gw)
// Add directory
tw.WriteHeader(&tar.Header{
Name: "app/",
Typeflag: tar.TypeDir,
Mode: 0755,
ModTime: time.Now(),
})
// Add binary
tw.WriteHeader(&tar.Header{
Name: path[1:], // strip leading slash for tar
Size: int64(len(content)),
Mode: 0755,
ModTime: time.Now(),
})
tw.Write(content)
tw.Close()
gw.Close()
return tarball.LayerFromReader(&buf)
}
func assembleImage(layer v1.Layer) (v1.Image, error) {
img, err := mutate.AppendLayers(empty.Image, layer)
if err != nil {
return nil, err
}
cfg, err := img.ConfigFile()
if err != nil {
return nil, err
}
cfg.Config.Entrypoint = []string{"/app/server"}
cfg.Config.Env = []string{"PORT=8080"}
cfg.Config.ExposedPorts = map[string]struct{}{"8080/tcp": {}}
cfg.Author = "go-devops-builder"
return mutate.ConfigFile(img, cfg)
}