In this post, we are going to see how we can reduce the size of our final docker image size for our golang app. To achieve this, we are going to be using the scratch docker image. This is going to reduce the size of the final image by over 95%.
Why? By reducing the size of our final docker image, we are reducing the attack surface area. This means that attackers have less libraries, tools to exploit in our docker image. For this to be possible, we will need to statically compile our go application, so that it doesn’t need any external libraries.
To achieve this, we are going to use two stages to build our final docker image. In the first stage, we are going to use the official image for Golang, to statically compile our go application. Then, in the second stage, we will copy our statically compiled binaries from the first stage. To get started, we are going to create a Dockerfile at the root of our project. We are going to name it scratch.dockerfile – feel free to change the filename, if you wish to.
NB: If you are new to docker and containers in general, I suggest you start by reading the following post here for the basics. You can also learn how to get started with docker here.
In order to make a meaningful demo, I needed a barebone Golang application. Therefore, I used the basic example from Gin Web Framework which can be found here. I didn’t make any modification to the above code. I am confident enough you can replace it with your own custom code, without any modifications.
Stage 1 – Building the Golang Application
In this stage, we are going to compile our go application statically. But first things first, inside our dockerfile, we will start by setting the base image for docker. We will be using the latest available image for Golang; hence no tag. We will also name our stage as
builder, this makes it easier to refer to it in the next stage.
Next, we are going to change our working directory, to the directory in which we will build our application. I recommend using the same structure as your dev environment, from the GOPATH. For the Golang images, the GOPATH is
/go. This will ensure that our go application behaves the same as it does in our dev environment.
Next, let’s copy our projects files, and install our projects dependencies and build our go application.
COPY . .
RUN go get .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
TIP: If you are using go modules, I recommend copying the project code in a directory outside the GOPATH (
/go/src). This is because, once you put it inside the
GOPATH, go modules will be disabled. This may cause your application to break.
COPY . .
Stage 2 – Deployment Image
In this stage, we are going to build the image which we will be deploying. So, let’s set our base image as scratch –
FROM scratch. Naming the stage is not necessary since it’s the last stage in our multi-stage build.
Next, we are going to copy
ca-certificates and compiled Golang static binaries from stage one – builder. First, let’s copy the
ca-certificates, this is important if you will be using SSL/TLS to access your application. Otherwise you can skip this one if your app is not using SSL/TLS.
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
Then, change the working directory to bin (or any other name of your choice), and copy the compiled binaries to that directory:
COPY --from=builder /go/github.com/coding-latte/golang-docker-multistage-build-demo/app .
And finally, set our go app to be executed when the container starts and expose port 8080.
If your app is using another port, replace port 8080 with that port. And if it’s using multiple ports, separate them with a single whitespace.
NB: You can find the complete dockerfile here. And if you would rather use alpine linux docker image instead of scratch, you can find the corresponding dockerfile here.
Building the Docker Image
We can now go ahead and build our docker image:
docker build --rm -f "**scratch.dockerfile**" -t demo:latest .
NB: If you are not using scratch.dockerfile as the name of your dockerfile, replace that with correct name and path in the above command. You can learn more about building docker images here.
So, our final docker image size when using scratch docker image is 15.5MB, as compared to 844MB when using official Golang docker image. If you used alpine image instead of scratch, you can add just an extra 5MB on top of the scratch final size, where you will get about 20MB. This is still a huge size reduction from the official docker image for golang.
You can find the source code for this post here.