In the first part of this series, we looked at the different types of images that are available to us to use as our base container images. In this post the second in the series we will look at adding a real application on top of our base image. As mentioned in our earlier post we are going to choose one compiled language, in this case, it will be Golang. In the next post in the series, we will tackle a dynamic language (Python).
So we are going to take a really simple Golang application that is basically going to serve a static HTML file. Don't get too caught up in the constructs of the application. The most important thing is that it compiles, is small and is quick to build so we can see what container base image is the best to use.
Before we choose which OS to start with one of the best practices to start using for a compiled languages is Docker multi-stage builds. What multi-stage builds give us is the ability to build our binary in one container. This will usually include all the build tools as well. Then copy that binary to a fresh container. So ditching all the build tools and unwanted packages you need to run a production application. For more information on multi-stage builds please go here
With all the different base images we will use multi-stage builds to try to keep our deployable image as small as possible. So in each of the Dockerfiles we will compile our Go binary in the build container then use multi-stage build to move it to the last container that will be our deployment artefact.
Now the first base container image that we will choose is the Full OS because if you are new to containers this will be the easiest to get up and running.
FROM golang:1.11-stretch as build
WORKDIR /go/src/github.com/scottyc/webapp
COPY web.go web.go
RUN CGO_ENABLED=0 GOOS=linux go build -o ./bin/webapp github.com/scottyc/webapp
FROM debian:stretch
RUN mkdir -p /web/static/
COPY --from=build /go/src/github.com/scottyc/webapp/bin/webapp /usr/bin
COPY index.html /web/static/index.html
WORKDIR /web
EXPOSE 3000
ENTRYPOINT ["webapp"]
This Dockerfile will result in an image that is 107mb
and remember from the first post had some critical vulnerabilities in the image. Now let’s move onto the slim OS image
FROM golang:1.11-stretch as build
WORKDIR /go/src/github.com/scottyc/webapp
COPY web.go web.go
RUN CGO_ENABLED=0 GOOS=linux go build -o ./bin/webapp github.com/scottyc/webapp
FROM debian:stretch-slim
RUN mkdir -p /web/static/
COPY --from=build /go/src/github.com/scottyc/webapp/bin/webapp /usr/bin
COPY index.html /web/static/index.html
WORKDIR /web
EXPOSE 3000
ENTRYPOINT ["webapp"]
Now you will notice that we used the full OS still as the build image which is totally fine as we will discard that anyway, this build will result in an image which is 61.9mb
in size. So that is quite a reduction without changing any of the application code. We still need to remember the image has vulnerable packages in it. Now let’s try Alpine Linux.
FROM golang:1.11.2-alpine3.8 as build
WORKDIR /go/src/github.com/scottyc/webapp
COPY web.go web.go
RUN CGO_ENABLED=0 GOOS=linux go build -o ./bin/webapp github.com/scottyc/webapp
FROM alpine:3.8
RUN mkdir -p /web/static/
COPY --from=build /go/src/github.com/scottyc/webapp/bin/webapp /usr/bin
COPY index.html /web/static/index.html
WORKDIR /web
EXPOSE 3000
ENTRYPOINT ["webapp"]
Now this build results in an image size of 11mb
and has no vulnerable packages in the image. So as you can see moving to Alpine has greatly decreased the image size and increased our security posture. Last but not least let’s look at scratch.
FROM golang:1.11.2-alpine3.8 as build
WORKDIR /go/src/github.com/scottyc/webapp
COPY web.go web.go
COPY index.html /web/static/index.html
RUN CGO_ENABLED=0 GOOS=linux go build -o ./bin/webapp github.com/scottyc/webapp
FROM scratch
COPY --from=build /go/src/github.com/scottyc/webapp/bin/webapp /usr/bin/webapp
COPY --from=build /web/static/index.html /web/static/index.html
EXPOSE 3000
ENTRYPOINT ["/usr/bin/webapp"]
Now you will notice that we use Alpine to build our binary as scratch has no tools or package manager. So we will use the build tools in Alpine then move the binary into the scratch container. The scratch container will only contain the Go binary. This will result in an image size of 6.59mb with the best security posture. So as you can see just by choosing a different container base image we have yielded greatly different results with the same application code. If you want to take any of these images for a test drive please visit my Github page. In the next post in the series, we will look at dynamic languages and use Python.
For further learning about containers or Kubernetes checkout out my OSS workshop.
Top comments (0)