How to Choose the Right Base Image for Multi-Stage Builds?

When choosing base images in a multi-stage Docker build, it depends on: - The project’s programming language (Node.js, Python, Java, Go, etc.) - The role of each stage (Build stage vs. Final stage) - Performance & security needs (Size, dependencies, vulnerabilities)


General Rule: Build Stage vs. Final Stage

Stage Purpose Typical Base Images
Build Stage Compiles the application, installs dependencies Alpine, Debian, Ubuntu, official language images (node, golang, python, maven, etc.)
Final Stage Runs the application efficiently Alpine, Distroless, Nginx, Scratch (minimal, optimized)

How to Decide?

πŸ‘‰ Is the project compiled (like Go, Java)?

  • Use a heavy image in the build stage (e.g., golang, maven, node)
  • Use a lightweight runtime in the final stage (e.g., scratch, distroless, nginx)

πŸ‘‰ Does the project need an interpreter (like Python, Node.js)?

  • Use a full OS base in the final stage (e.g., python:slim, node:alpine)

πŸ‘‰ Does the project serve static files?

  • Use nginx in the final stage

πŸ‘‰ Is security & size a priority?

  • Use alpine or distroless

Project-Specific Examples

Example 1: Node.js (React/Vue) Frontend with Nginx

Why? Node.js needed for npm build, but runtime should be lightweight (nginx).

Dockerfile:

# Build stage
FROM node:alpine AS build
WORKDIR /app
COPY . .
RUN npm install && npm run build

# Final stage (only static files)
FROM nginx:alpine AS final
COPY --from=build /app/dist /usr/share/nginx/html
CMD ["nginx", "-g", "daemon off;"]
- Node.js used only for building β†’ Nginx serves final static site - Faster, smaller, no unnecessary Node.js runtime


Example 2: Java Spring Boot with OpenJDK

Why? Maven used for build, but only JDK runtime needed in final stage.

Dockerfile:

# Build stage
FROM maven:3.8.6-openjdk-17 AS build
WORKDIR /app
COPY . .
RUN mvn clean package -DskipTests

# Final stage (runtime only)
FROM openjdk:17-jdk-slim AS final
WORKDIR /app
COPY --from=build /app/target/myapp.jar /app.jar
CMD ["java", "-jar", "/app.jar"]
- Maven used only in build stage - Final stage is much smaller, without Maven tools


Example 3: Python Flask App

Why? Python used for both build and runtime but optimized with python:slim.

Dockerfile:

# Build and runtime in one (smallest possible image)
FROM python:3.11-slim AS final
WORKDIR /app
COPY . .
RUN pip install --no-cache-dir -r requirements.txt
CMD ["python3", "app.py"]
- No need for multi-stage if runtime = build - Uses a slim image for security & size


Example 4: Golang App (Fully Compiled)

Why? Go compiles to a single binary, so final stage needs nothing except execution.

Dockerfile:

# Build stage
FROM golang:1.21-alpine AS build
WORKDIR /app
COPY . .
RUN go build -o myapp

# Final stage (smallest possible)
FROM scratch AS final
COPY --from=build /app/myapp /myapp
CMD ["/myapp"]
- Final image contains only the binary, no OS, no extra files - Extremely small (~10MB vs. 100+MB)


Key Takeaways

  • Pick a build image based on project dependencies (Node.js, Maven, Golang, etc.)
  • Pick a final image based on runtime needs (Alpine, Distroless, Nginx, Scratch, Slim, etc.)
  • Optimize for size & security
  • Always remove unnecessary dependencies from the final image