DEV Community

Abbas Ogaji
Abbas Ogaji

Posted on • Edited on

How to Dockerize your NestJS App for production

As we all know Docker is not just another buzzword, but one of the best containerization tool for software engineers. With the ability to ship any application regardless of its environmental requirements docker has solved problems in development and production stage like; inconsistencies when running your application with the phrase popularly known as "urghh..but it worked on my PC!"

This tutorial shows how to Dockerize a Nestjs app, built with the Nestjs CLI and Docker.

Requirements

  1. Docker installed
  2. A NestJs app

1. Docker Installation

a) For the installion guide on windows visit https://docs.docker.com/docker-for-windows/install/

b) For the installation guide on Linux visit
https://docs.docker.com/install/linux/docker-ce/ubuntu/

c) For the installation guide on macOs visit https://docs.docker.com/docker-for-mac/

2. NestJs Application

Create and develop your NestJs application or use the sample project provided below for the demo:
https://github.com/abbasogaji/dockerized-nestjs-production-app

Your nestJs application project structure usually would look like this:

Nestjs project structure

And its associated npm commands for running, testing and building your nestJs project are found in your package.json's scripts object (property);

package.json scripts

Some of these commands will be used when we dockerize our application.

NOW WE WRITE OUR Dockerfile

FROM node:10 AS builder
WORKDIR /app
COPY ./package.json ./
RUN npm install
COPY . .
RUN npm run build


FROM node:10-alpine
WORKDIR /app
COPY --from=builder /app ./
CMD ["npm", "run", "start:prod"]

Enter fullscreen mode Exit fullscreen mode

Breakdown

  • The build process is divided into two steps (multi-step build);

    • The First step uses node:10 as base image, installs dependencies and transpiles Typescript files to Javascript. Full node image is used for this process since it contains all the necessarity build tools required for dependencies with native build (node-gyp, python, gcc, g++, make)
    • The Second step uses node:10-alpine (lightweight version), copies file-system from the first step's (intermediate) container, sets command for running the application. A multi step build process was setup to efficiently install our dependencies at first step and to run a lightweight container from image the of our final step.
  • In First step;

    • We will set our application directory to "/app", so our application is bundled into "/app" in our docker image file-system
    • We will COPY our "package.json" then run "npm install" before we copy the remaining project files because that will prevent unnecessary installs anytime we re-build our image and make use of cached installs.
    • We will run "npm run build" to generate production files at "dist/main" directory which is required by our "run" command in production context i.e (npm run start:prod)
  • In Final step;

    • We will set our application directory to "/app", so our application is bundled into "/app" in our docker image file-system
    • We will copy the file-system of the previous step.
    • We will set command for running our application
  1. Create .dockerignore to avoid copying node_modules; so in your .dockerignore you type in "node_modules" and now you have your Dockerfile and .dockerignore file at your base directory of your project as shown below;

final project

  1. Now we build our image, assign a tag with the format "docker-username/project-name" then push to docker hub by running:
 docker build -t exampleuser/dockerized-nest-project .

 docker push exampleuser/dockerized-nest-project

Enter fullscreen mode Exit fullscreen mode

Question: Now we are done; but wait a minute? EXPOSE port command was omitted?.
Answer: because EXPOSE command is not respected by some Paas (Platform as service) providers e.g (Heroku).

but if you deploy your docker image to a cloud provider that requires EXPOSE command which maps your docker network ports to your host port. then you should add "EXPOSE 3000" IN Dockerfile before the last CMD command;

# Using Node:10 Image Since it contains all 
# the necessary build tools required for dependencies with native build (node-gyp, python, gcc, g++, make)
# First Stage : to install and build dependences

FROM node:10 AS builder
WORKDIR /app
COPY ./package.json ./
RUN npm install
COPY . .
RUN npm run build


# Second Stage : Setup command to run your app using lightweight node image
FROM node:10-alpine
WORKDIR /app
COPY --from=builder /app ./
EXPOSE 3000
CMD ["npm", "run", "start:prod"]

Enter fullscreen mode Exit fullscreen mode

Note : Using Multi-Step build process to dockerize our nestjs application isn't a necessity and was only used because we would like our image to be lightweight. for a single step build process we will write the following;

# Using Node:10 Image Since it contains all 
# the necessary build tools required for dependencies with native build (node-gyp, python, gcc, g++, make)

FROM node:10
WORKDIR /app
COPY ./package.json ./
RUN npm install
COPY . .
RUN npm run build
# EXPOSE 3000
CMD ["npm", "run", "start:prod"]

Enter fullscreen mode Exit fullscreen mode

Then finally we will have to rebuild, deply and run the image

Top comments (19)

Collapse
 
hehehai profile image
hehehai

I don't know why this is? , I followed the instructions on the tutorial, but I tried it, and it still didn't solve it!

Step 11/11 : CMD ["npm", "run", "start:prod"]
 ---> Running in e5f55126e787
Removing intermediate container e5f55126e787
 ---> 6a07a1a8b24a
Successfully built 6a07a1a8b24a
Successfully tagged bd-url-query_nest:latest
Creating bd-url-query ... done
Attaching to bd-url-query
bd-url-query |
bd-url-query | > bd-link@0.0.1 start:prod /app
bd-url-query | > node dist/main.js
bd-url-query |
bd-url-query | internal/modules/cjs/loader.js:1032
bd-url-query |   throw err;
bd-url-query |   ^
bd-url-query |
bd-url-query | Error: Cannot find module '/app/dist/main.js'
bd-url-query |     at Function.Module._resolveFilename (internal/modules/cjs/loader.js:1029:15)
bd-url-query |     at Function.Module._load (internal/modules/cjs/loader.js:898:27)
bd-url-query |     at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:71:12)
bd-url-query |     at internal/main/run_main_module.js:17:47 {
bd-url-query |   code: 'MODULE_NOT_FOUND',
bd-url-query |   requireStack: []
bd-url-query | }
bd-url-query | npm ERR! code ELIFECYCLE
bd-url-query | npm ERR! errno 1
bd-url-query | npm ERR! bd-link@0.0.1 start:prod: `node dist/main.js`
bd-url-query | npm ERR! Exit status 1
bd-url-query | npm ERR!
bd-url-query | npm ERR! Failed at the bd-link@0.0.1 start:prod script.
bd-url-query | npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
bd-url-query | npm WARN Local package.json exists, but node_modules missing, did you mean to install?
bd-url-query |
bd-url-query | npm ERR! A complete log of this run can be found in:
bd-url-query | npm ERR!     /root/.npm/_logs/2020-06-03T14_10_18_871Z-debug.log
bd-url-query exited with code 1
Enter fullscreen mode Exit fullscreen mode
Collapse
 
abbasogaji profile image
Abbas Ogaji • Edited

You might be missing some build dependencies using the alpine version, you can use this instead;

FROM node:10
WORKDIR /app
COPY ./package.json ./
RUN npm install
COPY . .
RUN npm run build
# EXPOSE 3000
CMD ["npm", "run", "start:prod"]

Collapse
 
hehehai profile image
hehehai

Sorry, I did not post the complete configuration. I tried it and found that it was a problem with WORKDIR. It was normal when I ran it for the first time. This problem occurred the second time docker-compose up. is normal. I am trying to find out why.

FROM node:latest

WORKDIR /app/bd-url-query

COPY package*.json .
COPY yarn.lock .

RUN yarn

COPY . .
RUN yarn prebuild && yarn build

CMD [ "node", "dist/main.js"]
Thread Thread
 
abbasogaji profile image
Abbas Ogaji • Edited

" Error: Cannot find module '/app/dist/main.js" it was looking main.js at "/app/dist/main.js" instead of "/app/bd-url-query/dist/main.js"

Just take a look at what you have in your directory, add the ls command i.e "RUN ls -l" before "CMD....." line in your Dockerfile and inspect the files you have there

Collapse
 
seand52 profile image
seand52

Nice post!

Just a small comment - I'm fairly certain that it's not necessary to do it in two steps by using first the full node image and then node-alpine. I got it working with just 1 step using node-alpine for everything:

FROM node:10-alpine
WORKDIR /app
COPY ${PWD}/package.json ./
RUN yarn
COPY . .
RUN yarn build
EXPOSE 5000
CMD ["sh", "-c", "yarn typeorm migration:run && yarn start:prod"]

Collapse
 
abbasogaji profile image
Abbas Ogaji • Edited

Very true, although they might be scenarios where your dependencies rely on native builds, and that might require you to install build tools like node-gpy (which is written in python [meaning also need to install python] ), make, gcc, g++ which might not exists on alpine version,

Although you can still get away with it by installing build tools via apk in Dockerfile

Collapse
 
goodluck11 profile image
goodluck11

This man...thanks for the article... this is Goodluck

Collapse
 
abbasogaji profile image
Abbas Ogaji

Mahnn how u doing๐Ÿ‘

Collapse
 
goodluck11 profile image
goodluck11

Very good man and you?

Collapse
 
bezerrarichard profile image
bezerraRichard

Thanks for the post! it was a lot of help.

Collapse
 
Sloan, the sloth mascot
Comment deleted
Collapse
 
abbasogaji profile image
Abbas Ogaji

thanks man

Collapse
 
balvinder294 profile image
Balvinder Singh

Well explained, nice article.

Collapse
 
leonardom profile image
leo

Thanks a lot for sharing!!! :)

Collapse
 
thavoo profile image
Gustavo Herrera

Good post. Thanks

Collapse
 
sonofab1rd profile image
Jacob Dierkens

Brilliant! Thank you!!

Collapse
 
hungleq profile image
hungleQ

Hi Buddy.
This post is really good. It fixed my issue.
Thanks mate.