Smaller image size helps you:
- make faster builds on your CI
- testing containers becomes faster
- less cost for build jobs / container space
. . .
Here's some quick hacks I found after playing around for a while with docker images
In this example, I'm shrinking an image of about ~900 MB to around ~8 MB (as tested on my docker)
For reference, I'll take this simple sample Dockerfile:
FROM golang
WORKDIR /app
COPY main.go .
RUN go build -v -o server
ENTRYPOINT [ "/app/server" ]
Which simply builds a go program, and runs it.
This sizes around 946 MB after building
1 - Base Image as Alpine
Alpine is a very small Linux distro used for containers
So most images you want to run, will have an alpine base equivalent of them. Where to find them?
For example, for the golang image from dockerhub:
You can choose the tags here to choose an alpine image.
So the Dockerfile with alpine base will be:
FROM golang:1.24-alpine #just any alpine tag from the tag list
WORKDIR /app
COPY go.mod .
COPY main.go .
RUN go build -v -o server -buildvcs=false
ENTRYPOINT [ "/app/server" ]
With this, the image build takes around 353MB size (3x smaller already)
2 - Multi Stage builds
I like to think of this like a:
multistage rocket, where there's multiple sections and after certain intervals, the sections push the rest, cutting itself off, one by one, until there's one final portion left.
Similarly, suppose in our go-program,
- we need the go compiler to build it first of all
- once its build, only the executable is needed (compiler or dependencies not needed during runtime may be dropped off)
Let's look at this with a Dockerfile:
# Stage 1: build
FROM golang:1.24-alpine AS builder-stage
WORKDIR /app
COPY go.mod .
COPY main.go .
RUN go build -v -o server -buildvcs=false
# Stage-2: run
FROM scratch
WORKDIR /app
COPY --from=builder-stage /app/server /app
ENTRYPOINT [ "/app/server" ]
- In stage-1 -> we build the "server" executable and it will be placed at /app/server (call this "builder" stage
- Then in 2nd stage -> take that executable from builder stage and simply run it.
In the 2nd stage, a base image called "scratch" is used, it is even more lightweight than alpine. But only used for cases like this, where there's literally no dependencies needed, just to run.
After this build, only the 2nd stage remains as final image.
Once we build this, the image size shrinks to 8.15MB! (almost 300x)
Limitations
This seems too good to be true, and it is yeah. Not for all cases can we optimize up to this extent.
Some cases, we simply need all the dependencies in run time as in compile time too (ex: interpreted languages like Python - although there's build tools for that, but limited), so it's tricky to use multi stage builds.
Repositories like Dockerhub also have compression on their end to optimize the size during deployments.
Even in the case of base image as alpine, some tools might not have great support in alpine
Trying it yourself
To replicate the following on your own,
Here's command to build the Dockerfile:
docker build -t <image-name:tag> .
And the go program I used for this (a very simple HTTP server):
package main
import (
"fmt"
"log"
"net/http"
)
func greet(w http.ResponseWriter, req *http.Request) {
fmt.Fprintf(w, "hello world\n")
}
func main() {
http.HandleFunc("/", greet)
fmt.Printf("Starting server at port 8090\n")
if err := http.ListenAndServe(":8090", nil); err != nil {
log.Fatal(err)
}
}
Top comments (0)