TL;DR
- Use Alpine Base Image Switching to a smaller Alpine version of Node.js can significantly reduce image size.
- Leverage Multi-Stage Builds Use multi-stage builds to separate build and production environments, including only necessary files in the final image.
- Install Only Production Dependencies to exclude development dependencies.
- Clean Up Post Installation remove unnecessary cached files and further reduce image size.
In modern web development, Docker has become an essential tool for packaging applications and their dependencies into containers. However, one common challenge developers face is managing the size of these Docker images. A smaller image reduces deployment times and bandwidth costs. As you know, NodeJS has very huge node_modules folder which is ton of mb in size. To reduce the size of the Docker image, we can use multi stage build process. In this blog post, we’ll explore how to create a production-graded Dockerfile for Node.js applications that prioritizes image size reduction while maintaining best practices.
Best Practices for Creating a Lightweight Dockerfile with NodeJS
I will be show step-by-step and explain how to create a production grade docker image for NestJS framework. Here is github repo example for this example code.
Start with Simple Dockerfile for NestJS
Firstly, get start with simple docker file. We will use this as a base image for our production image.
FROM node:20
# Create and change to the app directory
WORKDIR /usr/src/app
# Copy application dependency manifests to the container image.
# A wildcard is used to ensure both package.json AND package-lock.json are copied.
# Copy the package.json and package-lock.json
COPY package*.json ./
# Install dependencies.
RUN npm install
# Copy the rest of the application code
COPY . .
# Build the NestJS application
RUN npm run build
# Expose the port the app runs on
EXPOSE 3000
# Run the application
CMD ["npm", "run", "start:prod"]
node_modules
npm-debug.log
Dockerfile
.dockerignore
.git
.gitignore
tests
*.md
Result of the above Dockerfile is 1.14GB
Now, Let improve the image size step-by-step
1. Choose a Smaller Base Image
Next we can use lightweight base image such as alpine linux-based. Alpine images are built on a minimal Linux distribution, which significantly reduces unnecessary library.
FROM node:20-alpine
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "run", "start:prod"]
Result of the above Dockerfile is 308MB.
2. Use Multi-Stage Builds & Optimize Dependency Installation
Multi-stage builds allow you to create a separate build environment, enabling you to only copy over the necessary artifacts to your final production image. This drastically reduces image size by excluding unnecessary build files. Most of NodeJS Framework has a build step that will require a lot of dependencies that not need to be included in the final image. So we build the NodeJS Framework first and then copy it over to the final image.
Then we can remove unnecessary dependencies by using production only dependencies in stage of builded filed.
# Stage 1: Install All Dependencies & Build Dist File
FROM node:20-alpine AS builder
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# Stage 2: Production - Copy Builded Files from previous stage & install only production dependencies
FROM node:20-alpine AS production
WORKDIR /usr/src/app
# Copy only the necessary files from the builder stage
COPY --from=builder /usr/src/app/dist ./dist
COPY package*.json ./
RUN npm install --only=production
EXPOSE 3000
CMD ["npm", "run", "start:prod"]
Result of the above Dockerfile is 145MB. Because unnecessary dependencies will not install in production stage.
3. Clean Up After Installation
After installing dependencies, you can clean up unnecessary files and caches to reduce image size. This can be done using npm cache clean —force or similar commands if you’re using other package managers.
# Stage 1: Install All Dependencies & Build Dist File
FROM node:20-alpine AS builder
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# Stage 2: Production - Copy Builded Files from previous stage & install only production dependencies
FROM node:20-alpine AS production
WORKDIR /usr/src/app
# Copy only the necessary files from the builder stage
COPY --from=builder /usr/src/app/dist ./dist
COPY package*.json ./
RUN npm install --only=production && npm cache clean --force
EXPOSE 3000
CMD ["npm", "run", "start:prod"]
Result of the above Dockerfile is 143MB.
Conclusion
Creating a production-graded Dockerfile for your Node.js applications is crucial for optimizing performance and reducing image size. By leveraging multi-stage builds, selecting lightweight base images, managing dependencies wisely, and excluding unnecessary files with .dockerignore, you can significantly enhance your Docker images’ efficiency.