Setting up a NestJS project with Docker for Back-End development
Tags: docker, node, javascript, nestjs
Published: 2021-01-31 10:51:52

NestJS has been my go-to Back-End framework for API development in NodeJS for quite a while now after discovering how annoying it can be to set up a NodeJS + Express application, not to mention that if you want TypeScript support, it requires quite a lot of setting up to do.

Also, I believe implementing Docker for development (and definitely for production) is a must have thing in all of my Back-End applications since it makes deployment extremely easy and predictable no matter which service it's going to be deployed to.

# Prerequisites

I'm not going to go too much in depth on NestJS or Docker since the main focus of this tutorial is to run a NestJS app environment with Docker and that's about it. I will be making a separate series about NestJS and Docker going more in depth along with best practices in the future.

In order to fully understand how the upcoming code works, you should have a basic understanding of the following subjects:
* Working with Terminal
* JavaScript/TypeScript
* How Docker works

### Let's begin!

# Creating a new NestJS application

Start by installing the NestJS CLI using `npm` on your machine and create a new project:
```bash
$ npm i -g @nestjs/cli
$ nest new nestjs-docker
```
When creating a new project, NestJS is going to ask you
> Which package manager would you ❤️  to use?

I am going to choose `npm` for this tutorial, but you can choose `yarn` if you really want to.

After the installation is finished, `cd` into your new application directory and run the app like so:
```bash
$ cd nestjs-docker
$ npm run start:dev
```
Then, open your newly created app in the browser using the address http://localhost:3000 in order to make sure everything runs smoothly.

# Creating a new API service

Let's create a new API service that returns a hardcoded array of movies that we can use to test our API after containerizing it with Docker.

Open the newly created project directory with your favorite text/code editor (I'm using [Visual Studio Code](https://code.visualstudio.com)).
Your workspace should look like something like this:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/z3cunz1tvk0g4ed96go6.png)

Then, open the `src` directory. Here the source files of the application can be found in order to start our development

Open the file `app.service.ts` and replace the whole content of the file with the following code:
```typescript
import { Injectable } from '@nestjs/common';

export interface Movie {
  id: number;
  name: string;
  year: number;
}

@Injectable()
export class AppService {
  private movies: Movie[] = [
    { id: 1, name: 'Star Wars: The Force Awakens', year: 2015 },
    { id: 2, name: 'Star Wars: The Last Jedi', year: 2017 },
    { id: 3, name: 'Star Wars: The Rise of Skywalker', year: 2019 },
  ];

  getMovies(): Movie[] {
    return this.movies;
  }
}
```
**NOTE:** Adding an exportable Model (e.g. `Movie`) to the service class file is definitely not a good practice and you shouldn't do this under any circumstance. It's for demo purposes only. Use a different model file for your apps.

Next, open the file `app.controller.ts` and replace the whole content of the file with the following code:
```typescript
import { Controller, Get } from '@nestjs/common';
import { AppService, Movie } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getMovies(): Movie[] {
    return this.appService.getMovies();
  }
}
```

Run the project again using `npm run start:dev` and open the app in the browser at http://localhost:3000 or you can use [Postman](https://www.postman.com) and create a new `GET` request for a more formatted an semantic workflow.
The final result should look like this:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/sqog09d2wrg3mbqmr99u.png)

# Let's Dockerize this!

Now that we have our Back-End API app up and running, let's containerize it using Docker for development.

Start by creating the following files in the project's root  directory:
* `Dockerfile` - This file will be responsible for importing the Docker images, divide them into `development` and ` production` environments, copying all of our files and install `npm` dependencies
* `docker-compose.yml` - This file will be responsible for defining our containers, required images for the app other services, storage volumes, environment variables, etc...

Open the `Dockerfile` and add the following code inside:
```docker
FROM node:12.19.0-alpine3.9 AS development

WORKDIR /usr/src/app

COPY package*.json ./

RUN npm install glob rimraf

RUN npm install --only=development

COPY . .

RUN npm run build

FROM node:12.19.0-alpine3.9 as production

ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}

WORKDIR /usr/src/app

COPY package*.json ./

RUN npm install --only=production

COPY . .

COPY --from=development /usr/src/app/dist ./dist

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

Open the `docker-compose.yml` file and add the following code:
```yaml
version: '3.8'

services:
    dev:
        container_name: nestjs_api_dev
        image: nestjs-api-dev:1.0.0
        build:
            context: .
            target: development
            dockerfile: ./Dockerfile
        command: npm run start:debug
        ports:
            - 3000:3000
            - 9229:9229
        networks:
            - nesjs-network
        volumes:
            - .:/usr/src/app
            - /usr/src/app/node_modules
        restart: unless-stopped
    prod:
        container_name: nestjs_api_prod
        image: nestjs-api-prod:1.0.0
        build:
            context: .
            target: production
            dockerfile: ./Dockerfile
        command: npm run start:prod
        ports:
            - 3000:3000
            - 9229:9229
        networks:
            - nesjs-network
        volumes:
            - .:/usr/src/app
            - /usr/src/app/node_modules
        restart: unless-stopped

networks:
    nesjs-network:
```

# Running the Docker containers

Now that we have defined our Docker files, we can run our app solely on Docker.

To start our app, write the following command in your terminal:
```bash
docker-compose up dev
```
This will start it in development mode. We even get a file watcher when saving our files so we don't have to re-run it every time we make change 😍.

And to start our app in production mode, you guessed it... run the following command in your terminal:
```bash
docker-compose up prod
```

Make a `GET` request yet again to http://localhost:3000 and... voilà! Should work as expected.

**P.S:** If you want to ditch the terminal logging, you can run the container in a separate daemon using the `-d` flag like so:
```bash
docker-compose up -d prod
```

# Done!

You can find the full source code right [here](https://github.com/erezhod/nestjs-docker-tutorial).

Feel free to let me know what you think in the comments and ask away any questions you might have ✌️