python

Creating Multi-Stage Builds with NestJS: Reducing Build Time and Size

Multi-stage builds in NestJS optimize Docker images, reducing size and build times. They separate build and production stages, include only necessary files, and leverage caching for faster incremental builds.

Creating Multi-Stage Builds with NestJS: Reducing Build Time and Size

Alright, let’s dive into the world of multi-stage builds with NestJS. As a developer who’s been around the block a few times, I can tell you that optimizing your build process is like finding a secret shortcut on your daily commute – it might not seem like much at first, but boy, does it make a difference in the long run!

When I first started working with NestJS, I was blown away by its powerful features and modular architecture. But as my projects grew in complexity, I noticed that my build times were starting to creep up, and my Docker images were getting a bit… well, chunky. That’s when I discovered the magic of multi-stage builds.

Multi-stage builds are like a Marie Kondo approach to your Docker images – they help you keep only what sparks joy (or in this case, what’s absolutely necessary for your app to run). The basic idea is to use multiple stages in your Dockerfile, where each stage builds upon the previous one, but only carries forward what’s essential.

Let’s start with a simple example. Here’s what a basic Dockerfile for a NestJS app might look like:

FROM node:14

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .
RUN npm run build

CMD ["npm", "run", "start:prod"]

This works, but it’s not very efficient. We’re carrying around all our dev dependencies and source code in the final image. Let’s see how we can improve this with a multi-stage build:

# Build stage
FROM node:14 AS builder

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .
RUN npm run build

# Production stage
FROM node:14-alpine

WORKDIR /app

COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules

CMD ["node", "dist/main"]

Now we’re cooking with gas! This Dockerfile uses two stages: a build stage and a production stage. The build stage does all the heavy lifting – installing dependencies, copying the source code, and building the app. The production stage then copies only the built assets and production dependencies from the build stage.

The result? A much slimmer final image that contains only what’s needed to run your app in production. It’s like going from a bulky winter coat to a sleek windbreaker – you’re still protected from the elements, but you can move a lot faster!

But wait, there’s more! We can take this a step further by using a specific build image for NestJS. The NestJS CLI provides a nest build command that can create a standalone application, which can run without node_modules. Here’s how we can leverage that:

# Build stage
FROM node:14 AS builder

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .
RUN npm run build
RUN npm run build:prod

# Production stage
FROM node:14-alpine

WORKDIR /app

COPY --from=builder /app/dist ./dist

CMD ["node", "dist/main"]

In this version, we’re using npm run build:prod (which you’d set up in your package.json to run nest build --webpack) to create a standalone application. This means our production stage doesn’t even need the node_modules directory!

Now, you might be thinking, “That’s all well and good, but what about my development workflow?” Great question! One of the things I love about multi-stage builds is that they play nicely with development environments too. You can use the same Dockerfile for both production and development by leveraging build arguments:

# Build stage
FROM node:14 AS builder

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .
RUN npm run build

# Development stage
FROM builder AS development

CMD ["npm", "run", "start:dev"]

# Production stage
FROM node:14-alpine AS production

WORKDIR /app

COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules

CMD ["node", "dist/main"]

With this setup, you can build for development with docker build --target development -t myapp:dev . and for production with docker build --target production -t myapp:prod .. It’s like having your cake and eating it too!

But what about build time, you ask? Multi-stage builds can help there too. By leveraging Docker’s build cache effectively, you can significantly reduce build times for incremental changes. Here’s a pro tip: structure your Dockerfile to copy and install dependencies before copying your source code. This way, if your dependencies haven’t changed, Docker can use the cached layer:

FROM node:14 AS builder

WORKDIR /app

COPY package*.json ./
RUN npm install

# Copy source code after installing dependencies
COPY . .
RUN npm run build

This simple change can shave minutes off your build time when you’re iterating quickly on your code.

Now, let’s talk about some real-world optimizations. In one project I worked on, we were dealing with a monorepo structure where our NestJS app was just one part of a larger ecosystem. We used Lerna to manage our packages, which added an extra layer of complexity to our builds.

To tackle this, we created a custom build script that only built the packages our NestJS app depended on, rather than building the entire monorepo. We then used this script in our Dockerfile:

FROM node:14 AS builder

WORKDIR /app

COPY package*.json ./
COPY lerna.json ./
RUN npm install

COPY . .
RUN npm run build:nest-app

# ... rest of the Dockerfile

This approach, combined with careful use of .dockerignore to exclude unnecessary files, cut our build times by almost 40%!

Another technique I’ve found useful is to use build arguments to control what gets included in the final image. For example, you might want to include some debugging tools in a staging environment but not in production. Here’s how you can do that:

ARG NODE_ENV=production

FROM node:14-alpine

WORKDIR /app

COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules

RUN if [ "$NODE_ENV" = "staging" ] ; then npm install --only=production some-debug-tool ; fi

CMD ["node", "dist/main"]

You can then build your staging image with docker build --build-arg NODE_ENV=staging -t myapp:staging ..

One last tip: don’t forget about your npm scripts! They can be a powerful tool in your build optimization arsenal. For example, you might have a script that generates your API documentation. In development, you might want to regenerate this on every build, but for production, you could generate it once and copy it into your image:

FROM node:14 AS builder

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .
RUN npm run build
RUN npm run generate-docs

FROM node:14-alpine AS production

WORKDIR /app

COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/docs ./docs

CMD ["node", "dist/main"]

In conclusion, multi-stage builds in NestJS are like a Swiss Army knife for your Docker images – they help you create lean, mean, production-ready machines while still maintaining a flexible development environment. By carefully structuring your Dockerfile, leveraging build cache, and using techniques like build arguments and custom npm scripts, you can significantly reduce both your build times and final image sizes.

Remember, optimizing your build process is an ongoing journey. As your app evolves, so too should your build strategy. Keep experimenting, measuring, and refining. And most importantly, have fun with it! After all, there’s something deeply satisfying about watching those build times drop and image sizes shrink. Happy building!

Keywords: NestJS, Docker, multi-stage builds, optimization, containerization, DevOps, Node.js, microservices, CI/CD, performance



Similar Posts
Blog Image
Are You Running Your FastAPI App Without a Dashboard? Here's How to Fix That!

Guard Your FastAPI: Transform Monitoring with Prometheus and Grafana for a Smooth, Stable App

Blog Image
Marshmallow and Flask-RESTful: Building Scalable APIs with Ease

Flask, Flask-RESTful, and Marshmallow create a powerful ecosystem for building scalable APIs. They simplify development, handle data serialization, and provide robust validation, making API creation efficient and maintainable.

Blog Image
How to Handle Circular References in Marshmallow with Grace

Marshmallow circular references tackled with nested schemas, lambda functions, and two-pass serialization. Caching optimizes performance. Testing crucial for reliability. Mix techniques for complex structures.

Blog Image
Is Your FastAPI App Missing the Magic of CI/CD with GitHub Actions?

FastAPI Deployment: From GitHub Actions to Traefik Magic

Blog Image
Mastering Python's Context Managers: Boost Your Code's Power and Efficiency

Python context managers handle setup and cleanup tasks automatically. They're not limited to file operations but can be used for various purposes like timing code execution, managing database transactions, and changing object attributes temporarily. Custom context managers can be created using classes or decorators, offering flexibility and cleaner code. They're powerful tools for resource management and controlling execution environments.

Blog Image
Why Is Testing FastAPI with Pytest the Secret Sauce for Stable APIs?

Mastering FastAPI Testing: A Recipe for Reliable APIs