Harder, Better, Faster, Stronger Dockerfile

Sami Salih İbrahimbaş
GoTurkiye
Published in
5 min readJun 16, 2023

Step by Step and Logic

Earlier, I explained how we can minimize the dockerfile in a golang project. Today, I’m going to tell you how we can make it work faster.

Article Cover

First of All

I have explained how to write a better dockerfile before. Make sure you read this!

Introduction

In this section we will remember, question and gain some things before diving into the code.

What Brought Us Here?

Docker has made our lives much easier with its perspective, ease and security. So is it perfect? If you can configure it properly yes!

To be able to use something that works well, it is necessary to understand how it works. For example, if you are trying to drive and do not know the working principles of the car, you will be unsuccessful or inefficient.

That’s exactly what we’re going to do today as we reduce the size and speed up your Dockerfile files. Here we will apply some techniques that we have seen in different parts of our lives before!

What Does a Dockerfile Basically Do?

Dockerfile; It compiles our project into an image so that it can be run with Docker. Then when Docker is triggered it will run this image.

Of course, Dockerfile cannot recognize your project, know the requirements and produce solutions. Here, a person who knows the project should be involved.

To be more specific, our Dockerfile basically has two stages. These are compilation and execution respectively.

Docker will create a container for you that emulates an operating system. This is blank at first. If your project cannot compile or run with an empty container, you must install the required dependencies.

Docker comes with some ready made images with DockerHub. For example, these could be the language you will be using or a database. If you want to run a golang project with Docker, you can use the golang image published on DockerHub.

Compilation Stage

After choosing your software language during the compilation stage, one important issue is dependencies. Although this field varies according to the language you use, almost all languages have a package manager system. With this package manager, they bring the packages you depend on to your computer from other servers. Docker containers are initally empty, so you will need to re-pull these dependencies each time the container is created.

If your Docker project takes too long to compile, it’s probably because it’s constantly pulling external dependencies. It can also lead to high internet consumption.

Actually, this is a familiar situation for people who write API. Constantly going to the datasource causes performance loss. We eliminate or reduce this performance loss with the cache mechanism.

And that’s exactly what we’re going to do with Docker. The language dependencies we use should go if there are no dependencies in the cache, instead of trying to go every time the container is created.

After downloading our dependencies, we will compile our project with the base image we use and move on to the next stage.

Execation Stage

At this stage, we will need to run the output we obtained in the previous stage.

This is easy for languages with executable output like Golang or Java. Because only when we type the build name, the project will run. If you’re using interpretable technologies like NodeJS, you’ll need it to interpret it as it works here by being interpreted.

Docker has an image called scratch so that we can start tier two with minimal requirements. We will execute it using this and we will have the minimum size Docker image.

The Example Dockerfile

In this section, we will write a sample dockerfile and apply the knowledge we remember and acquired above.

Compilation Stage

At this stage, we we’ll explain the build phase of our Dockerfile and what each line does.

FROM golang:1.20-alpine AS builder

With the above line, we have informed Docker that we will use golang’s Docker image with 1.20-alpine tag for the builder stage.

Click here to access this image in DockerHub.

WORKDIR /
ENV CGO_ENABLED=0

By marking Workdir as / we have haken the directory where we run this Dockerfile as the root directory. With the CGO_ENABLED=0 line, we turn off the cgo feature so that there is no error on windows machines during the compilation stage.

RUN --mount=type=cache,target=/go/pkg/mod \
go mod download

Golang keeps dependencies in /go/pkg/mod . With the code above, we are telling golang to use this directory on our computer when the container is rebuilt. Thus, any library downloaded once will not be downloaded the next time, it will be taken from local.

COPY . .

With this code, we copy our project files to our docker container.

RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go build -o main ./main.go

In order to use our dependencies and previous compilations with this code, we perform a cache mount operation and compile our project.

Execution Stage

In this section, we will explain the execution process of our project.

FROM scratch

As I mentioned earlier, we are using this custom image that docker provides for an empty container.

Click here to access this image via DockerHub.

This image is not supported by AWS, but for languages that output executables like golang and java, your base image here isn’t much of a difference. The following steps are 99% the same!

ENV PORT 8080

With this code, we get the port to broadcast with env and give the default value as 8080 .

COPY --from=builder /main .

We copy the executable output we compiled from the builder stage.

EXPOSE $PORT

We expose the port we get with env.

CMD ["main"]

We are executing our executable output!

The Full Code

Here is the full version of the code we wrote.

FROM golang:1.20-alpine AS builder

WORKDIR /
ENV CGO_ENABLED=0
COPY ./go.* ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
COPY . .
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go build -o main ./main.go

FROM scratch

ENV PORT 8080

COPY --from=builder /main .

EXPOSE $PORT

CMD ["main"]

Testing

In this section, we will test the dockerfile we created above.

Let’s compile our project with the following command.

docker build -t test .
Build result

Here is the result I got on my second build above! A total of 0.8 seconds and as you can see the dependencies pull and build process is cached because I didn’t change anything in the code!

Build overview on the docker desktop

And above is the total size of our image in Docker Desktop! Only 5.81 MB! I’m sure that even a 10-second video on most of us will now double that size!

Last Words

I could have given the code in this article, but I wanted to tell what I know. I am sure that someone who reads this article to the end will have no trouble writing more complex Dockerfiles!

My next post will be on how to use Golang Private packages on Docker. Until then, take care of yourself.

Looking forward to your support!

--

--

Sami Salih İbrahimbaş
GoTurkiye

lifetime junior • software developer at monopayments