When people talk about Go executables being a single binary file without a bunch of dependencies, that's not necessarily true. The simplest of programs (hello world) will be linked static by default, but you don't have to get very complex to break away from that. Your Hello World app looks something like this:
package main
import (
"fmt"
)
func main() {
fmt.Println("Hello, World!")
}
And when you compile it with go build
, running ldd on it will produce not a dynamic executable.
But that's not a very useful program, especially for a language like Go which has a sizable focus on networking. Let's write a slightly more complex example, a simple utility that uses icanhazip.com to determine your public IP address. It's still quite simple:
package main
import (
"fmt"
"io/ioutil"
"net/http"
)
func main() {
resp, err := http.Get("http://icanhazip.com")
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
fmt.Print(string(body))
}
Compiling this with go build
and running ldd against it shows a very different story:
$ ldd whatsmyip
linux-vdso.so.1 => (0x00007ffe4e678000) libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f957504e000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f9574c84000)
/lib64/ld-linux-x86-64.so.2 (0x000055b02de9d000)
Suddenly, you're dependent on 4 external libraries! Now, most of this time this isn't a big deal. You're probably writing your code on a complete OS that has them, and chances are you're going to run them on something similar... But this is 2015, and the container revolution is upon us! So Docker has become one of those must-have technologies for giving you separation of concerns while still letting you consolidate hosts, and with a little effort you can make extremely small and efficient containers with almost no overhead. Docker will happily let you install a complete general purpose Linux userland (Try it! Install docker and run: docker run -it ubuntu:latest bash
) but that's a lot of overhead for a simple process. On my system, that command creates a container with almost 190 megabytes of data, just to get you that bash shell. What if your single process container doesn't need all that?
Docker has helpfully created their 'scratch' image, basically an empty template to build on. If you have a static binary, that can literally be the only data in your container. But first, you need a static binary! For our Hello, World application, that's straightforward. A simple Dockerfile where your hello executable lies:
FROM scratch
ADD hello /
CMD [ "/hello" ]
Then, docker build -t hello .
will create your container. docker images will show the size of any images you have created, this one is a measly 2.3 megabytes. To run it, just use "docker run hello
".
Trying to do this with your whatsmyip executable will fail, as it needs those 4 libraries to be included. Now, you could add those libraries in your Dockerfile (with the ADD command), but wasn't part of the magic of Go that you didn't have to worry about those libraries? Fortunately, while it's not obvious, it is usually easy to get that static binary. For our sample program, just telling go to use it's own networking stack instead of leaning on the operating system is enough. Just add -a -tags netgo
to your go build command! If you (or one of your imports) is using cgo, you may need to add some additional fields to your build command (many things will work by adding -ldflags '-extldflags "-static -lstdc++ -lm"'
)
And now you should have that wonderful statically linked binary, ready to roll put in a minimal Docker container. Just create a Dockerfile:
FROM scratch
ADD whatsmyip /
CMD [ "/whatsmyip" ]
and build with build -t whatsmyip .
and run with docker run whatsmyip
This new docker image is a measly 6.5 megabytes(use docker images
to show the sizes of your images), with no overhead from shared libraries, shells, basic shell utilities, package managers, etc. Just your code, and nothing more.
Enjoy!
Page 2 of 2