Multi-stage Dockerfiles let you define multiple build stages. With the target option in Compose, you can choose which stage to build — giving you different images from the same Dockerfile.

A multi-stage Dockerfile

# Stage 1: dependencies
FROM node:20-slim AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci

# Stage 2: development (with dev dependencies and tools)
FROM deps AS dev
RUN npm install --include=dev
COPY . .
CMD ["npm", "run", "dev"]

# Stage 3: build
FROM deps AS build
COPY . .
RUN npm run build

# Stage 4: production (minimal)
FROM node:20-slim AS production
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=deps /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]

Targeting stages in Compose

Use target to pick which stage to build:

services:
  app:
    build:
      context: .
      target: dev
    volumes:
      - .:/app
    ports:
      - "3000:3000"

Different targets per environment

This is where target really shines — use override files to build different stages:

# compose.yml
services:
  app:
    build:
      context: .
      target: production
    ports:
      - "3000:3000"

# compose.override.yml (local dev)
services:
  app:
    build:
      target: dev
    volumes:
      - .:/app

Running docker compose up locally builds the dev stage with source mounting. In CI or production, docker compose -f compose.yml up builds the production stage.

Multiple services from one Dockerfile

Use target to build different services from the same Dockerfile:

services:
  api:
    build:
      context: .
      target: production
    command: ["node", "dist/api.js"]

  worker:
    build:
      context: .
      target: production
    command: ["node", "dist/worker.js"]

  tests:
    build:
      context: .
      target: dev
    command: ["npm", "test"]
    profiles: ["test"]

The api and worker share the same production image, while tests uses the dev stage with test dependencies included.

Combining target with build args

Use build args to further customize stages:

services:
  app:
    build:
      context: .
      target: production
      args:
        NODE_ENV: production
        VERSION: ${VERSION:-dev}

Pro tip

Use docker compose build --progress=plain to see which stages are built and which are cached:

# See full build output including cache hits
docker compose build --progress=plain

# Build a specific service
docker compose build --progress=plain app

Stages that aren’t needed for the target are skipped entirely — multi-stage builds are efficient by design.

Further reading