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)
}

💻 Run locally

Copy the code above and run it on your machine

© 2026 ByteLearn.dev. Free courses for developers. · Privacy