How to Build Small Docker Images Containing Go Microservices
This tutorial shows you how to make small Docker images when building Go programs.
Sample Go program
I wrote two tutorials recently demonstrating Docker Compose and Traefik. They both used a small Docker image containing a simple Go REST API that returns a JSON string saying Hello World.
Let’s take a look at the code:
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
)
var version = "dev"
func main() {
appName := os.Getenv("APP_NAME")
if appName == "" {
appName = "DemoService"
}
port := os.Getenv("APP_PORT")
if port == "" {
panic("You must specify a port to listen on using the APP_PORT environment variable")
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Service-Name", appName)
w.Header().Set("Service-Version", version)
encoder := json.NewEncoder(w)
encoder.Encode(map[string]string{
"message": "Hello World!",
})
})
fmt.Printf("%s Version %s started listening on :%s\n", appName, version, port)
log.Fatal(http.ListenAndServe(":"+port, nil))
}
As you can see, the code doesn’t do much, it listens on a port specified by the APP_PORT environment variable and returns a JSON response to GET requests at /.
Even the small amount of code above can create large Docker images depending on how you build them. Let’s make the smallest image possible in the next step.
Create Dockerfile
Create a Dockerfile and add the following contents to it.
FROM golang:1.15.5-buster AS builder
ARG VERSION=dev
WORKDIR /build
COPY main.go .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -o demoservice \
-ldflags="-w -X=main.version=${VERSION}" main.go
FROM scratch
COPY --from=builder /build/demoservice /demoservice
ENTRYPOINT [ "/demoservice" ]
This Dockerfile uses a multi-stage build process. The first five lines are used to build the Go program and the last three copy the built program into a small scratch image.
In order for this to work correctly, we are statically compiling the Go program using CGO_ENABLED=0
. Read more about statically compiling Go programs here: https://www.arp242.net/static-go.html
We’ve also used -w
with -ldflags
to strip out debug info. This can make the image a little smaller but if you need debug info, remove this flag.
Build Docker image
Run the following to build the image:
docker build -t demoservice:1.0.0 . --build-arg VERSION=1.0.0

Note: specifying --build-arg VERSION=1.0.0
will assign 1.0.0 to the version variable in main.go at build time using -ldflags
.
Check its size
We can check it’s size by running:
docker images

As you can see from the screenshot above. The size of this image is only 5.08 MB. If we had of used a Debian slim image instead of scratch it would have been 74.3 MB.
We’ve saved a lot of space but does the program still work?
Let’s test it.
Test it works
Run the following command:
docker run --rm -e APP_PORT=8080 -p 8080:8080 demoservice:1.0.0

From another Terminal process run:
curl http://localhost:8080 -i

Conclusion
By using a statically compiled binary and copying it to a scratch image, we managed to save almost 70 MB of disk space creating a Docker image.