Docker
Dockerfile
RUN command
DevOps
Software Development

Multiple RUN vs. single chained RUN in Dockerfile, which is better?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

There is no universal winner between many RUN instructions and one long chained RUN in a Dockerfile. The right choice depends on what you are optimizing for: cache reuse, image cleanliness, readability, or package-manager behavior. The useful rule is to group commands that belong to one atomic step and separate commands that change independently.

Each RUN Creates a Layer

A RUN instruction creates a new filesystem layer. That matters because Docker caching works instruction by instruction.

Separate RUN example:

dockerfile
1FROM ubuntu:24.04
2
3RUN apt-get update
4RUN apt-get install -y curl
5RUN apt-get install -y ca-certificates

Chained RUN example:

dockerfile
1FROM ubuntu:24.04
2
3RUN apt-get update && \
4    apt-get install -y curl ca-certificates && \
5    rm -rf /var/lib/apt/lists/*

Both are valid. The difference is how caching and cleanup behave.

Chain Package Install and Cleanup Together

Package-manager workflows are the clearest case where a chained RUN is usually better.

dockerfile
RUN apt-get update && \
    apt-get install -y --no-install-recommends git make && \
    rm -rf /var/lib/apt/lists/*

If apt-get update happens in one layer and cleanup happens in another, the cleanup does not shrink the earlier layer that already stored the package metadata. That is why update, install, and cleanup usually belong in the same RUN.

Split Unrelated Steps When It Improves Clarity or Cache Reuse

Not every instruction should be forced into one giant shell chain. If steps are logically separate and likely to change independently, separate RUN instructions can be easier to maintain.

dockerfile
RUN useradd -m appuser
RUN mkdir -p /app && chown -R appuser:appuser /app

This separation makes the file easier to review and lets Docker reuse cached results for unaffected instructions.

The broader rule is simple: separate by change frequency and conceptual responsibility, not by superstition about layer count.

BuildKit Changed the Old “Fewer Layers Always Wins” Advice

Older Docker advice focused heavily on minimizing layer count. Modern BuildKit makes the conversation more nuanced, because cache mounts and smarter build behavior often matter more than raw layer count.

dockerfile
1# syntax=docker/dockerfile:1.7
2FROM python:3.12-slim
3WORKDIR /app
4COPY requirements.txt .
5
6RUN --mount=type=cache,target=/root/.cache/pip \
7    pip install --no-cache-dir -r requirements.txt
8
9COPY . .
10CMD ["python", "main.py"]

This is readable, efficient, and not built around the outdated idea that every separate RUN is automatically bad.

Instruction Order Usually Matters More Than Style Arguments

The biggest cache win often comes from ordering instructions by change frequency, not from arguing about one RUN versus many.

dockerfile
1COPY package.json package-lock.json ./
2RUN npm ci --omit=dev
3COPY . .
4RUN npm run build

Here the important optimization is that dependency installation is isolated from source-code changes. That cache behavior usually matters more than whether a few unrelated commands were chained together.

Readability Still Matters

A 200-character shell chain with multiple &&, cleanup commands, and conditional logic can become harder to maintain than a few well-separated steps. If the line becomes difficult to audit, the theoretical layer benefit is often not worth it.

Good Dockerfiles optimize for both machine behavior and human understanding. A build file is part of the codebase, not only a compression exercise.

Use Multi-Stage Builds for Bigger Wins

If size and security matter, multi-stage builds usually matter more than micro-optimizing RUN style.

dockerfile
1FROM golang:1.23 AS build
2WORKDIR /src
3COPY . .
4RUN go build -o app ./cmd/server
5
6FROM gcr.io/distroless/base-debian12
7COPY --from=build /src/app /app
8ENTRYPOINT ["/app"]

This often produces a much larger practical improvement than debating whether one build stage should have three RUN lines or one chained line.

Common Pitfalls

  • Splitting package update, install, and cleanup across separate layers.
  • Chaining unrelated commands into unreadable one-liners.
  • Optimizing for layer count while ignoring instruction order and cache behavior.
  • Treating old Docker folklore as a substitute for measuring real build and image outcomes.
  • Focusing on RUN style while missing larger gains from multi-stage builds.

Summary

  • There is no universal rule that one chained RUN is always better than multiple RUN lines.
  • Chain tightly related package-manager steps, especially when cleanup belongs with install.
  • Split unrelated steps when it improves readability or cache reuse.
  • Modern BuildKit reduces the value of simplistic “minimize layer count” advice.
  • Measure actual build time and image size instead of following Dockerfile dogma.

Course illustration
Course illustration

All Rights Reserved.