We use docker images to have same application with its dependencies on any kind environment. Having compile time and runtime dependencies is the nature of a specific developer’s life. For example, in previous article, we had Golang dependency to build our Golang REST API. We need to design our dependencies carefully to eliminate unnecessary dependency within container is alive. In this tutorial, we will see how to use docker multi-stage builds to bundle our application in real life.
Ways to Handle Dependencies
Let say that, you have a Golang application, and you are developing a docker image to run on production environment. You can follow 2 ways to handle this;
- Use a build tool like Jenkins to compile you Golang application into a binary and add this binary to docker image. You need to have Golang installed on your Jenkins machine to build Golang project.
- You can add Golang as dependency to your docker image, and you can run Golang application within container with
go run …
or build your Golang application inside docker image and execute binary.
In 1st strategy, you need to have Golang installed in order to build docker images, since you need a compiled binary. When a new joiner of your team clones project, he/she needs to install Golang and continue to work
In 2nd strategy, no need to install anything, just clone project and build docker image, since everything included inside Dockerfile definition.
Wait! Why we have Golang dependency inside docker image even I only need that at first time during compiling ? Because we are too lazy to install Golang locally and build project and use inside docker image to eliminate that dependency 🙂 Let’s find a proper way to be happy with our laziness and yet eliminate dependency.
Multi-Stage Builds
Multi-stage builds helps us to keep our Dockerfile clean and reduces the image size by not including the dependencies you will not need on run time. In order to do this, we will simply do some operations on first step and then send the output of first step to the second step as parameter.
FROM instrumentisto/glide as builder
WORKDIR /go/src/bitbucket.org/kloiahuseyin/flowmon-projects
COPY . .
RUN glide install
RUN CGO_ENABLED=0 GOOS=linux go build -a -tags flowmon -o build/flowmon-projects -ldflags ‘-w’ .
FROM scratch
COPY — from=builder /go/src/bitbucket.org/kloiahuseyin/flowmon-projects/build/flowmon-projects app
ENV PORT 3000
EXPOSE 3000
ENTRYPOINT [“/app”]
In first section, we add our dependent image for glide and provide an alias builder
. Since, this Dockerfile inside our project, we simply add project files to docker image. With glide install
, we install all the dependencies for Golang project. Finally, we compile our Golang application into a executable binary with CGO_ENABLED=0 GOOS=linux go build -a -tags flowmon -o build/flowmon-projects -ldflags ‘-w’ .
At the end of the first stage, we have likely created an image that contains our binary. On second step, we used stratch which is an empty image and then copied our binary from first image with FROM scratch
. Now we have our binary
COPY — from=builder /go/src/bitbucket.org/kloiahuseyin/flowmon-projects/build/flowmon-projects appapp
, and only thing we need to do is providing this as entrypoint to our image.
Conclusion
There are several ways to create a docker image and the best way is to do this with minimum dependencies for cross platforms. Instead of being needed to install some dependencies, it is better to do this within Dockerfile by using multi-stage builds. For example, you don’t need to go to the Jenkins to see application dependencies in order to create your docker image on your local image. If you want to see this in action, you can refer for source code here