Go tip of the week: Containers from scratch?

The typical Go binary is static and self-contained. It has absolutely no dependencies on system libraries or pre-installed runtimes.

A Go binary is therefore an ideal basis for creating super-small containers. There is no need to put a complete Linux distribution underneath a Go app. If you take it to the extreme, you can create a Go container FROM scratch.

But is this a good idea?

Black Friday Special:
My course Master Go is $60 off until Dec 2nd.
Hop over to AppliedGo.com for more info.

On a second glance, a container built from the scratch image (literally a zero-size, zero-content “image”) has a number of deficiencies.

  • No user management. A Go binary in a scratch container lives in a user-free world. Calling os/user.Current() panics in a scratch container.
  • Missing system folders. An app might assume to find usual paths in their usual places; for example, /tmp or $HOME. A scratch image does not set up any paths or directories.
  • No auxiliary apps. A Go app may want to run auxiliary tools via os.Exec() that are missing from a scratch image.
  • No CA certificates. If you are serious about running a web server, you need TLS certificates. Certificates are signed by a chain of issuer certificates, up to root certificates from a certificate authority (CA). None of these exist in a scratch image.
  • Your pure Go binary may still need C libraries. The two standard library packages os/user and net packages will call into libc on Linux systems if CGO is enabled at compile time. Setting CGO_ENABLED to 0 makes the two packages fall back to pure Go implementations of the user and DNS lookup functions, but the pure Go variants are less performant, so you might want to prefer the libc option. (Related: Building static binaries with Go on Linux - Eli Bendersky's website.)

This list is not exhaustive; there may be more obstacles depending on the app's needs.

So while Go binaries work well on scratch images in theory, the lack of basic features can be prohibitive.

Options to resolve this:

1. Work around the limits

Most of the limitations can be worked around; for example, by adding CA certificates or required directories and files to the image.

2. Use a “distroless” image

Distroless is a set of container images that are trimmed down to the barest minimum possible. The smallest version, the static image, is about 2MB in size and is made for statically linked binaries like Go binaries. The base image adds commonly used shared libaries like libc or openssl.

3. Use small images like Alpine

Alpine is a Linux distribution with a small footprint (about 8MB as a container image) that ships most of the things you would expect from a Linux distro: file system, user management, system libraries, packet management, and more.

Bottom line: Unless the space requirements are super strict, even a Go binary benefits from container images that are more than just the scratch image.