DEV Community

Atharva Unde
Atharva Unde

Posted on • Originally published at blog.atharvaunde.com

Docker: Layers, Caching, Multi-Stage Explained

Docker's efficiency is one of its biggest draws. But what makes Docker builds so fast? The secret lies in its layer-based architecture and clever caching mechanism. Let's dive in and see how it all works.

Dockerfiles: A Layered Cake

Every line in your Dockerfile is an instruction, and Docker treats each of these instructions as a distinct layer. But what is a layer, exactly?

Think of it like this: a layer is an intermediate snapshot of your container image during the build process. Each instruction in the Dockerfile creates a new layer, building upon the previous one.

For instance, consider this common Node.js Dockerfile:

FROM node:18
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
CMD ["node", "server.js"]
Enter fullscreen mode Exit fullscreen mode

This simple Dockerfile translates into five distinct layers:

  • Base Image Layer: FROM node:18 (The foundation upon which everything else is built)
  • Working Directory Layer: WORKDIR /app (Sets the working directory inside the container)
  • Dependency Definition Layer: COPY package.json . (Copies the package.json file)
  • Dependency Installation Layer: RUN npm install (Installs the project dependencies)
  • Application Code Layer: COPY . . (Copies the entire application code)

Each of these instructions results in a distinct layer that's stored in the image.

Docker's Caching Superpower

Here's where the magic happens: Docker caches each of these layers during the build process. This means that if a layer hasn't changed, Docker can reuse the cached version instead of rebuilding it from scratch. This dramatically speeds up subsequent builds.

  • Cache Hit: If an instruction and its inputs haven't changed, Docker pulls the existing layer from the cache.
  • Cache Miss: If an instruction or its inputs have changed, Docker invalidates the cache for that layer and all subsequent layers. This means it needs to rebuild not only the changed layer but also every layer that comes after it.

Cache Invalidation: When Things Go Wrong

The cache invalidation behavior is crucial to understand. Imagine you have a Dockerfile with eight instructions. If instruction #2 changes, Docker invalidates the cache for instruction #2 and all instructions that follow (3 through 8). They will all need to be rebuilt. This can lead to longer build times if not managed correctly.

A Real-World Example (Multi-Stage Build and Labels)

Let's examine a more complex scenario involving a multi-stage Dockerfile, which is a best practice for creating smaller and more secure images:

FROM node:20-alpine AS build-env
WORKDIR /app
COPY package.json yarn.lock ./
ENV NODE_ENV=production
RUN yarn install --frozen-lockfile --production
COPY index.js ./

FROM gcr.io/distroless/nodejs20-debian12
WORKDIR /app
LABEL org.opencontainers.image.authors="authoremail@example.com"
LABEL "com.example.vendor"="Example LLC"
LABEL version="1.0.0"
LABEL description="This image is used to run hello world backend written in Express Framework"
COPY --from=build-env /app /app
CMD ["index.js"]
Enter fullscreen mode Exit fullscreen mode

In this Dockerfile, we have two stages:

  1. build-env Stage: This stage uses a Node.js Alpine image to install dependencies and prepare the application for production.
  2. Final Stage: This stage uses a distroless image (gcr.io/distroless/nodejs20-debian12), which contains only the necessary runtime dependencies.

Here's how caching works in this multi-stage context:

  • Independent Caches: Each stage has its own separate cache. Changes in one stage don't automatically invalidate the cache of other stages, unless they affect the COPY --from instruction (which we'll discuss below).
  • build-env Stage Changes: If you modify package.json or yarn.lock in the build-env stage, the RUN yarn install instruction will be invalidated, and all subsequent instructions in that stage will need to be rebuilt.
  • COPY --from Interaction: The COPY --from=build-env /app /app instruction is crucial. If the contents of /app in the build-env stage change (due to a rebuild triggered by a change in package.json, for example), the COPY instruction will also produce a different result in the final stage, invalidating the final stage's cache from that point onward.
  • Label Invalidation: The LABEL instructions, while important for adding metadata, do not directly influence the caching mechanism. Changing label values will always cause the layer containing the LABEL instruction to be rebuilt, but it doesn't impact any previous layers.
  • Code Changes: If you simply modify code in the index.js file, only the COPY index.js ./ instruction within build-env, and the subsequent COPY --from instruction in the final stage will be affected. The dependency installation stage (RUN yarn install) can still be pulled from the cache, speeding up the build significantly.

Docker Caching and Multi-Stage Builds: Scenario Table

This table outlines how different changes to your Dockerfile or application code impact the caching mechanism in a multi-stage build.

Scenario Changed File/Instruction Impact on build-env Stage Cache Impact on Final Stage Cache Rebuilt Layers
Dependency Change: package.json or yarn.lock RUN yarn install and subsequent instructions are invalidated. COPY --from=build-env /app /app and subsequent instructions invalidated. All layers from RUN yarn install in build-env, and from COPY --from in the final stage
Code Change Only: index.js Only COPY index.js ./ is invalidated. COPY --from=build-env /app /app and subsequent instructions invalidated. COPY index.js ./ in build-env, and from COPY --from in the final stage
Dockerfile Change (build-env, Before COPY package.json)`: (e.g., adding a new ENV variable before COPY) All instructions after and including the changed instruction are invalidated. If the content of /app does not change, the final stage stays cached All layers from that step to end of build-env
Dockerfile Change (build-env, After COPY package.json) (e.g., adding an RUN after copy) All instructions after and including the changed instruction are invalidated. COPY --from=build-env /app /app and subsequent instructions invalidated. All layers from changed instruction till end of build-env and onwards.
Label Value Change: (Change in LABEL instruction in the final stage) No impact. Only the layer with the modified LABEL is invalidated. Layer containing the LABEL instruction in the final stage
No Changes N/A All layers are pulled from cache. All layers are pulled from cache. None

Explanation:

  • Scenario: Describes the type of change made.
  • Changed File/Instruction: Specifies the file or instruction that was modified.
  • Impact on build-env Stage Cache: Explains which layers in the build-env stage are invalidated.
  • Impact on Final Stage Cache: Explains which layers in the final stage are invalidated.
  • Rebuilt Layers: Lists the layers that will be rebuilt during the Docker build process.

The Takeaway: Order and Multi-Stage Considerations

With multi-stage builds, you need to consider caching within each stage, as well as how changes in one stage affect subsequent stages through COPY --from instructions. Strategic placement of instructions and careful management of dependencies are key to maximizing build performance.

In the next section, we will explore best practices to optimize caching and reduce unnecessary rebuilds. Stay tuned!

Heroku

This site is built on Heroku

Join the ranks of developers at Salesforce, Airbase, DEV, and more who deploy their mission critical applications on Heroku. Sign up today and launch your first app!

Get Started

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay