📜 ⬆️ ⬇️

Creating a minimal Docker container for Go applications

Hi, Habr! I bring to your attention the translation of the article by the founder of the Meetspaceapp service Nick Gauthier "Building Minimal Docker Containers for Go Applications" .

Reading time: 6 minutes

There are many, both official and community-supported containers for various programming languages ​​(including Go). But these containers can be quite large. Let's first compare the standard methods for creating containers for Go applications, and then I will show you how to create extremely small static containerized Go applications.
')

Part 1: Our "application"


For testing, we need some small application. Let's fetch google.com and output HTML size.

package main import ( "fmt" "io/ioutil" "net/http" "os" ) func main() { resp, err := http.Get("https://google.com") check(err) body, err := ioutil.ReadAll(resp.Body) check(err) fmt.Println(len(body)) } func check(err error) { if err != nil { fmt.Println(err) os.Exit(1) } } 

If we start, we get only a number. I got about 17K. I purposefully decided to use SSL, but I will explain the reason later.

Part 2: Doc.


Using the official Go image we will write “onbuild” Dockerfile:

 FROM golang:onbuild 

The “Onbuild” image assumes that your project has a standard structure and will create a standard Go application. If you need more flexibility, you can use the standard Go image and compile it yourself:

 FROM golang:latest RUN mkdir /app ADD . /app/ WORKDIR /app RUN go build -o main . CMD ["/app/main"] 

It would be nice to create a Makefile or something else similar to what you are using for the build of applications. We could load some resources from a CDN or import them from another project, or maybe we want to run tests in a container ...
As you can see, Go is a fairly simple dockerization, especially considering that we do not use the services and ports to which we need to connect. But there is one serious lack of official images - they are really big. Let's get a look:

 REPOSITORY SIZE TAG IMAGE ID CREATED VIRTUAL SIZE example-onbuild latest 9dfb1bbac2b8 19 minutes ago 520.7MB example-golang latest 02e19291523e 19 minutes ago 520.7MB golang onbuild 3be7ee2ec1ae 9 days ago 514.9MB golang 1.4.2 121a93c90463 9 days ago 514.9MB golang latest 121a93c90463 9 days ago 514.9MB 

The base image takes 514.9MB, and our application adds another 5.8MB. How is it that our compiled application requires 515MB dependencies?
The point is that our application was compiled inside the container. This means that the container needs to install Go. Consequently, it needs Go dependencies, as well as a package manager and a really whole OS. In fact, if you look at the Dockerfile for golang: 1.4, it installs with Debian Jessie, installs the GCC compiler and build tools, downloads Go, and installs it. So we get a whole Debian server and a Go toolkit to run our tiny application. What can you do about it?

Part 3: Compile!


Improve the situation can be, a little departing from the usual approach to all. To do this, we are going to compile Go in our working directory, and then add the binary file to the container. This means that a simple docker build will not work. We need a multistep container assembly:

 go build -o main . docker build -t example-scratch -f Dockerfile.scratch . 

And a simple Dockerfile.scratch:

 FROM scratch ADD main / CMD ["/main"] 

What is scratch? Scratch is a special blank image in docker. Its size is 0B:

 REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE example-scratch latest ca1ad50c9256 About a minute ago 5.60MB scratch latest 511136ea3c5a 22 months ago 0B 

As a result, our container takes only 5.6 MB. Fine! But there is one problem:

 $ docker run -it example-scratch no such file or directory 

What does it mean? It took me some time to realize that our binary Go file is looking for libraries in the operating system on which it is running. We compiled our application, but it is still dynamically linked to the libraries that need to be run (i.e. with all the C libraries). Unfortunately, scratch is empty, so there are no libraries or download paths. We need to change the build script to statically compile our application with all the built-in libraries:

 CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main . 

We disable cgo, which gives us a static binary. We also specify Linux as the OS (in case someone builds it on Mac or Windows). The -a flag means rebuilding all the packages we use, which rebuilds all imports with cgo disabled. Now we have a static binary. Let's run:

 $ docker run -it example-scratch Get https://google.com: x509: failed to load system roots and no roots provided 

What's that? That's why I decided to use SSL in our example. This is really a common "cant" for such scenarios: to perform SSL requests, we need SSL root certificates. So how do we add them to our container?
Depending on the operating system, certificates may lie in different places. For many Linux distributions, this is /etc/ssl/certs/ca-certificates.crt . So, first, we will copy the ca-certificates.crt from our computer (or a Linux virtual machine, or the online certificate provider) into our repository. Then we add the ADD to our Dockerfile to move this file to where Go expects it:

 FROM scratch ADD ca-certificates.crt /etc/ssl/certs/ ADD main / CMD ["/main"] 

Now just re-create our image and launch it. Works! Let's see the size of our application now:

 REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE example-scratch latest ca1ad50c9256 About a minute ago 6.12MB example-onbuild latest 9dfb1bbac2b8 19 minutes ago 520.7MB example-golang latest 02e19291523e 19 minutes ago 520.7MB golang onbuild 3be7ee2ec1ae 9 days ago 514.9MB golang 1.4.2 121a93c90463 9 days ago 514.9MB golang latest 121a93c90463 9 days ago 514.9MB scratch latest 511136ea3c5a 22 months ago 0B 

We added a little more than half a megabyte (and most of which is from a static file, and not from root certificates). We got a really small container - it will be very convenient to move it between registries.

Conclusion


Our goal was to reduce the size of the container for the Go application. The Go feature is that it can create a statically linked binary file containing the entire application. Other languages ​​can also do this, but not all. The use of such a technique of reducing the size of a container in other languages ​​will depend on their minimum requirements. For example, a Java or JVM application can be compiled outside the container and then embedded in a container that contains only the JVM (and its dependencies). But even so it will be less than a container with a JDK.

Source: https://habr.com/ru/post/460535/


All Articles