docker-compose
container orchestration
build caching
development tools
docker commands

docker-compose up vs docker-compose up --build vs docker-compose build --no-cache

Master System Design with Codemia

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

The three commands docker-compose up, docker-compose up --build, and docker-compose build --no-cache control how Docker Compose builds images and starts containers. The short answer is that up reuses existing images, up --build rebuilds before starting, and build --no-cache rebuilds every layer from scratch. Choosing the wrong one wastes minutes on every iteration or, worse, ships stale code.

How Docker Layer Caching Works

Before comparing the commands, you need to understand what Docker is actually caching. Every instruction in a Dockerfile produces an intermediate layer. Docker hashes the instruction text plus the content of any files added by COPY or ADD. If the hash matches a cached layer, Docker skips execution and reuses the cached result.

dockerfile
1FROM node:20-alpine          # Layer 1 - base image
2WORKDIR /app                 # Layer 2 - working directory
3COPY package*.json ./        # Layer 3 - dependency manifest
4RUN npm ci                   # Layer 4 - install dependencies
5COPY . .                     # Layer 5 - application source
6RUN npm run build            # Layer 6 - build step

If you only change application source code, layers 1 through 4 are cache hits. Docker rebuilds only layers 5 and 6. This is the foundation of fast iterative builds, and it directly affects which compose command you should reach for.

docker-compose up

This is the default development workflow command. It reads your docker-compose.yml, creates networks and volumes if needed, and starts every defined service. If an image already exists locally, Compose reuses it without rebuilding.

yaml
1services:
2  api:
3    build: ./api
4    ports:
5      - "3000:3000"
6    depends_on:
7      - db
8  db:
9    image: postgres:16-alpine
10    environment:
11      POSTGRES_PASSWORD: devpass
12    volumes:
13      - pgdata:/var/lib/postgresql/data
14
15volumes:
16  pgdata:
bash
docker-compose up -d

Running this the first time builds the api image because none exists. Running it again reuses that cached image, even if you have changed source files since the last build. That is the critical behavior to understand: docker-compose up does not watch your source tree for changes.

When to use it

Use plain up when you have not changed any Dockerfiles or application code since the last build, or when you are restarting containers that were stopped. It is also the right command when all your services use pre-built images from a registry and no local build step is involved.

docker-compose up --build

Adding --build tells Compose to run the build step before starting containers. Docker still uses layer caching, so unchanged layers are fast. Only modified layers and their dependents are rebuilt.

bash
docker-compose up --build -d

In practice, this is the command most developers should reach for during active development. Suppose you edit a route handler in your API:

python
1# api/routes/health.py - changed file
2from flask import Blueprint
3
4health_bp = Blueprint("health", __name__)
5
6@health_bp.get("/health")
7def health_check():
8    return {"status": "ok", "version": "2.1.0"}  # bumped version

Running docker-compose up --build detects that the COPY . . layer's context changed, rebuilds from that point forward, and starts the updated container. Without --build, the old image keeps running your previous code.

Interaction with layer caching

The --build flag does not disable the cache. It simply guarantees that Compose runs docker build before docker start. Layers whose inputs have not changed are still pulled from cache. This is why --build typically adds only seconds to a well-structured Dockerfile.

docker-compose build --no-cache

This command rebuilds every image from the first instruction, ignoring all cached layers. It is a separate build step that does not start containers afterward.

bash
docker-compose build --no-cache
docker-compose up -d

Because every layer executes fresh, this is the slowest option. A Node.js project that takes 5 seconds with cache hits might take 90 seconds without them, mostly due to npm ci re-downloading every dependency.

When to use it

There are specific scenarios where a full cache bust is necessary:

  • Debugging "works on my machine" issues. A cached layer might contain an artifact from a previous build that masks a real problem.
  • Base image updates. If node:20-alpine received a security patch, cached layers still reference the old base. A no-cache build pulls the fresh base.
  • CI/CD pipelines. Some teams run no-cache builds in CI to guarantee reproducibility, accepting the time cost in exchange for confidence.
  • Dependency resolution changes. If your lockfile has not changed but upstream package behavior has (rare but real), caching the npm ci layer hides the difference.

You can also target a single service:

bash
docker-compose build --no-cache api

That rebuilds only the api service from scratch, leaving other services cached.

Command Comparison

CommandBuilds images?Uses layer cache?Starts containers?Typical use case
docker-compose upOnly if no image existsYesYesRestarting unchanged services
docker-compose up --buildAlwaysYesYesActive development with code changes
docker-compose build --no-cacheAlwaysNoNo (run up separately)Debugging, CI, base image refresh

Dockerfile Structure Matters More Than the Command

The biggest performance lever is not which command you run but how your Dockerfile is structured. The general rule is to put instructions that change least frequently at the top and instructions that change most frequently at the bottom.

A poorly ordered Dockerfile:

dockerfile
1FROM python:3.12-slim
2WORKDIR /app
3COPY . .                     # any source change invalidates everything below
4RUN pip install -r requirements.txt
5CMD ["python", "app.py"]

A well-ordered Dockerfile:

dockerfile
1FROM python:3.12-slim
2WORKDIR /app
3COPY requirements.txt .
4RUN pip install -r requirements.txt
5COPY . .
6CMD ["python", "app.py"]

In the second version, changing application code only invalidates the final COPY . . layer. The dependency installation layer stays cached. This single change can turn a 60-second rebuild into a 3-second rebuild when using docker-compose up --build.

Multi-Stage Builds and Cache Implications

Production Dockerfiles often use multi-stage builds. Each stage has its own cache chain:

dockerfile
1# Stage 1: build
2FROM node:20-alpine AS builder
3WORKDIR /app
4COPY package*.json ./
5RUN npm ci
6COPY . .
7RUN npm run build
8
9# Stage 2: production image
10FROM node:20-alpine
11WORKDIR /app
12COPY --from=builder /app/dist ./dist
13COPY --from=builder /app/node_modules ./node_modules
14CMD ["node", "dist/index.js"]

With --build, Docker caches both stages independently. With --no-cache, both stages rebuild from scratch. Understanding this is important because multi-stage builds already reduce final image size; combining them with proper cache ordering keeps iteration speed high.

Common Pitfalls

Running plain up after code changes and wondering why nothing changed. This is the most frequent mistake. The existing image is reused. Use --build.

Using --no-cache as a default habit. Some developers add --no-cache to every build out of caution. This throws away all caching benefits and can add minutes to every cycle. Reserve it for situations where you have a specific reason to distrust the cache.

Forgetting that build --no-cache does not start containers. Unlike up --build, the build command only produces images. You still need to run up afterward.

Not structuring the Dockerfile for cache efficiency. If your COPY . . comes before RUN pip install, every source code change triggers a full dependency reinstall regardless of which compose command you use.

Ignoring .dockerignore. Without a .dockerignore file, Docker includes everything in the build context, including node_modules, .git, and test artifacts. This inflates context size and causes unnecessary cache invalidation.

Summary

  • Use docker-compose up when images are already built and you just need to start or restart containers.
  • Use docker-compose up --build as your standard development command whenever code or Dockerfiles have changed. Layer caching keeps it fast.
  • Use docker-compose build --no-cache only when you need a guaranteed clean build, such as debugging cache-related issues or refreshing base images in CI.
  • Structure your Dockerfile so that frequently changing layers come last. This has a bigger impact on build speed than which command you choose.
  • Always pair build --no-cache with a follow-up up command, since it does not start containers on its own.

Course illustration
Course illustration

All Rights Reserved.