backend / docker / 08_multi_stage_from.md

Multiple FROM Statements in Dockerfile (Multi-Stage Builds)

3 interview angles 10 min read source

Multiple FROM Statements in Dockerfile (Multi-Stage Builds)

Definition

Multiple FROM statements in a Dockerfile enable multi-stage builds, a feature that allows you to use multiple base images in a single Dockerfile. Each FROM statement starts a new build stage, and you can copy files from previous stages to create smaller, more efficient final images.

Why Use Multi-Stage Builds?

Problems Solved

  1. Large Image Sizes: Build tools and dependencies bloat final images
  2. Security: Build tools shouldn’t be in production images
  3. Efficiency: Separate build and runtime environments
  4. Optimization: Only include what’s needed in final image

Benefits

  • Smaller Images: Final image contains only runtime dependencies
  • Better Security: No build tools in production
  • Faster Deployments: Smaller images = faster pulls
  • Cleaner Separation: Build vs Runtime concerns

Basic Multi-Stage Build

Simple Example

# Stage 1: Build stage
FROM node:24 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

# Stage 2: Production stage
FROM node:24-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./
CMD ["node", "dist/index.js"]

What Happens:

  1. Stage 1 (builder): Installs dependencies and builds the application
  2. Stage 2 (production): Copies only built files and runtime dependencies
  3. Final Image: Contains only what’s needed to run the app

Build Command

# Build the image
docker build -t my-app:latest .

# The final image only contains stage 2
# Stage 1 is discarded after build

Advanced Multi-Stage Patterns

1. Named Stages

# Stage 1: Dependencies
FROM node:24 AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

# Stage 2: Build
FROM node:24 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 3: Production
FROM node:24-alpine AS production
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY package*.json ./
CMD ["node", "dist/index.js"]

2. Python Multi-Stage Build

# Stage 1: Build dependencies
FROM python:3.14-slim AS builder
WORKDIR /app

# Install build dependencies
RUN apt-get update && apt-get install -y \
    gcc \
    g++ \
    && rm -rf /var/lib/apt/lists/*

# Install Python dependencies
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

# Stage 2: Runtime
FROM python:3.14-slim
WORKDIR /app

# Copy installed packages from builder
COPY --from=builder /root/.local /root/.local

# Copy application code
COPY . .

# Make sure scripts in .local are usable
ENV PATH=/root/.local/bin:$PATH

CMD ["python", "app.py"]

3. Go Multi-Stage Build

# Stage 1: Build
FROM golang:1.21 AS builder
WORKDIR /app

# Copy go mod files
COPY go.mod go.sum ./
RUN go mod download

# Copy source code
COPY . .

# Build the application
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

# Stage 2: Minimal runtime
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/

# Copy binary from builder
COPY --from=builder /app/app .

CMD ["./app"]

4. Java Multi-Stage Build

# Stage 1: Build
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /app

# Copy pom.xml and download dependencies
COPY pom.xml .
RUN mvn dependency:go-offline

# Copy source and build
COPY src ./src
RUN mvn clean package -DskipTests

# Stage 2: Runtime
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app

# Copy JAR from builder
COPY --from=builder /app/target/app.jar app.jar

EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

5. Multi-Stage with Tests

# Stage 1: Dependencies
FROM node:24 AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci

# Stage 2: Build
FROM node:24 AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

# Stage 3: Test
FROM node:24 AS test
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm test

# Stage 4: Production
FROM node:24-alpine AS production
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=deps /app/node_modules ./node_modules
COPY package*.json ./
CMD ["node", "dist/index.js"]

Copying from Previous Stages

COPY –from Syntax

# Copy from named stage
COPY --from=builder /app/dist ./dist

# Copy from stage number (0-indexed)
COPY --from=0 /app/dist ./dist

# Copy from external image
COPY --from=nginx:alpine /etc/nginx/nginx.conf /etc/nginx/nginx.conf

Examples

# Stage 1
FROM node:24 AS builder
WORKDIR /app
RUN npm install && npm run build

# Stage 2
FROM nginx:alpine
# Copy built files from builder
COPY --from=builder /app/dist /usr/share/nginx/html
# Copy nginx config
COPY nginx.conf /etc/nginx/nginx.conf

Complex Multi-Stage Scenarios

1. Multiple Build Tools

# Stage 1: Install system dependencies
FROM ubuntu:22.04 AS base
RUN apt-get update && apt-get install -y \
    build-essential \
    cmake \
    && rm -rf /var/lib/apt/lists/*

# Stage 2: Build C++ application
FROM base AS cpp-builder
WORKDIR /app
COPY cpp-src ./cpp-src
RUN cd cpp-src && make

# Stage 3: Build Python application
FROM base AS python-builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user -r requirements.txt

# Stage 4: Final image
FROM python:3.14-slim
WORKDIR /app
COPY --from=cpp-builder /app/cpp-src/app ./cpp-app
COPY --from=python-builder /root/.local /root/.local
COPY . .
ENV PATH=/root/.local/bin:$PATH
CMD ["python", "app.py"]

2. Conditional Stages

# Build stage
FROM node:24 AS builder
WORKDIR /app
COPY . .
RUN npm run build

# Development stage
FROM node:24 AS development
WORKDIR /app
COPY --from=builder /app .
RUN npm install
CMD ["npm", "run", "dev"]

# Production stage
FROM node:24-alpine AS production
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./
CMD ["node", "dist/index.js"]

Build specific stage:

# Build development stage
docker build --target development -t my-app:dev .

# Build production stage
docker build --target production -t my-app:prod .

3. Multi-Architecture Builds

# Stage 1: Build for multiple architectures
FROM --platform=$BUILDPLATFORM node:24 AS builder
ARG TARGETPLATFORM
ARG BUILDPLATFORM
WORKDIR /app
COPY . .
RUN npm run build

# Stage 2: Runtime
FROM node:24-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/index.js"]

Best Practices

1. Name Your Stages

# Good: Named stages
FROM node:24 AS builder
FROM node:24-alpine AS production

# Avoid: Unnamed stages (harder to reference)
FROM node:24
FROM node:24-alpine

2. Use Alpine for Final Stage

# Build stage: Full image
FROM node:24 AS builder
# ... build steps ...

# Production: Minimal image
FROM node:24-alpine AS production
# ... copy from builder ...

3. Copy Only What’s Needed

# Good: Copy only built files
COPY --from=builder /app/dist ./dist

# Avoid: Copy entire build directory
COPY --from=builder /app ./

4. Separate Dependencies

# Stage 1: Install dependencies
FROM node:24 AS deps
COPY package*.json ./
RUN npm ci

# Stage 2: Build
FROM node:24 AS build
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

# Stage 3: Production (only production deps)
FROM node:24-alpine AS production
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist

5. Clean Up in Same Layer

# Good: Clean up in same RUN
RUN apt-get update && \
    apt-get install -y package && \
    rm -rf /var/lib/apt/lists/*

# Avoid: Separate RUN commands
RUN apt-get update
RUN apt-get install -y package
RUN rm -rf /var/lib/apt/lists/*

Common Interview Questions and Answers

Q1: What are multi-stage builds and why use them?

Multi-stage builds allow multiple FROM statements in a Dockerfile, each starting a new build stage. Benefits:

  1. Smaller Images: Final image contains only runtime dependencies
  2. Security: Build tools not in production image
  3. Efficiency: Separate build and runtime environments
  4. Optimization: Only include what’s needed

Example:

# Build stage
FROM node:24 AS builder
RUN npm install && npm run build

# Production stage
FROM node:24-alpine
COPY --from=builder /app/dist ./dist
# Final image is much smaller

Q2: How do you copy files between stages?

Use COPY --from:

# Stage 1
FROM node:24 AS builder
RUN npm run build

# Stage 2
FROM node:24-alpine
# Copy from named stage
COPY --from=builder /app/dist ./dist

# Copy from stage number (0-indexed)
COPY --from=0 /app/dist ./dist

# Copy from external image
COPY --from=nginx:alpine /etc/nginx/nginx.conf /etc/nginx/

Q3: What happens to intermediate stages?

Intermediate stages are:

  • Built: All stages are built during docker build
  • Cached: Stages are cached for faster rebuilds
  • Discarded: Only final stage is in final image
  • Accessible: Can be referenced by --from in later stages

View stages:

# Build with all stages
docker build -t my-app .

# Build specific stage
docker build --target builder -t my-app:builder .

Q4: Can you use multi-stage builds with docker-compose?

Yes:

version: '3.8'
services:
  app:
    build:
      context: .
      target: production  # Build specific stage
    ports:
      - "5000:5000"

Multiple targets:

services:
  app-dev:
    build:
      context: .
      target: development
  
  app-prod:
    build:
      context: .
      target: production

Q5: How do you optimize multi-stage builds?

Optimization strategies:

  1. Use Alpine Images:
FROM node:24-alpine AS production  # Smaller base
  1. Copy Only Needed Files:
COPY --from=builder /app/dist ./dist  # Not entire /app
  1. Separate Dependencies:
# Install deps separately
FROM node:24 AS deps
COPY package*.json ./
RUN npm ci

# Build separately
FROM node:24 AS build
COPY --from=deps /app/node_modules ./node_modules
  1. Clean Up in Same Layer:
RUN apt-get update && \
    apt-get install -y package && \
    rm -rf /var/lib/apt/lists/*

Q6: What’s the difference between single-stage and multi-stage builds?

Single-Stage:

FROM node:24
WORKDIR /app
COPY . .
RUN npm install && npm run build
CMD ["node", "dist/index.js"]
# Image includes: node, npm, build tools, source code

Multi-Stage:

FROM node:24 AS builder
RUN npm install && npm run build

FROM node:24-alpine
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/index.js"]
# Image includes: only runtime (alpine + dist)

Difference: Multi-stage produces smaller, more secure images.

Q7: How do you build specific stages?

Use --target:

# Build specific stage
docker build --target builder -t my-app:builder .

# Build final stage (default)
docker build -t my-app:latest .

# List all stages
docker build --target builder .

In docker-compose:

services:
  app:
    build:
      context: .
      target: production

Q8: Can you use variables in multi-stage builds?

Yes, with ARG:

# Stage 1
FROM node:24 AS builder
ARG NODE_ENV=production
ENV NODE_ENV=$NODE_ENV
RUN npm run build

# Stage 2
FROM node:24-alpine
ARG VERSION=latest
COPY --from=builder /app/dist ./dist
LABEL version=$VERSION

Build with args:

docker build --build-arg NODE_ENV=development -t my-app .

Q9: How do you debug multi-stage builds?

Debugging strategies:

  1. Build Specific Stage:
docker build --target builder -t debug:builder .
docker run -it debug:builder sh
  1. Inspect Intermediate Images:
# Build and inspect
docker build -t my-app .
docker images | grep my-app

# Run intermediate stage
docker run -it <intermediate-image-id> sh
  1. Add Debug Layers:
FROM node:24 AS builder
RUN npm install
# Debug: List files
RUN ls -la
RUN cat package.json

Q10: What are common mistakes in multi-stage builds?

Common mistakes:

  1. Not Naming Stages:
# Bad
FROM node:24
FROM node:24-alpine

# Good
FROM node:24 AS builder
FROM node:24-alpine AS production
  1. Copying Too Much:
# Bad
COPY --from=builder /app ./

# Good
COPY --from=builder /app/dist ./dist
  1. Forgetting Dependencies:
# Bad: Missing runtime deps
FROM alpine
COPY --from=builder /app/dist ./dist

# Good: Include runtime deps
FROM node:24-alpine
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
  1. Not Using Alpine:
# Bad: Large final image
FROM node:24 AS production

# Good: Small final image
FROM node:24-alpine AS production

Summary

Multi-stage builds enable:

  • Smaller Images: Only runtime dependencies in final image
  • Better Security: No build tools in production
  • Efficiency: Separate build and runtime environments
  • Flexibility: Multiple stages for different purposes

Key concepts:

  • Each FROM starts a new stage
  • Use COPY --from to copy between stages
  • Name stages for clarity
  • Only final stage is in final image
  • Use --target to build specific stages

Best practices:

  • Name all stages
  • Use Alpine for final stage
  • Copy only what’s needed
  • Separate dependencies
  • Clean up in same layer

Multi-stage builds are essential for creating production-ready Docker images that are small, secure, and efficient.

Interview angle

  • “What is a multi-stage build for?” - separating build from runtime. Compilers, headers and dev dependencies live in the build stage; only the artefact is copied into a slim final image. That cuts size dramatically and removes tooling an attacker could use.
  • “How does it help with secrets?” - a credential used during build never appears in the final image’s layers, since only the copied artefact carries over. Better still, use build secrets so it never lands in any layer.
  • “Can you target an intermediate stage?” - yes, --target, which is how you build a test image from the same Dockerfile without duplicating it.