GoLang: Go modules for package management during a multi-stage Docker build

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, modules

golang blog, modules in 2019

golang blog, Modules v2 and Beyond packaging with /v2

Paul Jolly, LondonGophers What are Go modules

golang, legacy GOPATH and go get