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
- Large Image Sizes: Build tools and dependencies bloat final images
- Security: Build tools shouldn’t be in production images
- Efficiency: Separate build and runtime environments
- 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:
- Stage 1 (builder): Installs dependencies and builds the application
- Stage 2 (production): Copies only built files and runtime dependencies
- 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:
- Smaller Images: Final image contains only runtime dependencies
- Security: Build tools not in production image
- Efficiency: Separate build and runtime environments
- 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
--fromin 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:
- Use Alpine Images:
FROM node:24-alpine AS production # Smaller base
- Copy Only Needed Files:
COPY --from=builder /app/dist ./dist # Not entire /app
- 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
- 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:
- Build Specific Stage:
docker build --target builder -t debug:builder .
docker run -it debug:builder sh
- 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
- 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:
- Not Naming Stages:
# Bad
FROM node:24
FROM node:24-alpine
# Good
FROM node:24 AS builder
FROM node:24-alpine AS production
- Copying Too Much:
# Bad
COPY --from=builder /app ./
# Good
COPY --from=builder /app/dist ./dist
- 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
- 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
FROMstarts a new stage - Use
COPY --fromto copy between stages - Name stages for clarity
- Only final stage is in final image
- Use
--targetto 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.