Optimize Node.js Images with Multi-Stage Builds
Multi-stage builds separate your build environment from your final runtime. This lets you use heavy tools to build your Node.js app, then ship only the lean production code, drastically reducing image size and attack surface.
WHY IT EXISTS Default Dockerfiles for Node.js applications often create bloated, insecure, and slow images. They bundle build tools, development dependencies, and source code into the final production image. This increases the attack surface and slows down deployment pipelines due to large image sizes.
THE MENTAL MODEL A multi-stage build is like having a separate workshop to assemble furniture. In the first stage (the workshop), you use all your heavy tools and read the full instruction manual (the Node SDK, devDependencies, compilers). Once the furniture is built, you move only the finished piece into your home (the final image), leaving all the tools, scraps, and manuals behind. The result is a clean, minimal final environment.
HOW IT WORKS A single Dockerfile can contain multiple FROM instructions. Each FROM starts a new build stage, which can be named (e.g., FROM node:18 AS builder). A later stage can then copy files from a previous one using the COPY --from=<stage_name> flag. For a Node.js app, a 'builder' stage installs all dependencies, including devDependencies, to build or transpile the code. A final 'production' stage starts from a clean, minimal Node.js image and copies only the necessary application code and production dependencies from the builder stage. Only the final stage is saved as the resulting image.
WHEN TO USE IT Use multi-stage builds for virtually all production Node.js applications destined for containers. The benefits are significant: smaller image sizes (often by 50-90%), improved security by excluding build tools like compilers and shells, and faster CI/CD pipeline performance due to smaller artifacts being pushed and pulled.
WHEN NOT TO USE IT There are very few reasons to avoid multi-stage builds. For a quick, temporary development container where image size is completely irrelevant, a single-stage build might be marginally faster to write. However, for any code that will be shared or deployed, using a multi-stage build is a best practice.
ONE CANONICAL EXAMPLE A typical Dockerfile first defines a 'builder' stage starting from a full Node image. It copies package.json and package-lock.json, then runs npm install to leverage Docker's layer caching. Only then does it copy the rest of the source code and run a build script if needed. The second, final stage starts from a minimal base image like node:18-alpine. It copies package*.json again and runs npm install --only=production. Finally, it uses COPY --from=builder to pull the built application code from the first stage. The final image contains only the runtime, production dependencies, and the application itself.
Read the original → docs.docker.com
Get five bites like this every day.
Tezvyn delivers a daily feed of 60-second tech bites with quizzes to lock in what you learn.