Docker Layer Caching Explained: Tips to Improve Build Times

Dhananjay PatelDhananjay Patel
4 min read

Introduction

In our previous discussion on Docker image optimization, we focused on reducing image size to achieve faster deployments and lower storage costs. Now, let’s address another vital aspect of Docker workflows: build speed.

The time it takes to build a Docker image can significantly impact your development and deployment cycles. Fortunately, Docker offers a powerful feature called layer caching that can drastically reduce build times by reusing unchanged layers from previous builds.

In this blog, we’ll dive into how Docker layer caching works, practical tips to use it effectively, and common pitfalls to avoid.


What Is Docker Layer Caching?

Docker images are constructed layer by layer, with each instruction in the Dockerfile creating a new layer.

For example:

FROM node:16  
WORKDIR /app  
COPY package.json .  
RUN npm install  
COPY . .  
CMD ["npm", "start"]

In this Dockerfile, each instruction (FROM, WORKDIR, COPY, etc.) generates a new layer in the image. Docker saves these layers in the cache. If a subsequent build encounters an instruction that hasn’t changed, Docker reuses the cached layer instead of recreating it, speeding up the build process.


Why Is Layer Caching Important?

  1. Faster Builds: Reusing cached layers reduces the time spent on unchanged instructions.

  2. Improved Development Workflow: Iterative changes become quicker to test and deploy.

  3. Cost Efficiency: Shorter build times reduce compute resource usage in CI/CD pipelines.


How Docker Layer Caching Works

Docker processes the Dockerfile sequentially:

  1. It examines the first instruction.

  2. If the instruction hasn’t changed since the last build, Docker uses the cached layer.

  3. Once a layer’s cache is invalidated, all subsequent layers are rebuilt.

For instance:

  • If COPY package.json changes, the cache for the RUN npm install step will also be invalidated. (same example as above)

  • Instructions after the invalidated layer will not benefit from caching.


Best Practices for Leveraging Docker Layer Caching

  1. Organize Your Instructions Thoughtfully

    To maximize caching, place instructions that rarely change at the top of your Dockerfile.

    Example:

     # Better
     COPY package.json package-lock.json .  
     RUN npm install  
     COPY . .
    

    In this example, updates to your application code (copied in the last step) won’t invalidate the cached npm install layer.

  2. Leverage Multi-Stage Builds

    Multi-stage builds allow you to separate the build and runtime environments, reducing unnecessary layers in the final image.

    Example for Node.js App:

     # Build Stage
     FROM node:16 AS builder  
     WORKDIR /app  
     COPY package.json package-lock.json ./  
     RUN npm install  
     COPY . .  
     RUN npm run build  
    
     # Runtime Stage
     FROM nginx:alpine  
     COPY --from=builder /app/build /usr/share/nginx/html  
     CMD ["nginx", "-g", "daemon off;"]
    

    This approach ensures that only the production-ready artifacts are included in the final image, significantly reducing size and build time.

  3. Use .dockerignore to Avoid Irrelevant Files

    Include a .dockerignore file to exclude unnecessary files like .git directories, logs, or node_modules that could invalidate caching.

    Example .dockerignore:

     node_modules  
     *.log  
     .git
    
  4. Avoid Frequent Changes to Dependency Files

    Modifications to files like package.json or requirements.txt can invalidate cache for subsequent layers. If possible, group and minimize such changes.

  5. Combine Commands to Reduce Layers

    Each instruction creates a new layer. Combining commands into a single RUN statement minimizes layer count and keeps images compact.

     RUN apt-get update && apt-get install -y curl vim \  
         && apt-get clean && rm -rf /var/lib/apt/lists/*
    

    or there is one more option, Rather than endless && \ statements this would be more readable, especially for more complex runs.

     RUN <<EOF
     apt-get update
     apt-get install -y curl vim 
     apt-get clean
     rm -rf /var/lib/apt/lists/*  
     EOF
    
  6. Bump Cache for Layer-Sensitive Changes

    When changes in dependencies (e.g., bumping package.json version) invalidate a cache, consider temporary techniques like pre-defining dependency versions to isolate changes.


Common Pitfalls to Avoid

  1. Changing Order of Instructions

    Rearranging Dockerfile instructions can invalidate the cache for no reason. Be consistent in the order.

  2. Neglecting Cleanup

    Temporary files in one layer persist unless explicitly removed in the same instruction.

    Fix:

     RUN apt-get update && apt-get install -y curl \  
         && rm -rf /var/lib/apt/lists/*
    
     RUN <<EOF
     apt-get update
     apt-get install -y curl vim 
     apt-get clean
     rm -rf /var/lib/apt/lists/*  
     EOF
    
  3. Forgetting the Build Context

    Large files in the build context can slow down COPY or ADD instructions and invalidate the cache.


Tools to Enhance Build Speed

  1. BuildKit

    Docker BuildKit offers advanced caching mechanisms and parallelism for faster builds.

    In the another blog I will deep dive into Buildkit.

    as of now just Enable BuildKit:

     DOCKER_BUILDKIT=1 docker build .
    
  2. Layer Caching in CI/CD

    Many CI/CD platforms, like GitHub Actions and GitLab CI, support Docker layer caching to avoid rebuilding unchanged layers in every pipeline run.


Conclusion

Docker layer caching is a game-changer for accelerating builds and optimizing your workflows. By structuring your Dockerfile smartly, leveraging tools like BuildKit, and avoiding common pitfalls, you can drastically reduce build times and improve developer productivity.

Start experimenting with these techniques, and let me know how much faster your builds become! 🚀

0
Subscribe to my newsletter

Read articles from Dhananjay Patel directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Dhananjay Patel
Dhananjay Patel