Github

Building Small Docker Images for Go

Let's take this simple Go web app:

package main

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

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		hello := fmt.Sprintf("hello %v", time.Now().UnixNano())
		fmt.Fprintf(w, hello)
		log.Println("Received", hello)
	})

	log.Println("Starting server on http://localhost:80")
	log.Fatal(http.ListenAndServe(":80", nil))
}

And build a Docker image for it:

FROM golang:1.8.1-alpine
RUN mkdir /app 
ADD . /app/ 
WORKDIR /app 
RUN go build -o goapp
EXPOSE 80
ENTRYPOINT ./goapp

This very simple Go app will end up with a Docker image of 263MB!

$ docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
mjp/dockerfat       latest              af3a6a992d2e        25 seconds ago      263MB

That’s because this Docker image contains not only your app, but everything needed to build it (including the Go compiler, GCC, etc).

Docker now supports multi-stage builds, which means you can create an image to build your Go app, and then copy it into another, more minimal image for deployment. Here’s how it works:

# build stage
FROM golang:1.8.1-alpine AS build-env
ADD . /src
RUN cd /src && go build -o goapp

# final stage
FROM alpine
WORKDIR /app
COPY --from=build-env /src/goapp /app/
EXPOSE 80
ENTRYPOINT ./goapp

This uses the full Go docker image to build the app, but the much smaller alpine image to actually run it.

And the resulting image:

$ docker image ls
REPOSITORY          TAG               IMAGE ID            CREATED             SIZE
mjp/dockerslim      latest            46af8ae936ef        7 seconds ago       9.88MB

Only 9.88MB!