dockergodocker-api

Send Docker Context as Tar with Go Client can't find Dockerfile


I am using the Go Docker Client to attempt to build an image from a Dockerfile whose contents are defined in code.

According to the Docker Daemon API Documentation

The input stream must be a tar archive...

...The archive must include a build instructions file, typically called Dockerfile at the archive’s root.

So I want to create the build context in code, write it to a tar file, then send that to the Docker Daemon to be built. To do this I can use the ImageBuild function and pass in the tar file (build context) as an io.ReadCloser. As long as my Dockerfile is at the root of that compressed archive, it should find it and build it.

However, I get the common error:

Error response from daemon: Cannot locate specified Dockerfile: Dockerfile

Which obviously means that it can't find the Dockerfile at the root of the archive. I am unsure why. I believe the way I am doing this adds a Dockerfile to the root of the tar archive. The daemon should see this. What am I misunderstanding here?

code snippet to reproduce

    var buf bytes.Buffer
    tarWriter := tar.NewWriter(&buf)

    contents := "FROM alpine\nCMD [\"echo\", \"this is from the archive\"]"

    if err := tarWriter.WriteHeader(&tar.Header{
        Name:     "Dockerfile",
        Mode:     777,
        Size:     int64(len(contents)),
        Typeflag: tar.TypeReg,
    }); err != nil {
        panic(err)
    }

    if _, err := tarWriter.Write([]byte(contents)); err != nil {
        panic(err)
    }

    if err := tarWriter.Close(); err != nil {
        panic(err)
    }

    reader := tar.NewReader(&buf)
    c, err := client.NewEnvClient()
    if err != nil {
        panic(err)
    }

    _, err = c.ImageBuild(context.Background(), reader, types.ImageBuildOptions{
        Context:    reader,
        Dockerfile: "Dockerfile",
    })
    if err != nil {
        panic(err)
    }

go.mod file

module docker-tar

go 1.12

require (
    github.com/docker/distribution v2.7.1+incompatible // indirect
    github.com/docker/docker v1.13.1
    github.com/docker/go-connections v0.4.0 // indirect
    github.com/docker/go-units v0.4.0 // indirect
    github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
    github.com/pkg/errors v0.8.1 // indirect
    golang.org/x/net v0.0.0-20191112182307-2180aed22343 // indirect
)

Solution

    1. Add a zero in front of 777 for octal numeral system: 0777, 0o777, or 0O777

    2. Use reader := bytes.NewReader(buf.Bytes()) not tar.NewReader(&buf)

    3. Use client.WithAPIVersionNegotiation() for newer versions too.

    4. Try this working version:

    package main
    
    import (
        "archive/tar"
        "bytes"
        "context"
        "fmt"
    
        "github.com/docker/docker/api/types"
        "github.com/docker/docker/client"
    )
    
    func main() {
        var buf bytes.Buffer
        tarWriter := tar.NewWriter(&buf)
        contents := `FROM alpine:3.10.3
    CMD ["echo", "this is from the archive"]
    `
        header := &tar.Header{
            Name:     "Dockerfile",
            Mode:     0o777,
            Size:     int64(len(contents)),
            Typeflag: tar.TypeReg,
        }
        err := tarWriter.WriteHeader(header)
        if err != nil {
            panic(err)
        }
        _, err = tarWriter.Write([]byte(contents))
        if err != nil {
            panic(err)
        }
        err = tarWriter.Close()
        if err != nil {
            panic(err)
        }
    
        c, err := client.NewClientWithOpts(client.WithAPIVersionNegotiation())
        if err != nil {
            panic(err)
        }
        fmt.Println(c.ClientVersion())
    
        reader := bytes.NewReader(buf.Bytes()) // tar.NewReader(&buf)
        ctx := context.Background()
        buildOptions := types.ImageBuildOptions{
            Context:    reader,
            Dockerfile: "Dockerfile",
            Tags:       []string{"alpine-echo:1.2.4"},
        }
        _, err = c.ImageBuild(ctx, reader, buildOptions)
        if err != nil {
            panic(err)
        }
    }
    
    
    

    Output for docker image ls after go run .:

    REPOSITORY                       TAG                 IMAGE ID            CREATED             SIZE
    alpine-echo                      1.2.4               d81774f32812        26 seconds ago      5.55MB
    alpine                           3.10.3              b168ac0e770e        4 days ago          5.55MB
    

    Output for docker run alpine-echo:1.2.4:

    this is from the archive
    

    Note: You may need to edit FROM alpine:3.10.3 for your specific version.