profile

Ivan on the Server Side

Building Container Images Like a Pro


Building container images can be both an easy and a hard problem. Building just an image is usually as straightforward as replicating your local app build steps in a Dockerfile’s RUN instructions. Building a small, maintainable, and secure image, on the other hand, may involve lots of considerations, elaborate techniques, and domain knowledge.

To help you become a true container image build master, I prepared a learning path that starts with creating and pushing your very first container image and goes through analyzing the contents of the image to picking an optimal base and learning how to spot and troubleshoot runtime problems caused by flawed image composition.

Warming up: Docker 101

To get started, let’s build and push a very simple container image: Challenge: Build and Publish a Container Image With Docker

Know what you’ve built

One of the critical but often overlooked skills when working with container images is the ability to introspect their contents and know exactly what’s inside.

Yes, images are often represented as a sequence of overlaid folders, but when extracted, the insides look like a regular folder, which often contains a typical Linux distro root filesystem (rootfs).

In the How To Extract Container Image Filesystem Using Docker tutorial, you’ll learn about different ways to extract a container image filesystem using standard Docker commands (spoiler: it’s not that simple if you want to inspect the truly unmodified original rootfs).

When you're done with the tutorial, don't forget to check your skills by solving this (slightly more complex) challenge: Extract the Filesystem of a Container Image.

Choosing an optimal base

Once you are familiar with the main docker build mechanics, it’s time to tackle probably the most important topic: how to pick an optimal base image for your application.

The A Deeper Look into Node.js Docker Images tutorial explores various Node.js base image options and serves as an example of how to do proper due diligence when choosing a base image.

While the tutorial is focused on Node.js images, it touches on an important image composition problem that reoccurs in many other container images, such as python, ruby, or even rust - and to a smaller extent, golang.

Multi-stage builds

Choosing the right base container image can be particularly tricky if you want to both build and run your application in Docker. The tools (e.g., compilers and linters) and packages (e.g., testing frameworks, code generators) needed to build a typical application differ significantly from what’s required to run it in production.

For a production image to be efficient and secure, it must contain only an absolutely minimal set of OS-level packages and runtime dependencies, whereas compiler toolchains tend to be huge and riddled with CVEs.

Luckily, with multi-stage builds, you don’t need to pick a single base image to satisfy both build and runtime requirements. Learn more about this vital technique in the How to Build Smaller Container Images: Docker Multi-Stage Builds tutorial (with practical examples for Node.js, Go, Rust, and Java).

Practice time: Containerizing Node.js applications

Container images are often poorly structured because Dockerfiles are treated as no man’s land:

  • Application developers may lack the motivation and skills to write optimal Dockerfiles.
  • DevOps engineers may lack deep knowledge of the application stack and current best practices.

This disconnect is understandable - it’s a lot of cross-functional knowledge to juggle!

To solve these Node.js challenges, you might need to consult framework documentation on producing standalone builds:

While Next.js and Nuxt support “standalone” builds, other frameworks can use different build practices. For instance, in Svelte, project dependencies must be copied from the build stage to the production stage, along with the app bundle itself.

While doing this, it’s crucial to avoid copying dev-time dependencies by mistake. This challenge highlights the importance of truly understanding stack-specific nuances in addition to Docker best build practices:

And don't forget to use multi-stage builds while building these Node.js images!

What does the smallest possible base image look like?

Building container images FROM scratch to keep them as small (and secure) as possible is a common approach - especially for compiled languages like Go or Rust. While it works in simple cases, more complex applications can run into issues due to missing runtime components.

In the Building Container Images FROM Scratch: 6 Pitfalls That Are Often Overlooked tutorial, you’ll learn about the pitfalls of building images FROM scratch and typical application expectations for execution environments.

Practice time: Dockerize a statically linked Go application

Apply the knowledge from the previous tutorial to containerize this simple Go app: Build a Production-Ready Go Container Image: A Statically Linked Application.

Troubleshoot a containerized Go application

Solve this challenge to understand how missing components in a FROM scratch image can cause runtime problems: When a FROM scratch Container Image Is Not Good Enough.

Distroless as a better runtime base

If FROM scratch failures are common, is there a better way to build minimal container images? Enter distroless images, often called "better scratch." They have (almost) no CVEs and include essential components like a proper rootfs layout, CA certificates, and time zone info.

Learn more here: What's Inside Distroless Container Images: Taking a Closer Look.

Practice time: Dockerize a dynamically linked Go application

Use your newfound knowledge to containerize this more complex Go app: Build a Production-Ready Go Container Image: A Dynamically Linked Application.

Troubleshoot another containerized Go application

Another service failing due to a bad containerization attempt - can you fix it? Apply your knowledge of distroless images to rebuild the container: Pick the Right Distroless Base Image For Your Application.

Ideal image rootfs isn't everything

A flawed Dockerfile can cause more than just missing files or packages. Solve this challenge to explore another common issue—broken signal propagation: Ensure a Graceful Termination for a Container With an Entrypoint Script.

Know your application stack

The same issue (broken graceful termination) can arise from different root causes. This challenge highlights how a lack of knowledge about your app stack or Linux basics (e.g., sub-shells and signal propagation) can lead to production issues: Ensure a Graceful Termination for a Containerized Node.js Application.

Container Image Security 101

Last but not least! Poor image composition (unnecessary dependencies) or lack of proper maintenance (missing security patches) can lead to extra work for your security team - or worse, production breaches.

Learn how to scan your images for vulnerabilities and patch affected packages in this challenge by a long-time Docker engineer, Felipe Cruz: Docker Scout: Remediating CVEs in a Container Image.

Need an expert opinion? Reach out!

Learning how to build optimal container images takes a lot of time and effort. While I encourage you to go through all the above materials to acquire this skill yourself, it's totally fine if you and your team need some expert help today, and I've got some great news.

Kyle Quest and I teamed up to:

  • Provide expert Dockerfile reviews (and rewrites).
  • Do a few YouTube streams of us going through some prominent Dockerfile examples - Check out the first one!
  • Conduct workshops on image building (for teams).

If it sounds like something your team would benefit from, fill in the form at gooddockerfiles.com. Limited-time offer!

Happy building!

Cheers

Ivan

Ivan on the Server Side

A satellite project of labs.iximiuz.com - an indie learning platform to master Linux, Containers, and Kubernetes the hands-on way 🚀

Share this page