Tom RayTom Ray
Published on
Last updated:

How to build a NestJS Docker image for production

How to build a NestJS Docker image for production

When deploying my NestJS project, I found there wasn't loads online on how to write the Dockerfile to build the Docker image needed for containerized deployment.

So I wrote this guide which takes you through step-by-step how to setup a Docker image for your NestJs project!

Ready? Let's dive in.

P.S. if you want to just copy and paste the production ready Dockerfile, just skip to this section.

Table of Contents

Writing the Dockerfile

A container image is an isolated package of software that includes everything you need to run the code. You can define container images by writing a Dockerfile which provides the instructions on how to build the image.

Let's add the Dockerfile now:

touch Dockerfile

And then let's add the instructions to the Dockerfile. See the comments which explain each step:

Dockerfile
# Base image
FROM node:18

# Create app directory
WORKDIR /usr/src/app

# A wildcard is used to ensure both package.json AND package-lock.json are copied
COPY package*.json ./

# Install app dependencies
RUN npm install

# Bundle app source
COPY . .

# Creates a "dist" folder with the production build
RUN npm run build

# Start the server using the production build
CMD [ "node", "dist/main.js" ]

Similar to a .gitignore file, we can add a .dockerignore file which will prevent certain files from being included in the image build.

touch .dockerignore

Then exclude the following files from the image build:

.dockerignore
Dockerfile
.dockerignore
node_modules
npm-debug.log

Test the container locally

Let's now do some testing locally to check if the Dockerfile behaves as we expect.

Let's first build the image using the command in your terminal at the root of your project (you can swap out nest-cloud-run with your project name). Don't forget the .!

docker build -t nest-cloud-run .

You can verify the image has been created by running docker images which will output a list of Docker images you have on your local machine:

docker images
REPOSITORY                   TAG       IMAGE ID       CREATED          SIZE
nest-cloud-run               latest    004f7f222139   31 seconds ago   1.24GB

Let's now start the container and run the image with this command (be sure to same image name used above):

docker run -p80:3000 nest-cloud-run

You can now access the NestJS app by visiting http://localhost in your browser (just http://localhost without any port numbers).

I ran into a couple of issues on my machine when running the container, mainly due to with conflicts with ports from other containers I had running.

If you run into similar trouble, you can try running the command docker rm -f $(docker ps -aq) which stops and removes all running containers.

Optimize Dockerfile for production

Now that we've confirmed the image is working locally, let's try to reduce the size of the image.

Deployment tools like Cloud Run factor in the size of the image when calculating how much to charge you, so it's a good idea to keep the image size as small as possible.

Running the command docker images gives us our image size:

docker images
REPOSITORY                   TAG       IMAGE ID       CREATED          SIZE
nest-cloud-run               latest    004f7f222139   31 seconds ago   1.24GB

1.24GB is pretty big! Let's dive back into our Dockerfile and make some optimizations.

Use Alpine node images

It's recommended to use the Alpine node images when trying to optimize for image size. Using node:18-alpine instead of node:18 by itself reduces the image size from 1.24GB to 466MB!

Add a NODE_ENV environment variable

Many libraries have optimizations built in when the NODE_ENV environment variable is set to producton, so we can set this environment variable in the Dockerfile build by adding the following line to our Dockerfile (see next section for where to place this in the Dockerfile):

ENV NODE_ENV production

Use multistage builds

In your Dockerfile you can define multistage builds which is a way to sequentially build the most optimized image by building multiple images.

In practice, here's how we can use multistage builds in our Dockerfile:

Dockerfile
# Base image
FROM node:18-alpine As development

# Create app directory
WORKDIR /usr/src/app

# Copy application dependency manifests to the container image.
# A wildcard is used to ensure copying both package.json AND package-lock.json (when available).
# Copying this first prevents re-running npm install on every code change.
COPY package*.json ./

# Install app dependencies
RUN npm install

# Bundle app source
COPY . .

# Creates a "dist" folder with the production build
RUN npm run build

# Base image for production
FROM node:18-alpine As production

# Set NODE_ENV environment variable
ENV NODE_ENV production

# Create app directory
WORKDIR /usr/src/app

# Copy application dependency manifests to the container image.
# A wildcard is used to ensure copying both package.json AND package-lock.json (when available).
# Copying this first prevents re-running npm install on every code change.
COPY package*.json ./

# Install production dependencies.
# If you have a package-lock.json, speedier builds with 'npm ci', otherwise use 'npm install --only=production'
RUN npm ci --only=production

# Bundle app source
COPY . .

# Copy the bundled code
COPY --from=development /usr/src/app/dist ./dist

# Start the server using the production build
CMD [ "node", "dist/main.js" ]

Once you've updated your Dockerfile, you'll need to re-run the commands to build your image:

docker build -t nest-cloud-run .

And then the command to spin up your container:

docker run -p80:3000 nest-cloud-run

If you run docker images again to check our image size, you'll see it's now signifantly smaller:

docker images
REPOSITORY                   TAG       IMAGE ID       CREATED          SIZE
nest-cloud-run               latest    004f7f222139   31 seconds ago   189MB

Troubleshooting

You might come into the following errors:

Error: Cannot find module 'webpack'

It's likely you're using the wrong node version in your base image if you're getting errors like the following:

  • Error: Cannot find module 'webpack'
  • ERROR [development 6/6] RUN npm run build
  • npm ERR! nest-cloud-run@0.0.1 build: nest build

Instead of using FROM node:14-alpine, use FROM node:18-alpine to solve this issue.

Conclusion

In summary, here is our production optimized Docker image for a NestJS project:

Dockerfile
# Base image
FROM node:18-alpine As development

# Create app directory
WORKDIR /usr/src/app

# Copy application dependency manifests to the container image.
# A wildcard is used to ensure copying both package.json AND package-lock.json (when available).
# Copying this first prevents re-running npm install on every code change.
COPY package*.json ./

# Install app dependencies
RUN npm install

# Bundle app source
COPY . .

# Creates a "dist" folder with the production build
RUN npm run build

# Base image for production
FROM node:18-alpine As production

# Set NODE_ENV environment variable
ENV NODE_ENV production

# Create app directory
WORKDIR /usr/src/app

# Copy application dependency manifests to the container image.
# A wildcard is used to ensure copying both package.json AND package-lock.json (when available).
# Copying this first prevents re-running npm install on every code change.
COPY package*.json ./

# Install production dependencies.
# If you have a package-lock.json, speedier builds with 'npm ci', otherwise use 'npm install --only=production'
RUN npm ci --only=production

# Bundle app source
COPY . .

# Copy the bundled code
COPY --from=development /usr/src/app/dist ./dist

# Start the server using the production build
CMD [ "node", "dist/main.js" ]

Do you have any further optimizations you can make to the above image? Drop them in the comments below!