Optimizing the docker image size for compiled language applications can be done using multi-stage docker build.
This article is written based on an example application written in Golang. This multi-stage build can be applied to any other language such as C#, Java, etc.
Why should we reduce docker image size
By optimizing and reducing the size of docker image, we can speed up the build and deployment of our containers. A simple way to understand this concept is to compare the speed of downloading a 1GB file vs a 10MB file while ignoring the network speed as a variable element.
A docker image containing only what we require also make the image more secure as there are lesser packages in the image that actually reduces the attack surface due to vulnerabilities in the packages.
Creating a helloworld application and using normal docker build
Let's start by creating a hello world application in Go.
package main
import (
"fmt"
)
func main() {
fmt.Println("Hello World")
}
Next, let's create a dockerfile to build the above application.
FROM golang:1.18.4-bullseye
ADD . /src
WORKDIR /src
RUN go build helloworld.go
Build the docker image.
docker build -t helloworld:dev .
Check the image size of the built image. You will be surprised a small application like this takes 822MB of space!
~# docker images | grep helloworld
helloworld dev ae548d70a93b 13 minutes ago 822MB
Using multi-stage build to optimize image size
Edit your dockerfile above to the following:
FROM golang:1.18.4-bullseye as builder
ADD . /src
WORKDIR /src
RUN go build helloworld.go
FROM debian:bookworm-slim
WORKDIR /app
COPY --from=builder /src/helloworld /app
ENTRYPOINT ["/app/helloworld"]
What was changed above is we introduce a builder stage and a final stage (for deployment) to the docker build process. This is because we do not require the Go SDK in the final stage as Go will compile to an executable file once built. As such, we would only require the executable file to be deployed to the final stage.
To further optimize the space usage, we also chose the slim image for debian bookworm which is the codename for Debian 12. A slim image includes only the minimal packages required to run the operating system, allowing us to reduce the image size significantly while also reducing the attack surface for the packages included.
An debian slim base image is only 72.5MB in size.
~# docker images | grep debian | grep bookworm
debian bookworm-slim 34cb971e12b4 4 days ago 72.5MB
Build the docker image.
docker build -t helloworld:dev .
Check the image size of the built image.
~# docker images | grep helloworld
helloworld dev 5b901b9d578d 4 seconds ago 74.3MB
Run the application.
~# docker run -it helloworld:dev
Hello World
There you go, you have successfully created a small docker image to run your application with the usage of multi-stage docker build.
Usage of alpine linux
The choice of operating system for your final stage is highly dependent on your requirements. To further reduce the image size, we can also use alpine linux which is a even slimmer version of linux designed to run on a floppy disk.
An alpine linux base docker image is only 5.27MB in size.
~# docker images | grep alpine
alpine latest 6e30ab57aeee 7 weeks ago 5.27M
Edit your dockerfile above to the following:
FROM golang:1.18.4-bullseye as builder
ADD . /src
WORKDIR /src
RUN go build helloworld.go
FROM alpine:latest
WORKDIR /app
COPY --from=builder /src/helloworld /app
ENTRYPOINT ["/app/helloworld"]
Build the docker image.
docker build -t helloworld:dev .
Check the image size of the built image.
~# docker images | grep helloworld
helloworld dev f323e5a8547e 3 seconds ago 7.07MB
Run the application:
~# docker run -it helloworld:dev
Hello World
Here we have it, a docker image consisting of 7.07MB of space to run our application!
You can refer to the files in GitHub for reference (https://github.com/alexlogy/multi-stage-docker-build-example).