Recently I’ve been writing a service in Go to enhance the projects dashboard on Bitbucket – if you haven’t heard we launched Atlassian Connect for Bitbucket as a way for anyone to build add-ons for three millions of Bitbucket users out there. Like many other Gophers I’ve been happily deploying my Go services using Docker. The process is smooth and pleasurable if not for one thing: the size of the official default Go image.
After all is said and done my application – which by itself would comprise of around
~6MB of static binary in size, becomes a whopping
642MB when using the default Go Docker image. Our internal Docker registry handles that with no problems but it seems such a waste of space.
Recently I found this clear article detailing a Go, Docker workflow with clear instructions and snippets showing how to statically compile an application and shrink it to
1% of the size. The technique is elegant and simple enough but because my development system is OSX that approach needs to be modified with an extra layer of complexity. I need to manage cross-compilation of my Go project across OS architectures (OSX, Linux). I did some research and attempts at using gonative, but ended up going the Docker route to solve everything.
While working through the problem I remembered an older article from Xebia that did something smart: perform the build and link step inside Docker containers and store the (now compatible) binary in a
scratch image. The
scratch image is the smallest possible Docker image and it’s generally used to build base images or to contain single binaries.
So that’s what I set out to replicate with the new insight from the former article. I ended up with a streamlined process which automates everything smoothly:
- Write a multi-purpose
Makefileto both setup the build environment inside Docker and statically compile the Go application (read more about it below).
- Create a
Dockerfileto build the statically linked Go binary (called “
- Run it and extract the Linux binary using “
- Create a bare bones
Dockerfilethat adds the binary to a “
scratch” Docker image, plus the needed static web application files (“
- Profit! Run application using Docker.
Here a breakdown of the steps in detail.
Write a multi-purpose Makefile
Makefile will be capable of doing several things:
- Collect the dependencies needed by our Go application.
- Assemble the right Docker container to build our statically linked Go binary.
- Build our Go program.
- Inject the binary and the application static assets into a minimal Docker image.
The interesting bit here is that the same
Makefile will be used both to create the build container and as configuration inside the container for the compilation command (if you want you’re free to split the two logical uses in separate Makefiles but I found it delightfully efficient to keep only one).
Here’s how the
Makefile looks like:
default: builddocker setup: go get golang.org/x/oauth2 go get golang.org/x/oauth2/jwt go get google.golang.org/api/analytics/v3 buildgo: CGO_ENABLED=0 GOOS=linux go build -ldflags "-s" -a -installsuffix cgo -o main ./go/src/bitbucket.org/durdn/project-name builddocker: docker build -t durdn/build-project-name -f ./Dockerfile.build . docker run -t durdn/build-project-name /bin/true docker cp `docker ps -q -n=1`:/main . chmod 755 ./main docker build --rm=true --tag=durdn/project-name -f Dockerfile.static . run: builddocker docker run -p 8080:8080 durdn/project-name
golang Docker image expects the Go code to be stored in “
./go/src/...” The build flags specify you want a static binary. The
builddocker step does the following:
- Build a container (tagged
durdn/build-project-name) with the Go tool chain and the dependencies included.
- The build step will compile the Go application statically.
- Generate a container from the resulting image:
docker run durdn/build-project-name /bin/true.
- Extract Linux static binary generated:
docker cp $(docker ps -q -n=1):/main .
- Make it executable:
chmod 755 ./main.
- Copy the binary and the static assets into a minimal image.
Makefile with the simple:
Build the static Linux binary in a container
Makefile uses two separate Dockerfiles as already mentioned. Let’s have a look at the
FROM golang ADD Makefile / WORKDIR / RUN make setup ADD ./collector /go/src/bitbucket.org/durdn/project-name/collector ADD ./dashboard /go/src/bitbucket.org/durdn/project-name/dashboard RUN make buildgo CMD ["/bin/bash"]
Dockefile allows us to build the static Go binary calling
make. If you want to kick off the build manually you can simply type:
docker build -t durdn/app-name -f ./Dockerfile.build .
This will generate the cross-compiled binary executable as
./main inside the container.
Create tiny Go Docker image
The last step is to create a minimal Docker container and put our binary into it. For this we we can use the very tiny
tianon/true or the
scratch image mentioned before. This is the magical step that allows to shrink the application image hundredfold.
Dockerfile.static for this step is pretty straight forward:
# Create a minimal container to run a Golang static binary FROM tianon/true MAINTAINER Nicola Paolucci "email@example.com" EXPOSE 8080 COPY certs/certs /etc/ssl/certs/ca-certificates.crt COPY dashboard/config.json /config.json COPY dashboard/properties.json /properties.json ADD dashboard/dashboards /dashboards ADD dashboard/public /public ADD dashboard/widgets /widgets ADD main / ENV PORT=8080 CMD ["/main"]
Run it like this:
docker build --rm --tag=durdn/project-name -f Dockerfile.static .
As explained in the Docker workflow mentioned before, the certificates are needed if we want the application to run smoothly in a cross architecture setting.
After the build command the application can be started as you would expect with:
docker run -p 8080:8080 da-dashboard --config=config.json
The end result is beautiful, a Docker image weighting
8.6MB including all the static assets. I know it’s a small thing but it makes me feel so accomplished.
Find an example of the setup in this small Git repository.
Liked this piece and want more Go content? Check out my recent article on Learning Go with flash cards.