2HAASStart a project
All posts

Tech Tips

Speed up Docker builds with layer caching

Slow Docker builds delay every pull request. Order your Dockerfile for the layer cache, shrink the build context and use BuildKit cache mounts to cut build time.

The 2HAAS team2 min read

A Docker build that takes eight minutes is eight minutes added to every pull request and every deploy. Most of that time is usually spent reinstalling dependencies that did not change. Docker can skip that work if the Dockerfile lets it.

How the layer cache works

Docker builds an image one instruction at a time and caches the result of each one as a layer. On the next build it reuses a cached layer as long as the instruction and the files it depends on are unchanged. The moment one layer changes, every layer after it is rebuilt.

So the rule is simple: put what changes rarely at the top and what changes often at the bottom.

Copy the dependency manifest first

This Dockerfile reinstalls every package whenever any source file changes:

COPY . .
RUN npm ci

This one reinstalls only when the dependencies change:

COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

It is the single most valuable change for build speed, and it works the same way for requirements.txt, go.mod or composer.json.

Keep the build context small

Before the build starts, Docker sends the whole folder to the builder. A .dockerignore file keeps large and irrelevant paths out.

node_modules
.git
dist
coverage

A smaller context uploads faster and stops unrelated file changes from invalidating the cache.

Use BuildKit cache mounts

BuildKit, the default builder in current Docker versions, can keep a package manager's cache between builds, even when the layer itself has to be rebuilt.

RUN --mount=type=cache,target=/root/.npm npm ci

When a dependency changes, npm downloads only the new package instead of all of them.

Share the cache in CI

A fresh CI runner starts with an empty cache. Export it to your registry and import it on the next run.

docker buildx build \
  --cache-from type=registry,ref=registry.example.com/app:cache \
  --cache-to type=registry,ref=registry.example.com/app:cache,mode=max \
  -t registry.example.com/app:latest --push .

What to expect

With the manifest copied first and a shared cache, a build where only application code changed often drops from minutes to well under one. Faster builds mean faster feedback, and less paid CI time.

If the images themselves are heavy, the next step is to reduce Docker image size with multi-stage builds.

Keep reading

  1. Guides2 min read

    Reduce Docker image size with multi-stage builds

    Large Docker images slow down every build, push and deploy. Multi-stage builds, slim base images and a .dockerignore file can cut an image to a fraction of its size.

  2. Tech Tips2 min read

    Structured logging: make your logs searchable

    Plain text logs are easy to write and painful to search. Structured JSON logs with levels and consistent fields turn finding one failed order into a single query.

  3. Tech Tips2 min read

    Async error handling in JavaScript: try/catch done right

    Unhandled promise rejections crash Node.js services and leave browser apps quietly broken. Catch async errors with try/catch, .catch and a global safety net.

Tell us what’s in the way.

A few lines is enough. You get a candid yes, a candid no, or a sharper question.

  • Working software in week four
  • A reply in one business day
  • Handover is the deliverable

Prefer email? [email protected]

The problem, who it is for, and what is in the way right now.

One business day to a real reply. No sales sequence, no call required.