Update Oct 2020: multi-stage builds now provide a standard way to leverage a fat build image, while allowing your published image to remain small. This article is still useful for comparing base image sizes.
GoLang applications are a great architectural fit for a Docker container because of their single binary executable. But you need to be aware that the base image that you choose for your GoLang binary will greatly affects the size of the Docker image.
You can use the heavy weight golang base image available from Docker Hub. This contains an entire base OS filesystem along with the an installation of the GoLang tools and packages. Your Dockerfile copies the .go source code into the container, downloads any dependencies, and does the full build inside the container itself. This can result in an image hundreds of megabytes large.
The alternative is that you can install the GoLang compiler to your local host computer, and use the local compiler to build the GoLang binary. Then using either the minimalist scratch image or a purposely small filesystem like Alpine as the base, you can copy just your single GoLang binary to the container. This results in a very small docker image.
As long as there are no arcane library dependencies or filesystem path assumptions introduced by your custom code, going with the smaller image size is typically advantageous.
Prerequisite
This article assumes you have installed Docker as described in my article here.
OPTION 1: Build binary inside container
Download my Docker hello world github project.
$ sudo apt-get install git -y $ git clone https://github.com/fabianlee/docker-my-hello-world-golang.git $ cd docker-my-hello-world-golang
If you cat the “Dockerfile” you will see it tells Docker to use the base image “golang:latest”, copy the contents of the current directory into the container, run the GoLang compiler to build the executable, and then set the binary as the main process of the container. This all happens inside the container, and not on your host machine.
FROM golang:latest RUN mkdir /app WORKDIR /app COPY . . RUN go build -o main . CMD ["/app/main"]
Run the steps below to build the Docker image, this will take a while if you have never run this step before because the “golang:latest” image is hundreds of megabytes.
$ docker build -t my-hello-world .
If you want to see the image run, use this command:
$ docker run -it my-hello-world hello, world
To view the Docker image size, use the command below.
$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE my-hello-world latest f447222c719e 11 minutes ago 798MB
You can see that this image is 789MB (bigger than a CD), and that is because it contains an entire operating system as a base layer along with all the GoLang tools and package dependencies.
OPTION 2: Build binary locally, put binary into container
The other approach would be to build the GoLang binary locally, and then place this single non-linked executable into a minimalist container.
Prerequisites
You need to install GoLang on your local host machine. Follow the instructions for Ubuntu 16.04 as described in my article here, or Ubuntu 14.04. This includes setting the $GOPATH and $GOBIN environment variables.
Build the GoLang binary locally
First, grab the project off github:
$ sudo apt-get install git -y $ mkdir -p $GOPATH/src; cd $_ $ git clone https://github.com/fabianlee/docker-my-hello-world-golang.git $ cd docker-my-hello-world-golang
Then build the static, non-linked binary using the instructions below:
$ export CGO_ENABLED=0 $ go build -o main . $ ./main hello, world $ ldd ./main not a dynamic executable
Put Binary into scratch container
The scratch image, available in the Docker Hub is meant for the most minimal image requirements. Our hello world project certainly falls under that category, so let’s see how small an image we can construct.
Below is the Dockerfile.scratch which copies the “main” executable to the root path.
FROM scratch ADD main / CMD ["/main"]
Build/run the image:
$ docker build -t my-hello-world-scratch -f Dockerfile.scratch . $ docker run -it my-hello-world-scratch hello, world
And check the image size:
$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE my-hello-world-scratch latest cee1f1ea8163 About a minute ago 2.02MB my-hello-world latest f447222c719e 21 minutes ago 798MB
The “my-hello-world-scratch” image is only 2.02Mb compared to the 798Mb of the “golang:latest” image. If we truly don’t need any of the supporting libraries or niceties of Ubuntu inside the container, this is a huge savings.
Put binary into Alpine container
Instead of using the scratch image, let’s use an alpine base image that is purpose-made to have a light footprint (~5Mb) but still provide tools and utilities that make working within a container possible.
Below is the Dockerfile.alpine which copies the “main” executable to the /app path.
FROM alpine:latest RUN mkdir /app WORKDIR /app COPY . . CMD ["/app/main"]
Build/run the image:
$ docker build -t my-hello-world-alpine -f Dockerfile.alpine . $ docker run -it my-hello-world-alpine hello, world
And check the image size:
$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE my-hello-world-alpine latest 65de4ed20940 19 seconds ago 6.17MB my-hello-world-scratch latest cee1f1ea8163 2 minutes ago 2.02MB my-hello-world latest f447222c719e 22 minutes ago 798MB
The image based on Alpine is only 6.17Mb, which is slightly larger than scratch, but orders of magnitude smaller than one based on the full blown GoLang image.
REFERENCES
https://blog.codeship.com/building-minimal-docker-containers-for-go-applications/ (build inside heavy container vs copy binary into light container)
https://docs.docker.com/samples/library/golang/
https://hub.docker.com/_/golang/
NOTES
removes stopped containers
docker rm $(docker ps -aq)
removes all stopped containers, volumes not used, images without container associated
docker system prune