Optimize Golang Docker images [Multi-Stage Build]


GO, GOLANG Solutions

Reviewer: Deepak Prasad

Introduction

You have built your application and it's now time to package and deploy your application to the cloud so that everyone can use it. But how would you package your application in preparation for deployment? Today you will learn how to package your application using Docker.

 

Docker container

In simple terms , Docker is a tool that allows developers to easily package and deploy their application in a container to run on a host operating system in the cloud. But what is a container ? Docker container is a lightweight software package that is executable in nature that only contains code, runtime,system tools , system binaries and settings to run an application.

 

Docker image

In order to build a docker container , you need a set of instructions that will build a container. Docker containers are built from docker images. A docker image is a read only template that contains a set of instructions for creating a docker container.

 

Multistage

In this article, you will learn how to build docker images and containers using multiple stages for a simple Go application that handles HTTP requests. Multistage will help us reduce the size of our docker images by over 90% , which is a very good thing as this small image saves you running costs after deployment to the cloud.

 

Prerequisites

  1. Go version 1.17 or higher
  2. Docker installed
  3. Code editor of your choice, I will use Visual Studio Code

 

Application structure

You will build a very basic application using Go. The application consists of a main.go file, go.mod and Dockerfile. Navigate to your working directory using the terminal and follow the following steps.

Create a working directory and navigate into it

mkdir go-multistage-docker && cd go-multistage-docker

Create main.go file and Dockerfile

touch main.go Dockerfile

Initialize go module

go mod init go-multistage-docker

 

Build Go application

In this section you will build a HTTP server that has a single home route. You will use the net/http package to handle requests. Enter the below code in the main.go file.

package main


import (
   "fmt"
   "log"
   "net/http"
)


func handler(w http.ResponseWriter, r *http.Request) {
   fmt.Fprint(w, "Hello from Go Docker multistage")
}


func main() {
   http.HandleFunc("/", handler)
   fmt.Println("server running at 0.0.0.0:8080")
   log.Fatal(http.ListenAndServe("0.0.0.0:8080", nil))
}

In the above code , we import the “fmt” package to enable you to print some messages to the terminal and the browser using the Println() and Fprint() methods respectively. Lastly , you import the net/http package to help you handle incoming requests.

The handler function is responsible for handling incoming requests. It takes in a response writer (http.ResponseWriter) and a pointer to request (*http.Request) as arguments. The fmt.Fprint() method writes to the writer (w) the string “hello from Go Docker multistage”. This is what will be rendered in the browser.

The main function is the starting point of our simple server. Using the http.HandleFunc() method, we route requests to “/” to the handler function defined earlier.

Ensure you are in the root folder of your application (same location as the main.go file) using the terminal and enter go run main.go

$ go run main.go
server running at 0.0.0.0:8080

To check if the server is running , open your browser and enter http://0.0.0.0:8080/ in the address bar. You will see the text “Hello from Go Docker multistage” text in the browser as shown below.

Optimize Golang Docker images [Multi-Stage Build]

 

Build Docker Image

In this section, you will write some instructions for building docker images in the Dockerfile that we created earlier. Add the below code in the Dockerfile.

FROM golang
WORKDIR /src/app
COPY go.mod main.go ./
RUN go build -o /server
CMD ["/server"]

Dockerfile contains instructions for creating a docker container. These steps are discussed below.

  • FROM golang:  This instructs docker download golang runtime into the container from Docker hub.
  • WORKDIR /src/app : Creates a folder in our container where all our code will be stored.
  • COPY go.mod main.go ./ : Copies the go.mod and main.go files in the working directory that was created in the previous step
  • RUN go build -o /server: Runs the Go command to build Go binaries and save the binaries in the /server directory. The -o flag defines the directory where the binaries will be saved
  • CMD [“/server”]: Runs the server. Remember docker containers are executable , therefore this command executes the code in the container.

 

Build a docker image

The next thing to do is to build an image from the above docker instructions. In your terminal, same location as your Dockerfile and main.go file, issue the below command docker build -t docker-multistage .

$ docker build -t docker-multistage .
Sending build context to Docker daemon  4.096kB
Step 1/5 : FROM golang
---> 8295ad49803f
Step 2/5 : WORKDIR /src/app
---> Using cache
---> 139f2f1ee298
Step 3/5 : COPY go.mod main.go ./
---> Using cache
---> 777089d1871d
Step 4/5 : RUN go build -o /server
---> Using cache
---> 658016ff6d3c
Step 5/5 : CMD ["/server"]
---> Using cache
---> 435bad0e3765
Successfully built 435bad0e3765
Successfully tagged docker-multistage:latest

The above command instructs the docker client in your machine, to build and tag an image called docker-multistage using Dockerfile in the current directory. The build command build and image, the -t docker-multistage assigns a name to your image and the period (.) indicates the location of your Dockerfile, which is the root directory. To view the docker image issue the command docker images | grep docker-multistage. The result will show that the docker image has a size of 999MB.That is a very big sized docker image. In the next section we will work to reduce the size of the image.

$ docker images | grep docker-multistage
docker-multistage   latest    435bad0e3765   About a minute ago   999MB

 

Run docker container

Next thing to do is to run the container to ensure that it is working correctly. In your terminal issue the command<b> docker run -p “8080:8080” docker-multistage</b>

$ docker run -p "8080:8080" docker-multistage
server running at 0.0.0.0:8080

Open your browser and enter http://0.0.0.0:8080/ in the address bar. You will see the text  “Hello from Go Docker multistage” text in the browser as shown below.

The command asks the docker client to run the docker container by opening port 8080 on the host machine and passing requests to port 8080 in the container.

Optimize Golang Docker images [Multi-Stage Build]

All seems well up to this point. The next goal is to reduce the docker image size.

 

Reduce Docker Image size

1: Use Alpine Base Image

Your docker image size at the moment is 999MB. In this section you will reduce the size of the image by over 50%. This is a great achievement as far as the image size is concerned.

In order to reduce the image size, we are going to replace the Docker Golang base image and use Golang Alpine Image. According to Docker, alpine image is a minimal docker based on Alpine linux with complete package index and only 5MB in size. That is the only thing we need to do . Replace FROM golang with FROM golang:alpine in your Dockerfile.

FROM golang:alpine
WORKDIR /src/app
COPY go.mod main.go ./
RUN go build -o /server
CMD ["/server"]

Next thing is to build the docker-multistage image . Issue the command docker build -t docker-multistage .  in the terminal as shown below

$ docker build -t docker-multistage .
Sending build context to Docker daemon  4.096kB
Step 1/5 : FROM golang:alpine
alpine: Pulling from library/golang
8921db27df28: Already exists
a2f8637abd91: Pull complete
d48e7ca896ec: Pull complete
4f26d270037d: Pull complete
Digest: sha256:2381c1e5f8350a901597d633b2e517775eeac7a6682be39225a93b22cfd0f8bb
Status: Downloaded newer image for golang:alpine
---> feb4bbda921c
Step 2/5 : WORKDIR /src/app
---> Running in 8fb99f8a5fe5
Removing intermediate container 8fb99f8a5fe5
---> 78c0f65bf444
Step 3/5 : COPY go.mod main.go ./
---> cee7342cbce0
Step 4/5 : RUN go build -o /server
---> Running in fdb51e5acf97
Removing intermediate container fdb51e5acf97
---> adf73b97d75f
Step 5/5 : CMD ["/server"]
---> Running in 62004386ca6c
Removing intermediate container 62004386ca6c
---> f65d28ae8e4a
Successfully built f65d28ae8e4a
Successfully tagged docker-multistage:latest

This is a very good result, the image size is 361MB. We can even reduce it further by using multistage.

 

2. Reduce Docker Image size using Multi-Stage

In this section you will add more commands to the Dockerfile. We will build the docker image using stages hence the name multistage.

FROM golang:alpine as builder
WORKDIR /src/app
COPY go.mod main.go ./
RUN go build -o server
FROM alpine
WORKDIR /root/
COPY --from=builder /src/app ./app
CMD ["./app/server"]

In the above Dockerfile you will use golang:alpine base image and tag it as builder.

  • FROM golang:alpine as builder: This docker image will use Golang Alpine as the base image and tag it as builder. It is tagged because it will be used later for referencing. This is the first stage of the build process. At the end of this stage the builder image will host our code binary in the /server file. The rest of the commands remain untouched.
  • FROM alpine: This the second stage of the build process.Alpine is the minimal image from Docker and it's about 5MB in size.
  • WORKDIR /root/ : In this new base image you create a working directory just like the previous image.
  • COPY –from=builder /src/app . : Copies binaries from the builder image that was created in the previous stage and paste/move them in the working directory using the period (.). In the builder image, binaries were stored in /src/app
  • CMD [“./app/server”] : Run the application

Now build the new multistage image

$ docker build -t docker-multistage .
Sending build context to Docker daemon  4.096kB
Step 1/8 : FROM golang:alpine as builder
---> feb4bbda921c
Step 2/8 : WORKDIR /src/app
---> Using cache
---> 78c0f65bf444
Step 3/8 : COPY go.mod main.go ./
---> Using cache
---> cee7342cbce0
Step 4/8 : RUN go build -o server
---> Using cache
---> c41ed3af2a4c
Step 5/8 : FROM alpine
---> 042a816809aa
Step 6/8 : WORKDIR /root/
---> Using cache
---> f4a4016d9345
Step 7/8 : COPY --from=builder /src/app ./app
---> Using cache
---> 9a1a644b81d5
Step 8/8 : CMD ["./app/server"]
---> Running in cc36e17444f6
Removing intermediate container cc36e17444f6
---> 00f71b20b745
Successfully built 00f71b20b745
Successfully tagged docker-multistage:latest

Check the image size

$ docker images | grep docker-multistage
docker-multistage       latest       00f71b20b745   5 minutes ago    13.6MB

This is amazing results, the image size is 13.6MB. Now we can deploy our docker image to the cloud and run it at an affordable fee.

 

Conclusion

In this article, you learn how to package Go applications in a docker image. The first step is to build a docker image using the official Golang which is 999MB in size. The second step is to build the image using Golang Alpine image which reduces size to 361MB. The final step you use Docker multistage to reduce the size to 13.6MB.

 

References

https://hub.docker.com/_/alpine

 

Antony Shikubu

Antony Shikubu

He is highly skilled software developer with expertise in Python, Golang, and AWS cloud services. Skilled in building scalable solutions, he specializes in Django, Flask, Pandas, and NumPy for web apps and data processing, ensuring robust and maintainable code for diverse projects. You can reach out to him on his LinkedIn profile.

Can't find what you're searching for? Let us assist you.

Enter your query below, and we'll provide instant results tailored to your needs.

If my articles on GoLinuxCloud has helped you, kindly consider buying me a coffee as a token of appreciation.

Buy GoLinuxCloud a Coffee

For any other feedbacks or questions you can send mail to admin@golinuxcloud.com

Thank You for your support!!

Leave a Comment