One Dockerfile, multiple environments. Use target to build only the stage you need - faster builds, smaller images, cleaner separation.

The basics

Multi-stage Dockerfile:

# Development stage
FROM node:20-alpine AS development
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["npm", "run", "dev"]

# Production stage
FROM node:20-alpine AS production
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
CMD ["npm", "start"]

Target specific stages in compose.yml:

services:
  app-dev:
    build:
      context: .
      target: development  # Stops at development stage
    volumes:
      - .:/app

  app-prod:
    build:
      context: .
      target: production  # Builds to production stage

Real example

Go application with test and production stages:

# Dockerfile
FROM golang:1.21-alpine AS base
WORKDIR /app
COPY go.* ./
RUN go mod download

FROM base AS test
COPY . .
RUN go test -v ./...

FROM base AS builder
COPY . .
RUN CGO_ENABLED=0 go build -o server

FROM alpine:3.19 AS production
RUN apk --no-cache add ca-certificates
COPY --from=builder /app/server /server
CMD ["/server"]
# compose.yml
services:
  app:
    build:
      context: .
      target: ${BUILD_TARGET:-production}
    ports:
      - "8080:8080"

  test:
    build:
      context: .
      target: test
    profiles: ["test"]

Running different targets

# Development
BUILD_TARGET=development docker compose up

# Run tests
docker compose --profile test build

# Production
BUILD_TARGET=production docker compose build

Size comparison

# Development image (includes all tools)
docker compose build --no-cache app-dev
# Image size: 450MB

# Production image (optimized)
docker compose build --no-cache app-prod
# Image size: 12MB

That’s 37x smaller for production!

CI pattern

Run tests without building production:

services:
  ci-test:
    build:
      context: .
      target: test
# CI fails fast if tests don't pass
docker compose build ci-test || exit 1

Pro tip

Use environment variables for flexible target selection:

# Development by default
export BUILD_TARGET=development

# Switch to production for deployment
BUILD_TARGET=production docker compose up -d

One compose file, multiple environments!

Further reading