Microcontainers – Tiny, Portable Docker Containers


microwhale-vs-bigwhale

Containers are awesome. Microcontainers are awesomer.

Docker enables you to package up your application along with all of the application’s dependencies into a nice self-contained image. You can then use use that image to run your application in containers. The problem is you usually package up a lot more than what you need so you end up with a huge image and therefore huge containers. Most people who start using Docker will use Docker’s official repositories for their language of choice, but unfortunately if you use them, you’ll end up with images the size of the empire state building when you could be building images the size of a bird house. You simply don’t need all of the cruft that comes along with those images. If you build a Node image for your application using the official Node image, it will be a minimum of 643 MB because that’s the size of the official Node image.

I created a simple Hello World Node app and built it on top of the official Node image and it weighs in at 644MB.

That’s huge! My app is less than 1 MB with dependencies and the Node.js runtime is ~20MB, what’s taking up the other ~620 MB?? We must be able to do better.

What is a Microcontainer?

A Microcontainer contains only the OS libraries and language dependencies required to run an application and the application itself. Nothing more.

Rather than starting with everything but the kitchen sink, start with the bare minimum and add dependencies on an as needed basis.

Taking the exact same Node app above, using a really small base image and installing just the essentials, namely Node.js and its dependencies, it comes out to 29MB. A full 22 times smaller!

 

pasted_image_at_2016_01_22_11_20_am

Regular Image vs MicroImage

 

Try running both of those yourself right now if you’d like, docker run –rm -p 8080:8080 treeder/tiny-node:fat, then docker run –rm -p 8080:8080 treeder/tiny-node:latest. Exact same app, vastly different sizes.

Why Are Microcontainers Great?

There are many benefits to using MicroContainers:

  • Size — MicroContainers are small. As shown above, without changing any code the image is 22 times smaller than a typical image.
  • Fast/Easy Distribution — Because the size is so much smaller, it’s much quicker to download the image from a Docker registry (eg: Docker Hub) and therefore it can be distributed to different machines much quicker.
  • Improved Security — Less code/less programs in the container means less attack surface. And, the base OS can be more secure (more below).

These benefits are similar to the benefits of Unikernels, with none of the drawbacks.

How to Build Microcontainers

The base image for all Docker images is the `scratch` image. It has essentially nothing in it. This may sound useless, but you can actually use it to create the smallest possible image for your application, if you can compile your application to a static binary with zero dependencies like you can with Go or C. For instance, my treeder/static-go image contains a Go web app and the entire image including my app is 5MB.

That’s about as small as you can get. The scratch image + your application binary.

Not everyone is using Go (unfortunately) so you’ll probably have more dependencies and you’ll want something with a bit more than the scratch image. Enter Alpine Linux. I won’t bore you with the details, but their tagline says it all: “Alpine Linux is a security-oriented, lightweight Linux distribution based on musl libc and busybox.” You can read more about what each of those things mean here, but what we care the most about for this article is the “lightweight” part. The base Alpine image is only 5MB:

So now we have a very nice OS as a base with a nice package system to add our dependencies. For our simple Node app, we only need Node itself so we can just add the Node package and nothing else. Our Dockerfile looks like this:

https://gist.github.com/treeder/c0e55b1627ea54818c8f

Simple and clean. We only have Node and what Node needs in the image now.

Now to add our code to the image, it’s just a few more lines in our Dockerfile:

https://gist.github.com/treeder/efc76a08c37f0eb71c8a

You can grab sample code and see the full build instructions here, but you get the idea. We now have a very small OS + only the dependencies we need + our application code. Nothing more.

The same rules apply to all languages.

Base Images for all Languages

As luck would have it, we’ve already built base images for all major languages and you can find them here:

https://github.com/iron-io/dockers

These have some optimizations in them to make them as small as possible and we update them regularly, which makes them a slightly better choice than doing it yourself. Using the Iron.io base images, the Dockerfile above for the Node app changes to this:

https://gist.github.com/treeder/17bd41328c54218a50c2

Also, for every language, we built two versions of the image. one for building and one for running. The images for building have all the build tools in them so are generally much bigger than the ones for running.

For instance, to build your Node dependencies, you’d use iron/node:dev like this:

https://gist.github.com/treeder/a204f08d11d906aff66a

Then use iron/node in your Dockerfile or to run it:

https://gist.github.com/treeder/0c627750f72e905f768e

Same goes for all the other languages, but you’d use their build/vendor/run commands.

If you’d like to use a different version of a language, you can change the tag. For instance, you could use iron/node:4.1 or iron/node:0.12. You can find all the version tags on Docker Hub for each language. The Node tags are here for instance: https://hub.docker.com/r/iron/node/tags/. You’ll find links to all the other Docker Hub tags from the iron-io/dockers repo.

How to Build and Package for All Languages

This probably isn’t luck anymore, but we also have examples of using the above base images for most major languages here:

https://github.com/iron-io/dockerworker

If you look at the README’s for each language in that repository, it will walk you through how to build your dependencies, test your code, build a small Docker image and test the image.

No Going Back

After reading this post, you should be able to create Docker images for your applications that contain nothing more than what is required to run your app. A container is essentially an instance of an image, so once you start firing up containers using your image, you’ve entered the world of Microcontainers. And there’s no going back.

Just used your “tiny image” technique on one of my golang repos. Its awesome! thanks for the great post. shrunk it down to 5mb amazing. — Harlow Ward @ Clearbit

 

10 Comments

naxhh

about 1 week ago

Not to be that guy that complains but since this is about small Images you should deleted the files apk update generates and concat all that in one line so you don't generate extra layers Example in the alpine readme https://hub.docker.com/_/alpine/

Reply

treeder

about 1 week ago

All our Iron images do delete the apk files, eg: https://github.com/iron-io/dockers/blob/master/ruby/Dockerfile#L10 . I'm curious to know what difference extra layers make in terms of size?

Reply

naxhh

about 1 week ago

Sorry, I didn't check the final images :) I just was quickly reading with the mobile. Checking imagelayers it seems that the rm from cache does not remove anything but the update generates 700k every time you execute it. So I will check that a bit more. I don't think less layers is about final size but more about download speed. The same applies to the good practice of executing only one apk add with packages sorted by name and the re-use of existing layers on your pc. Btw apk add --no-cache should do the trick

Reply

treeder

about 7 days ago

Ya, the layers don't show the size decrease for some reason, but the total image size is much smaller. In iron/node, we uninstall npm and it cuts the image size down by something like 20MB. Even though the layer for npm uninstall shows 0.

Reply

tom_m

about 1 week ago

This kinda stuff is really cool. It's going to get even smaller and more secure in the future too. Iniatives like this are foreshadowing things a bit: https://github.com/deferpanic/gorump

Reply

Harlow Ward

about 7 days ago

Great post @treeder:disqus . Can you expand on the advantage of building the Golang static binary inside the Docker container as opposed to building with `GOOS` and `GOARCH` flags?

Reply

treeder

about 6 days ago

I don't think there's an advantage to build it either way, you should end up with the same bin eight way. I just like building inside containers regardless so I don't have to setup anything on my machine to do it (GOPATH, etc) and if it's not static, you can build/test on whatever system you are going to be running on. For instance, a Go binary built on Ubuntu will not run on Alpine, so just build it inside an Alpine container and you're good to go. And you can test it on the destination operating system too.

Reply

Rémi Alvergnat

about 6 days ago

Great post, thanks ! Micro containers are often better, people should keep in mind that if you have 10 container sharing the same big 650Mb image, it won't eat 10x650Mb, but 1x650Mb. So if size matters for ease of distribution and public images, I'm not sure it's that critical and it can even be counter-productive in some case. If host runs 10 containers based on 10 distinct micro image, it might eat up more space that it would by using a single bigger and more generalist base image ...

Reply

Dave Newton

about 6 days ago

But the generalized base container would still be the same, and the specialized images would still have the same stuff. I think you'd need a pretty wide swath of micro images to add up to even a few macro images.

Reply

Leave a Comment

Please be polite. We appreciate that.
Your email address will not be published and required fields are marked