API with NestJS #92. Increasing the developer experience with Docker Compose

Docker NestJS

This entry is part 92 of 121 in the API with NestJS

The previous article taught us how to use Docker and Docker Compose with NestJS. In this article, we expand our knowledge by applying various tricks and tips on increasing the development experience with Docker and Docker Compose.

Check out this repository if you want to see the full code for this article.

Building the Docker Image automatically

In the previous part of this series, we built our basic .

Dockerfile

Then, we built the Docker image by running an appropriate command and giving it a tag.

Then, we added the above tag to our Docker Compose configuration.

docker-compose.yml

Once we have the above file, we can run to run our application.

Automating the process of building the image

Unfortunately, the above process requires the developers to run two commands instead of one. Also, we must remember to run every time there is a change in our code.

Instead, we can point Docker Compose to our and expect it to build the required Docker image.

docker-compose.yml

Thanks to adding the section to our configuration and pointing to the directory with the , we can expect Docker Compose to build the necessary image.

There is one caveat, though. To ensure that Docker Compose always rebuilds the image even if an old version is available, we need to add the flag.

Dealing with cache

By adding the flag, we expect Docker Compose to rebuild our image every time we run . Let’s look at how Docker handles cache to avoid waiting too much time for the build to finish.

Each instruction in our roughly translates to a layer in our image. Therefore, whenever a layer changes, it must be rebuilt together with all the following layers.

Let’s say we made a slight change in our file. Unfortunately, it affects the command since we use it to copy all of our files.

  • FROM node:18-alpine
  • WORKDIR /user/src/app
  • COPY . .
  • RUN npm ci –omit=dev
  • RUN npm run build
  • USER node
  • CMD [“npm”, “run”, “start:prod”]

Due to how we structured our , making changes to our source code causes Docker to reinitialize our whole directory with the command. Let’s improve that by changing how we use the instruction.

Dockerfile

Above, we first copy only the and files. Then, we install all of the dependencies. We can see it as a milestone that Docker reaches and stores in the cache. Now, Docker knows that modifying the file does not affect the command and does not reinstall the packages unnecessarily.

  • FROM node:18-alpine
  • WORKDIR /user/src/app
  • COPY package.json package-lock.json ./
  • RUN npm ci –omit=dev
  • COPY . .
  • RUN npm run build
  • USER node
  • CMD [“npm”, “run”, “start:prod”]

The above approach can drastically decrease the time required for the Docker image to be built.

Restarting the application on changes

Applying changes we made to our source code now takes a bit of work. First, we need to stop all of our Docker containers and then rerun them. It causes the Docker image with the API to be rebuilt.

Instead, when running our application in development, we can do the following:

  • install the necessary dependencies,
  • run the command

When using the above approach, NestJS watches for any changes made to the source code and restarts automatically.

Implementing a multi-stage Docker build

The issue is that when we build our Docker image using the , it always creates a production build and ends with .

The first step to changing the above is to implement a multi-stage build. Thanks to this approach, we don’t need separate for development and production. Instead, we divide our into stages.

Each stage begins with a statement. We can copy files between stages, leaving behind any files we don’t need anymore. Thanks to that, we can achieve a smaller Docker image.

Dockerfile

Each of our stages above use the image as base, but that does not have to be the case.

Please notice above that our final Docker image contains only , , and . Thanks to that, we’ve managed to shave off some unnecessary data by copying only the files necessary to run the application.

Modifying the Docker Compose configuration

Thanks to dividing our into stages, we can tell Docker Compose to target a specific stage.

docker-compose.yml

Above, we explicitly tell Docker only to run the stage from our . This means that Docker won’t create a production build.

Since our stage does not contain the instruction, we need some way to tell Docker what to do. We do that by adding to our Docker Compose configuration.

So far, we’ve been using the property to allow our PostgreSQL Docker container to persist the data outside of the container. Thanks to doing that, when we run our PostgreSQL container after it’s been shut down, we don’t end up with an empty database. We can use the same approach to the Docker container with our NestJS application.

Thanks to adding to our , Docker synchronizes the  directory in the Docker container with the directory on our host machine. Thanks to that, whenever we change our source code, the process is aware of it and restarts our NestJS application.

Running the debugger

A very big part of the developer experience is to be able to use a debugger. Fortunately, we can connect the debugger to a Node.js application running in a container.

First, let’s add a new script into our file.

package.json

A few important things are happening above. First, we add the to establish a WebSocket connection that our debugger can connect to. Our debugger also might require the flag, but the NestJS CLI does not support it out of the box anymore. Because of that, we need to use the hack with the flag.

We also need to allow our host machine to establish a connection with our Docker container on port . To do that, we need to slightly alter the section in our Docker Compose configuration.

docker-compose.yml

Please notice that we are running the command above.

Debugging through WebStorm

To debug our application running in a Docker container using WebStorm, we first need to run in the terminal to run all of our containers. Remember that because we’ve used the flag, our NestJS application will not run until we connect the debugger.

Then, we need to go to “Run -> Edit Configurations” and create the “Attach to Node.js/Chrome” configuration by clicking on the plus icon.

Once we do that, we need to check the “Reconnect automatically” checkbox so that the debugger reconnects when our application restarts after changes.

As soon as we choose “Run -> Debug ‘Attach to container’, WebStorm connects the debugger through a WebSocket to our Docker container.

You can also debug using Visual Studio Code in a similar way. Check out the official Visual Studio Code documentation for a step-by-step explanation.

Summary

In this article, we implemented a few significant developer experience improvements that make our work with NestJS and Docker easier. We’ve automated building the Docker image by changing our Docker Compose configuration. When doing the above, we’ve also improved how we handle cache by making subtle changes to our . We also learned how to restart our application on changes and use the debugger with the NestJS app running in the container. All of the above can definitely increase the developer experience and make our job easier.

Series Navigation<< API with NestJS #91. Dockerizing a NestJS API with Docker ComposeAPI with NestJS #93. Deploying a NestJS app with Amazon ECS and RDS >>
Subscribe
Notify of
guest
3 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Pedro
Pedro
1 year ago

Thanks for the article, in my projects I have a similar approach but I haven’t used the debugger, I will check this out.

Philip
Philip
1 year ago

Great article! For debugging in VS Code, I use a launch configuration similar to this in my .code-workspace file:

Last edited 1 year ago by Philip
Chris
Chris
1 year ago

When I update docker-compose.yaml nest-api bl;ock with
networks:
– postgres
Nest cannot connect to DB but without nest starts ok