Module F-2·35 min read

Writing optimal Dockerfiles, layer caching, BuildKit mounts, multi-stage builds, and multi-architecture image registries.

JJS
Written by Jatin Jain Saraf · Senior Software Engineer

Introduction

A container is only as good as the image it runs from. In the JavaScript ecosystem, it is painfully common to see Next.js or Express Docker images that exceed 1GB in size, take 10 minutes to build, and constantly invalidate their caches.

In this module, we will explore how to architect production-grade Docker images. We will move beyond basic Dockerfile syntax and dive into Layer Caching, BuildKit optimizations, Multi-Stage Builds, and Multi-Architecture support.


The Layer Caching Mechanism

To write an optimized Dockerfile, you must first understand how Docker caches layers.

Every instruction in a Dockerfile (FROM, RUN, COPY, etc.) creates a new layer. Docker builds an image layer by layer, top to bottom. If an instruction and its inputs haven't changed since the last build, Docker skips executing it and reuses the cached layer.

However, there is a golden rule of caching: If a layer's cache is invalidated, all subsequent layers below it are also invalidated.

The Node.js Caching Anti-Pattern

Here is the most common mistake in Node.js Dockerfiles:

dockerfile

Because COPY . . copies your entire codebase, editing a single markdown file or CSS file will invalidate the cache for that layer. Because that layer is invalidated, the next layer (RUN npm install) is also invalidated.

You end up waiting 3 minutes for npm install on every single build, even if your package.json hasn't changed.

The Correct Caching Strategy

You must separate your dependency installation from your source code:

dockerfile

By copying package.json before the rest of the code, the RUN npm ci layer is cached. It will only re-execute if you actually install a new package.


Docker BuildKit: Next-Generation Building

Modern Docker uses BuildKit, a highly optimized backend builder. BuildKit enables features like parallel stage execution, secret management, and advanced cache mounts.

To explicitly use BuildKit features, you must add a special syntax directive at the very top of your Dockerfile:

dockerfile

Cache Mounts

Even with the correct package.json strategy above, what happens when you do add a new dependency? npm ci has to download every single package from the internet again, because the Docker build environment doesn't have access to your host machine's ~/.npm cache.

BuildKit solves this with Cache Mounts.

dockerfile

The --mount=type=cache flag persists the /root/.npm directory between Docker builds. If you add a single new dependency, npm will pull it, but it will use the cache for the other 500 packages. Build times drop from minutes to seconds.

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.