The Last Dockerfile You Need for NestJS

The Last Dockerfile You Need for NestJS

In this article

  • 🚒 We'll write a docker multi-stage build together.
  • πŸŽ‰ We'll discover how to use the docker file in development and production.
  • ✨ We'll apply some of the best practices recommended by the Docker team.

Let's go.


This article is also available on

Feel free to read it on your favorite platform✨


First, let's think about the use cases to design the docker build:

  • To install all the dependencies to support the local dev server.
  • To run a production server with an optimized bundle.

In order to cover both of the use cases, we'll use Docker's multi-stage builds and split the build into 3 stages:

Docker multi-stage build overview
  • dev: to simply install all the dependencies.
  • build: to compile a production build with optimized bundle size.
  • prod: to serve the production build.

The Docker Multi-stage Build

The "dev" Stage

It's fairly straightforward. All we need to do is to install npm dependencies and apply some of the best practices:

  • We'll use "node:alpine" as the based image to produce minimal image size.
  • We'll install missing shared libraries from node:alpine.
  • We'll assign a non-root user to Docker to limit its privileges
  • We'll set the environment to "development".
  • We'll install the dependencies based on yarn lock file to achieve consistent installation across machines.
Dockerfile
#
# β€πŸ’» Development
#
FROM node:18-alpine as dev
# add the missing shared libraries from alpine base image
RUN apk add --no-cache libc6-compat
# Create app folder
WORKDIR /app

# Set to dev environment
ENV NODE_ENV development

# Create non-root user for Docker
RUN addgroup --system --gid 1001 node
RUN adduser --system --uid 1001 node

# Copy source code into app folder
COPY --chown=node:node . .

# Install dependencies
RUN yarn --frozen-lockfile

# Set Docker as a non-root user
USER node

The "build" Stage

The build stage is the most interesting stage. The purpose is to compile the source code and generate the least amount of assets.

  • We'll use the same base image and best practices as the dev stage.
  • We'll set the environment to "production".
  • We'll copy the dependencies we installed in the dev stage to run the Nest CLI build script.
  • Once the build is completed, we'll install the production-only dependencies and clean the yarn cache. This will minimize the bundle size.
Dockerfile
#
# 🏑 Production Build
#
FROM node:18-alpine as build

WORKDIR /app
RUN apk add --no-cache libc6-compat

# Set to production environment
ENV NODE_ENV production

# Re-create non-root user for Docker
RUN addgroup --system --gid 1001 node
RUN adduser --system --uid 1001 node

# In order to run `yarn build` we need access to the Nest CLI.
# Nest CLI is a dev dependency.
COPY --chown=node:node --from=dev /app/node_modules ./node_modules
# Copy source code
COPY --chown=node:node . .

# Generate the production build. The build script runs "nest build" to compile the application.
RUN yarn build

# Install only the production dependencies and clean cache to optimize image size.
RUN yarn --frozen-lockfile --production && yarn cache clean

# Set Docker as a non-root user
USER node

The "prod" Stage

Now that we have the optimized production build, we can complete the docker build by serving the NestJS server.

  • We'll use the same base image and best practices as the build stage.
  • We'll copy the compiled output and the production-only node_modules from the build stage. These are the necessary files we need to run the server.
  • Finally, start the server with Node. "nest start" command is not available at this stage because the Nest CLI is a dev dependency.
Dockerfile
#
# πŸš€ Production Server
#
FROM node:18-alpine as prod

WORKDIR /app
RUN apk add --no-cache libc6-compat

# Set to production environment
ENV NODE_ENV production

# Re-create non-root user for Docker
RUN addgroup --system --gid 1001 node
RUN adduser --system --uid 1001 node

# Copy only the necessary files
COPY --chown=node:node --from=build /app/dist dist
COPY --chown=node:node --from=build /app/node_modules node_modules

# Set Docker as non-root user
USER node

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

The Complete Multi-stage Dockerfile

Dockerfile
#
# β€πŸ’» Development
#
FROM node:18-alpine as dev
# add the missing shared libraries from alpine base image
RUN apk add --no-cache libc6-compat
# Create app folder
WORKDIR /app

# Set to dev environment
ENV NODE_ENV dev

# Create non-root user for Docker
RUN addgroup --system --gid 1001 node
RUN adduser --system --uid 1001 node

# Copy source code into app folder
COPY --chown=node:node . .

# Install dependencies
RUN yarn --frozen-lockfile

# Set Docker as a non-root user
USER node

#
# 🏑 Production Build
#
FROM node:18-alpine as build

WORKDIR /app
RUN apk add --no-cache libc6-compat

# Set to production environment
ENV NODE_ENV production

# Re-create non-root user for Docker
RUN addgroup --system --gid 1001 node
RUN adduser --system --uid 1001 node

# In order to run `yarn build` we need access to the Nest CLI.
# Nest CLI is a dev dependency.
COPY --chown=node:node --from=dev /app/node_modules ./node_modules
# Copy source code
COPY --chown=node:node . .

# Generate the production build. The build script runs "nest build" to compile the application.
RUN yarn build

# Install only the production dependencies and clean cache to optimize image size.
RUN yarn --frozen-lockfile --production && yarn cache clean

# Set Docker as a non-root user
USER node

#
# πŸš€ Production Server
#
FROM node:18-alpine as prod

WORKDIR /app
RUN apk add --no-cache libc6-compat

# Set to production environment
ENV NODE_ENV production

# Re-create non-root user for Docker
RUN addgroup --system --gid 1001 node
RUN adduser --system --uid 1001 node

# Copy only the necessary files
COPY --chown=node:node --from=build /app/dist dist
COPY --chown=node:node --from=build /app/node_modules node_modules

# Set Docker as non-root user
USER node

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

How to Use It in Local Development

We are done with the hard part!

To use the docker file for local development, all we need to do is to build the docker image with the dev stage. For example, we can build and run the NestJS server with watch mode in a docker compose file:

docker-compose.yml
services:
  api:
    container_name: nestjs
    image: nestjs-dev
    restart: unless-stopped
    build:
      context: .
      dockerfile: Dockerfile
      # ✨ Target the dev stage
      target: dev
    # Mount host directory to docker container to support watch mode
    volumes:
      - .:/app
      # This ensures that the NestJS container manages the node_modules folder
      # rather than synchronizes it with the host machine
      - /app/node_modules
    env_file:
      - docker.env
    ports:
      - 8082:8082
    networks:
      - nest
    depends_on:
      - postgres
    command: npx nest start --watch

  # ...other services

How to Use It in Production

You can simply build the production image by building all 3 stages. For example, we can run "docker build" in GitHub Actions as part of the deployment workflow.

.github/workflows/ci.yml
jobs
  deploy:
    steps:
      - uses: actions/checkout@v3

      - uses: actions/setup-node@v3
        with:
          node-version: '18.x'

      - name: Build NestJS image
        env:
          REGISTRY: nestjs-regiestry
          REPOSITORY: nestjs-example
          IMAGE_TAG: example
        # ✨ target the production stage
        run: docker build . -t $REGISTRY/$REPOSITORY:$IMAGE_TAG --target prod

      # ...rest of the steps

Since the "prod" is the last stage, we don't need to specify the target for the build command. Running

docker build . -t $REGISTRY/$REPOSITORY:$IMAGE_TAG

will build the same image.

References


πŸ’¬ Comments on Reddit r/Nestjs_framework and r/javascript.


Here you have it! Thanks for reading through πŸ™Œ If you find this article useful, please share it to help more people in their engineering journey.

🐦 Feel free to connect with me on twitter!

Ready for the next article? πŸ‘‰ Optimize Google Cloud BigQuery and Control Cost

Happy coding!

Daw-Chih Liou
Daw-Chih Liou

Daw-Chih is a software engineer, UX advocate, and creator who is dedicated to Web engineering. His background in Human Centered Computing has led him to work with startups and public companies across North America, Asia, and Europe. He is passionate about meeting business trajectory with user journey and utilizing engineering architecture and performance monitoring to provide optimal user experience.