In this article, I want to outline my go about building a Docker base image from scratch. Admittedly in most cases, this won't be the most practical approach, but it can have its advantages.
Let's say you would want to dockerize an application that uses a newly released runtime, e. g. Deno. The problem is that the only images released yet are community-driven, which can come with downsides like irregular updates or the lack of applied best practices.
In a case like this, it can make sense to build your own image from scratch.
The “intuitive” approach
I planned to base everything on Alpine Linux, which is a super lightweight distribution and therefore often used when working with Docker. Then I just needed to fetch the Deno binary, place it into the right directory and maybe handle some additional user permissions and environment variables.
Deno is built on Glibc, which in Apline has been “replaced” with musl libc, meaning that we cannot execute our binary. After a bit of research, the only viable solution seemed to be recompiling the binary myself. I personally have little to no experience working with binaries, so this seemed almost impossible for me to do.
Another approach would be to use a version of the Alpine image containing the Glibc library. Although this would work, it is considered bad practice.
Distroless. Google themselves describe it as language-focused Docker images minus the operating system. The way it works with Docker is through a multi-stage build using e. g. Alpine only to obtain the binary. In the case of Deno, we use the Distroless image for Rust as our second stage.
Note that a non-root user is provided out of the box.
For a more detailed look at Distroless Docker, I strongly recommend this talk by Matthew Moore.
Now you can dockerize your application with the added benefit of having full control over almost every component of your image. As already mentioned, I would strongly suggest using the inbuilt non-root user, which should come with all Distroless images. Deno specifically, I would recommend caching both the dependencies as well as the primary entry point. This way, your application doesn't need to be recompiled on each startup.
I encourage everyone, especially people who are new to Docker, to build their own base image for a runtime of their choice. I think it provides a unique look and may remove some of the confusion you encounter at the beginning.
Thanks for taking the time