My previous article on multi-stage builds to create Docker images for Go laid the foundation for using an intermediate image as the builder your Go binary. However, this example was intentionally simplistic and did not address package and dependency management.
Since the release of Go 1.11, the standard tooling has natively supported the concept of “modules”, an alternative to $GOPATH. This effectively deprecated 3rd party package managers like glide and dep.
In this article, I will show you how to incorporate the management of dependencies into an multi-stage Docker build.
Module support
To ensure Go module support we first set the GO111MODULE environment variable, then run “go mod init” which creates a file named “go.mod” and creates a cache of dependencies in the “$GOPATH/pkg” directory.
# ensure module support is enabled export GO111MODULE=on # initialize and create go.mod go mod init # list package dependencies go list -m all # build go build
Dockerfile with module support
Per my github project, golang-multistage-modules, we use the same commands as above when use our intermediate build layer to construct the executable.
# enable module support ENV GO111MODULE=on WORKDIR $GOPATH/src # get main project from git RUN git clone https://github.com/fabianlee/go-vendortest1.git \ && ls /build/src/go-vendortest1 WORKDIR $GOPATH/src/go-vendortest1/vendortest # compile, place executable into /build # by default, use git HEAD of external package "go-myutil" ARG BRANCH=HEAD RUN go mod init \ && go get github.com/fabianlee/go-myutil@$BRANCH \ && go list -m all \ && CGO_ENABLED=0 GOOS=linux go build -a -o out . \ && cp out $GOPATH/. # intermediate executable CMD [ "/build/out" ] # # generate clean, final image for end users # FROM alpine:3.11.3 # copy golang binary into container COPY --from=builder /build/out . # executable CMD [ "./out" ]
The command “go mod init” will by default fetch the latest HEAD version of each dependency. In this case, the “go get” of the go-myutil package right after is not necessary. But, if you do override the build argument, this allows us to fetch a different branch/tag of the go-myutil package. This package has branches: “HEAD” and “mybranch1“.
Github project
If you want to see this project at work, pull it from my github repository golang-multistage-modules.
# get source code, install make utility git clone https://github.com/fabianlee/golang-multistage-modules.git cd golang-multistage-modules sudo apt-get install make -y # build, go-myutil:HEAD (make docker-build) sudo docker build -f Dockerfile -t fabianlee/golang-multistage-modules:1.0.0 . # run test (make docker-test) sudo docker run -it --rm fabianlee/golang-multistage-modules:1.0.0 Version: master
Notice that the output from the executable is “Version: master”, which comes from the file myutil.go in the HEAD of the dependency project.
If we rebuild the image but override the BRANCH build argument with “mybranch1”, and run the same test:
# build go-myutil:mybranch1 (make docker-build-mybranch1) sudo docker build -f Dockerfile --build-arg BRANCH=mybranch1 -t fabianlee/golang-multistage-modules:1.0.0 . # run test (make docker-test) sudo docker run -it --rm fabianlee/golang-multistage-modules:1.0.0 Version: mybranch1
Now the output from the executable is “Version: mybranch1”, which comes from the file myutil.go in the ‘mybranch1’ of the dependency project.
REFERENCES
golang blog, Modules v2 and Beyond packaging with /v2
Paul Jolly, LondonGophers What are Go modules
golang, legacy GOPATH and go get