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!
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:
FROM alpine RUN apk update && apk upgrade RUN apk add nodejs
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:
FROM alpine RUN apk update && apk upgrade RUN apk add nodejs WORKDIR /app ADD . /app ENTRYPOINT [ "node", "server.js" ]
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:
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:
FROM iron/node WORKDIR /app ADD . /app ENTRYPOINT [ "node", "server.js" ]
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:
docker run --rm -v "$PWD":/app -w /app iron/node:dev npm install
Then use iron/node in your Dockerfile or to run it:
docker run --rm -it -p 8080:8080 -v "$PWD":/app -w /app iron/node node server.js
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:
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