Avoid Bastardizing Your Docker Images

Bastardize–cool word right? In this context it means something along the lines of debasing an original concept by tacking on elements that are incongruous. In the realm of gaming, for example, Pokemon Go is a bastardization of the original Pokemon games. Some may argue that the contemporary Pokemon games are a bastardization of the original Pokemon games. In object-oriented programming, someone telling you that your base class is bastardized wants you to know that 1. Your approach to abstraction is fundamentally flawed 2. You bolted on functionality instead of refactoring or 3. That he knows cool words like bastardize. Potentially all three.

When it comes to creating a base docker image, especially your first base docker image, it’s easy to get carried away. I’ve developed images–Dockerfile templates, really–that have powered an assortment of different microservices. I’ve also had to tear apart images that seemed like a really good idea but turned out to be bastards in the first degree. Embarrassingly, I was several months into learning docker (the ineffective and careless way) before I started to wonder if there might be something wrong with the aboriginal docker image that was ubiquitous at my workplace.

Before a team starts running Docker in production, it’s important to have a pure (though not obsessively minimalistic) function-specific set of images. Below, I will talk about the pitfalls of moving too quickly from “wow, docker is useful!” to “let’s get this running in production.”

It’s not perfect, but it’s fit for purpose and easy to understand. So what would it mean to bastardize this image? Is it as yum installed Vim? Well, no, though it’s considered bad practice to do this (the documentation explicitly says not to). The image itself has a clear, singular purpose, even if you allow yourself to use Vi Improved inside the container.

The real example from work is quite extreme, to the point of seeming contrived, but I’m telling the truth when I say that this is what I was working with. In addition to the basic flask app, there was a Swagger yaml file mapped to the routes using connexion. That is normal. Then, there was a SwaggerUI. Right off the bat, that’s a violation. The UI can be run as its own container. More easily, in fact. Then there was an NGAdmin dashboard. Again, it should have been its own container. On top of that, there was Redis and a profiler, both of which I never used. All of this was wrapped up into one single, hideous entrypoint command. On top of that, in place of a docker-compose file, there was a 50+ line makefile. Don’t ask.

The total build time could have been in upwards of twenty minutes on a slow box (read: mine), and the image size was at least 1.2GB.

I want to reiterate that there is no reason to do this. All of the supporting apps can be easily configured as standalone docker containers that interact with the backend app via a docker compose file. In addition, there is no opportunity to scale the app. If you wanted to scale one of these monolithic containers, you’d suddenly have three swagger uis, three ng admin uis, and a whole lot of confusion.

The purpose of docker, and, really, microservices in general, is that you don’t have to worry about how each service is implemented, just how it exposes its functionality. For my monster docker image, though, I was rebuilding and managing dependencies for extra features that already are provided by standalone images in docker hub. In short, design your Docker images like you would an object oriented program. If you have a compound run command or containers doing more than one thing, chances are that you are going down a dark path.