Step-by-Step Guide to Building an Efficient Multi-Stage Dockerfile
Best Practices for Structuring a Multi-Stage Dockerfile Efficiently

I'm an IT professional and business analyst, sharing my day-to-day troubleshooting challenges to help others gain practical experience while exploring the latest technology trends and DevOps practices. My goal is to create a space for exchanging ideas, discussing solutions, and staying updated with evolving tech practices.
Introduction
Optimizing Docker images is crucial for reducing deployment overhead, improving security, and enhancing performance. A well-structured multi-stage Dockerfile helps achieve these goals by separating build and runtime environments, leveraging Docker layer caching, and minimizing the final image size. In this article, we will explore the optimization strategies and best practices for building an efficient multi-stage Dockerfile.
Identify Optimization Goals:
Reduce Final Image Size: Remove unnecessary build tools and dependencies from the runtime image.
Improve Security: Minimize the attack surface by using minimal base images and dropping unnecessary privileges.
Leverage Layer Caching: Structure the Dockerfile to maximize caching efficiency for faster builds.
Multi-Stage Build Strategy:
1. Build Stage:
Use a larger base image with required compilers and tools.
Install dependencies and compile/package the application.
2. Runtime Stage:
Use a minimal base image (e.g., Alpine, scratch).
Copy only the compiled artifacts from the build stage.
Key Optimization Strategies:
Use Small Base Images: Alpine (Node.js), Distroless (Java/Python) minimize unnecessary libraries.
Remove Unnecessary Files: Delete temporary files, caches, and unused dependencies after the build process.
Leverage Layer Caching: Copy dependency files first to optimize build time and avoid unnecessary reinstallations.
Copy Only Required Files: Avoid copying the entire source code; instead, copy only necessary artifacts.
Implementation: Multi-Stage Dockerfile Example
Optimized Dockerfile for a Node.js App:
# ----------------------
# Stage 1: Build Stage
# ----------------------
FROM node:18-alpine AS builder
WORKDIR /app
# Copy package files first to leverage Docker caching
COPY package*.json ./
# Install dependencies (including devDependencies)
RUN npm ci
# Copy source code and build
COPY . .
RUN npm run build # Compiles code to ./dist
# ----------------------
# Stage 2: Runtime Stage
# ----------------------
FROM nginx:alpine
# Copy compiled assets from the build stage
COPY --from=builder /app/dist /usr/share/nginx/html
# Copy custom nginx configuration (optional)
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Expose port and run nginx
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Explanation of Optimizations
Multi-Stage Separation:
Builder Stage: Uses
node:18-alpineto install dependencies and compile code.Runtime Stage: Uses
nginx:alpine(5MB) to serve static files, discarding unnecessary Node.js build tools.
Layer Caching:
- Copying
package*.jsonfirst ensures dependency installations are cached unless package files change.
- Copying
Minimal Base Images:
- Alpine-based images are lightweight, reducing the attack surface and unnecessary package installations.
Efficient File Copying:
- Only the final
distfolder is copied into the runtime stage, avoiding unnecessary files from being included.
- Only the final
Automatic Cleanup:
- Since the builder stage is discarded after artifact copying, manual cleanup isn't required.
Result
Before Optimization: A Node.js image with build tools might be ~1GB.
After Optimization: The final image (Nginx + static files) is ~20MB.
Additional Best Practices
Use
.dockerignoreto exclude unnecessary files:node_modules Dockerfile .gitEnhance Security:
- Run containers as non-root:
RUN chown -R nginx:nginx /usr/share/nginx/html
USER nginx
Further Size Reduction:
Use
scratch(empty base image) for Go/Rust binaries.Use
distrolessimages for Python/Java (Google’s minimal images).
Testing the Optimized Image
1. Build the Image:
docker build -t my-optimized-app .
2. Verify the Image Size:
docker images | grep my-optimized-app
3. Run the Container:
docker run -p 3000:80 my-optimized-app
Conclusion
Using a well-structured multi-stage Dockerfile significantly improves efficiency, security, and maintainability. By leveraging small base images, layer caching, and artifact separation, teams can create lightweight, production-ready containers. Regularly refining these practices ensures optimal performance and cost efficiency in Dockerized applications.



