DEV Community

Cover image for How to build a NestJS Docker image for production
Tom Ray
Tom Ray

Posted on • Originally published at tomray.dev

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.

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
Enter fullscreen mode Exit fullscreen mode

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

# 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" ]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then exclude the following files from the image build:

Dockerfile
.dockerignore
node_modules
npm-debug.log
Enter fullscreen mode Exit fullscreen mode

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 .
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

You can now access the NestJS app by visiting https://localhost in your browser (just https://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
Enter fullscreen mode Exit fullscreen mode

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!

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:

# 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

# 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" ]

Enter fullscreen mode Exit fullscreen mode

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

docker build -t nest-cloud-run .
Enter fullscreen mode Exit fullscreen mode

And then the command to spin up your container:

docker run -p80:3000 nest-cloud-run
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

# 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

# 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" ]

Enter fullscreen mode Exit fullscreen mode

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

Top comments (2)

Collapse
 
askya profile image
ASKYA

Using RUN npm ci --only=production && npm cache clean --force instead of your RUN npm ci --only=production saves about 20MB in final image size. And the application runs fine.

Collapse
 
andrewbaisden profile image
Andrew Baisden

Great guide.