My Process for Creating a New Dockerfile

Today I wondered - what goes through my head, when I want to create a new Dockerfile? I don’t think I have written about this before.

I think it could be useful to get a glimpse of how I approach creating a new Dockerfile, what the high-level thinking looks like and what decisions I care about.

Let’s start with the most important part: the context around the Dockerfile.

What Is This For?

Before starting on the Dockerfile, it’s important for me to understand how the future Dockerimage will be used.

Am I setting the right goals? Is it an application I know how to operate already and have collected experience with? I take special care to make small steps, and set achievable goals for myself.

When I’m starting a new Dockerfile it’s either for a fresh project, or an application I’m not yet familiar with.

I’m using Docker as a lab to find out what an application does to the file system out of curiosity. You can read about it here.

If I catch myself thinking about going to production right away, I would readjust my goals and set myself up for a better experience at this point. Things are better if you give them time to grow at a convenient pace.

What Will Be Around the Future Containers?

If it’s a web application, we’ll need a database and maybe one or two other backing services.

I like to start with that environment, and create a simple docker-compose file where the future image will fit into. I think of this like a rough draft for a future painting - to get the proportions right and give myself an overview.

What Can I Build on Top Of?

I usually don’t start from scratch. There’s so much work out there of people who have figured out rough edges and took time to smooth them out.

I’ll look for a past project, a popular public open-source boilerplate template (in case of Django, that would be cookiecutter-django for me). I don’t need to agree or keep all of the parts.

Requirements and preferences vary after all. I will cut out parts I don’t need or disagree with to arrive at a small working first version with as little effort as possible.

Usually, I will cut the example down to the bare functional minimum as a starting point.

The cutting down with confidence is probably what takes experience and understanding of what’s going on. I think I would be pretty hesitant if I didn’t know exactly what I needed… The curse of knowledge makes this hard to reason about.

How Will the Image Be Used?

Now that we have a soon-to-be-working Dockerfile, it’s time to think about how we’ll want to use it.

Will we bind-mount code from the host into the future containers? Do we want to create a self-contained replication of the development environment? This will influence what we’ll need to COPY into the image.

Is there a folder where the code will live? Does everything we have so far fit the way we’ll want to achieve with the image (and container stack)?

The Nitty Gritty

Once all of this is decided upon, I’ll go through an internal checklist.

  • Is the base image right for me? For example: does it strike a good balance between size and convenience, is it well-used for the stack I’m dockerizing, is it familiar to me?
  • Does it do more than I need? I’ll try to simplify if possible.
  • Will it build reasonably fast? There are a few tweaks I try to add right away to reduce the image build time.
  • Does the image as it is pass basic sanity checks? Is the main command running as non-root user or at least drop privileges, does we use an init process? Am I leaking secrets by accident?

Once everything looks good and missing instructions have been added, the only thing left is to pass configuration values to the application.

Build the Image, Run the Stack

Now it’s just about a few iterations of building the image, trying to run it as part of the docker-compose stack and smoothing out issues as they appear.

Workflows are seldomly perfect or flawless on the first try. A few iterations are a natural thing to expect.

More Sophistication and Complexity?

Once everything works for the first time, it’s a great moment to appreciate the milestone and do a tiny celebration. That moment when everything works is one of the best parts of the craft for me.

The first version of the Dockerfile is done. It’s a starting point for future improvements.

Now we can iterate on the initial Dockerfile and figure out how to shape it to our needs. Increasing complexity gradually, and growing a more complex working system from a simpler working system.

There’s a lot which can be done usually. Do we want to move towards production deployments? Multi-stage builds with different parts of the stack are on the menu then. A proper tagging scheme for future images needs to be decided on. Ops concerns will start influencing the shape the Dockerfile will take.

But hey, one step at a time.

In Conclusion

We’re done creating a new Dockerfile for now, and can improve it with time, just as we are improving the dockerized project as well.

I hope this walkthrough of my process for creating a new Dockerfile has helped you! As you see, there’s a lot of focus on doing the right thing, and taking small steps. At this pace, the technical decisions are not overwhelming and you can focus on necessary details one after the other.

A good fundamental understanding of Docker lies beneath this whole process. If you are interested in expanding your understanding, I have a list of things I wish I knew about Docker when starting out. I think it might be useful to you as well.