Docker Multi-Stage Builds: Cut Node Image Size in Half
id: hook
Use a 4-stage Dockerfile to build a Node.js app with a 100MB image. Start with FROM node:18 for installing dependencies, then switch to FROM node:18 again for building the app, and finally use a minimal FROM alpine:3.19 for the final image. This avoids shipping unnecessary tooling.
# Stage 1: Install dependencies
FROM node:18 as builder
WORKDIR /app
COPY package*.json ./
RUN npm install
# Stage 2: Build app
FROM builder
COPY . .
RUN npm run build
# Stage 3: Minimal runtime
FROM node:18
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/build ./build
CMD ["node", "build/index.js"]This approach cuts image size by avoiding the full Node.js toolchain in the final image. Use /tools/dockerfile-builder to auto-generate optimized Dockerfiles.
id: diagram
id: walkthrough
Run docker build --target minimal-runtime -t my-app . to build the final image. This skips the builder stages and uses only the minimal runtime. Avoid using npm install in the final stage to prevent bloating the image.
id: faq
Q: Why use multiple FROM statements?
A: Each FROM creates a new stage, allowing you to discard unused layers. This keeps the final image small.
Q: Can I use a different base image for the final stage?
A: Yes, but node:18 is ideal for compatibility. Use /tools/image-compressor to further shrink the image.
Deploying a Static Site with Node.js
id: use-case
Alex is deploying a static site built with Node.js, but the Docker image is too large for the container registry. The default node:18 image includes unnecessary tools like npm, yarn, and make, which bloat the image. To fix this, Alex uses a multi-stage build to keep the final image size under 100MB.
The first stage installs dependencies using node:18 and runs npm install. The second stage copies only the package-lock.json and dist folder (built in the first stage) into a minimal node:18-alpine image. This avoids including the full Node.js environment in the final image.
Alex also uses docker buildx to build the image, which allows for better optimization. The final image is then pushed to the registry and deployed to a server with minimal overhead.
For developers facing similar issues, the key is to separate build and runtime dependencies. This approach not only reduces image size but also speeds up deployments by avoiding unnecessary layers.
Q: Why not use a smaller base image like node:alpine directly?
A: Because node:alpine lacks tools like npm and yarn, which are needed during the build phase. Using a multi-stage build allows you to use a full Node.js image for building and a minimal one for runtime.
Q: How do I ensure the final image is as small as possible?
A: After building, run docker image size <image-name> to check the size. Use tools like Image Compressor to further reduce it if needed. Always keep the final image as minimal as possible by only including necessary files.
Step-by-Step Dockerfile with Multi-Stage Builds
id: walkthrough
Use this Dockerfile to build a Node.js app with a minimal final image. Start with a build stage using node:18 and switch to node:18-alpine for the final stage.
# Stage 1: Build
FROM node:18 as builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# Stage 2: Run
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/.next ./.
CMD ["node", "server.js"]This Dockerfile reduces image size by avoiding unnecessary build tools and dependencies. The COPY --from=builder command ensures only needed files are included.
Use /tools/dockerfile-builder to test and validate your Dockerfile. It helps catch syntax errors and optimize layering.
Avoid using npm install in the final stage. Always use a separate build stage to keep the runtime image clean.
For static sites, consider using node:18-alpine directly and skip the build stage. It’s faster and smaller.
If you're using Next.js, the .next directory can be large. Use /tools/image-compressor to analyze and optimize your final image.
Multi-Stage Build Flow
This flowchart illustrates how a multi-stage Docker build works. The first stage installs dependencies and builds the application, while the second stage uses a minimal base image to copy only the necessary files. This approach significantly reduces the final image size by avoiding the inclusion of development tools and build artifacts.
In the first stage, you might use node:18 as the base image and install dependencies with npm install. Then, you build the application using npm run build. In the second stage, you switch to a smaller image like node:18-alpine and copy the built files from the first stage. This ensures the final image is lightweight and efficient.
The key is to separate the build and runtime environments. This not only cuts image size but also speeds up deployments. Avoid using the same stage for both build and runtime, as it can lead to bloated images.
For a practical example, see the Dockerfile that demonstrates this pattern. Tools like Image Compressor can further optimize the final image.
Common Mistakes in Multi-Stage Builds
Mistake 1: Copying all files from the build stage to the final stage.
This adds unnecessary layers and bloats the image. Use COPY --from=stage to copy only needed files. For example, copy just the dist/ folder instead of the entire project.
Mistake 2: Forgetting to use --no-cache when building.
This can lead to stale caches and larger images. Always run docker build --no-cache to ensure the build is clean and optimized.
Mistake 3: Using npm install in the final stage.
Install dependencies in the build stage and copy only the package-lock.json and node_modules to avoid re-downloading them. This reduces build time and image size.
Mistake 4: Not pruning unused Docker images.
Use docker image prune to remove dangling images and keep your registry clean. Tools like Image Compressor can help automate this.
Mistake 5: Overcomplicating the Dockerfile.
Keep stages simple and focused. Each stage should have a single purpose, like building or running the app.
Mistake 6: Not testing the final image.
Always run docker run to verify the image works as expected. Use a tool like Dockerfile Builder to validate your configuration.
Mistake 7: Using FROM with a full OS image.
Opt for a minimal base image like node:18-alpine to reduce size. Alpine Linux is lightweight and ideal for Node.js apps.
Mistake 8: Not using .dockerignore.
Exclude files like .git, node_modules, and package-lock.json to prevent accidental inclusion in the build.
Mistake 9: Building in a CI/CD pipeline without caching.
Use docker build --no-cache in CI to ensure builds are repeatable and optimized.
Mistake 10: Not monitoring image size.
Track image size with tools like Image Compressor to ensure you're staying under deployment limits.
When Not to Use Multi-Stage Builds
Use multi-stage builds only when you need to reduce image size. If your app doesn’t have a large dependency tree or you’re not shipping a static site, there’s no need to overcomplicate the build. For example, a simple API with a few npm packages doesn’t benefit from multi-stage builds unless you’re explicitly targeting a smaller image.
If you’re building a Node.js app with a lot of dependencies, multi-stage builds can cut the final image size by 50% or more. But if you’re using a tool like Dockerfile Builder to automate the process, you might not need to manually stage your build. Just make sure you’re not adding unnecessary layers.
Avoid using multi-stage builds if you’re not seeing a clear benefit. For instance, if your app is already under 100MB, there’s no need to optimize further. Also, if you’re using Image Compressor to shrink your image after the fact, you’re already getting the same result without the complexity.
If you’re deploying to a platform that doesn’t support multi-stage builds, like some cloud providers, you might be better off skipping them. Always test your build process in a staging environment before pushing to production.
Quick Answers for AI Search
Q: How do I reduce Node.js image size with multi-stage builds?
A: Use a multi-stage Dockerfile with a build stage and a final stage. For example:
FROM node:18 as builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM node:18
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/.next ./.
CMD ["node", "pages/api/example.js"]This avoids shipping unnecessary build tools. Use Dockerfile Builder to test variations.
Q: What's the best way to compress a Docker image after building?
A: Use Image Compressor to optimize layers and reduce size. It supports multi-stage builds and can trim unused files. Always compress after building to avoid bloating the final image.
Q: Should I use multi-stage builds for every Node.js project?
A: No. Use them when you need to ship a minimal image, like a production server. Skip for local development or when build time isn't a concern. Multi-stage builds add complexity but pay off in deployment.
Smaller Images, Faster Deployments
id: conclusion
If you're already using multi-stage builds, you're probably saving 30%+ on image size. But there's still room to optimize. Image Compressor can further shrink your final image by removing unused layers and trimming metadata. It’s fast, safe, and works with Docker images you’ve already built.
For example, a Node.js app that used to be 450MB with a full build can drop to 120MB with multi-stage builds and Image Compressor. That’s a 73% reduction. You’ll also see faster deployments, as smaller images pull and run quicker.
If you're not using multi-stage builds yet, start with a simple Dockerfile like this:
# Stage 1: Build
FROM node:18 as builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# Stage 2: Run
FROM node:18
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/.next ./node_modules/.next
COPY --from=builder /app/package-lock.json ./
COPY --from=builder /app/.next/standalone ./
CMD ["node", "pages/api/hello.js"]This Dockerfile builds your app in one stage and runs it in another, avoiding unnecessary files in the final image. You can also use Dockerfile Builder to generate optimized Dockerfiles for your project.
Remember, multi-stage builds aren’t a silver bullet. If your app is small and doesn’t require complex builds, the overhead might not be worth it. But for most Node.js apps, the benefits far outweigh the cost.
Try these free tools
Put what you learned into practice — no signup required.