loading...

How to Dockerize your NestJS App for production

abbasogaji profile image Abbas Ogaji Updated on ・4 min read

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

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

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

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

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

Posted on by:

abbasogaji profile

Abbas Ogaji

@abbasogaji

Software Engineer | Side project @kimixbond | Contributor @voicemed.io | B.Eng Computer Engineering from Federal University of Technology Minna.

Discussion

markdown guide
 

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
 

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

 

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

" 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

 

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

 

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

 

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

 
 
 
 

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

 
 

Thanks a lot for sharing!!! :)