Building Docker Images In Memory

While developing flagops - a distributed async job scheduler - I came across the need to implement a way to build Docker images from a given git repository in a simple and stateles way.

Cloning The Repository

First, we clone the repository into memory using go-git/go-git:

func CloneRepository(url string) billy.Filesystem {
    store := memory.NewStorage()
    fs := memfs.New()

    repo, _ := git.Clone(store, fs, &git.CloneOptions{
        URL:  url,
    })

    return fs
}

This will return an in-memory filesystem containing all of our files.

Building The Tar

We convert this in-memory filesystem into a Tar archive:

func Tar(filesystem billy.Filesystem) io.Reader {
    var buffer bytes.Buffer

    gzWriter := gzip.NewWriter(&buffer)
    defer gzWriter.Close()

    tarWriter := tar.NewWriter(gzWriter)
    defer tarWriter.Close()

    util.Walk(fs, ".", func(path string, info fs.FileInfo, _ error) error {
        header, _ := tar.FileInfoHeader(info, info.Name())

        header.Name = strings.TrimPrefix(strings.Replace(path, ".", "", -1), string(filepath.Separator))

        tarWriter.WriteHeader(header)

        if info.IsDir() {
            return nil
        }

        f, _ := filesystem.Open(path)

        io.Copy(tarWriter, f)

        f.Close()

        return nil
    })

    return &buffer
}

Buliding The Docker Image

Now we build the image through the docker/docker/client package as follows:

func BuildImage(name string, tar io.Reader) {
    client.ImageBuild(ctx, tar, types.ImageBuildOptions{
        Tags:       []string{name},
        Dockerfile: "Dockerfile",
    })
}

Putting It All Together

var client = client.NewClientWithOpts(client.FromEnv)

func main() {
    fs := CloneRepository("github.com/deadbeef/deadbeef")
    reader := Tar(fs)
    BuildImage("deadbeef", reader)
}
$ docker image ls deadbeef
REPOSITORY   TAG       IMAGE ID       CREATED         SIZE
deadbeef     92d575be  49a17de64f5f   2 minutes ago   17MB

Pushing The Image

In addition, if we wished to publish the built image we could do through the following code:

func PushImage(name string) {
    encoded, _ := json.Marshal(types.AuthConfig{
        Username: "username",
        Password: "password",
    })

    auth := base64.URLEncoding.EncodeToString(encoded)

    client.ImagePush(ctx, name, types.ImagePushOptions{
        RegistryAuth: auth,
    })
}