A Long journey involved with Dockerizing a NestJS app and debug efficiently.
This will be a "step-by-step" and a little explaining so you don't just copy-paste ๐.
Installation ๐จ
NestJS - a NodeJS framework for server API mainly aims to be developed with TypeScript (but there's also a JS version).
Installing NestJS convenience Command Line Interface to bootstrap the project.
npm install -g @nestjs/cli // OR yarn global add @nestjs/cli
Docker and Docker-Compose - Dockerizing/Packaging into a standardized unit for development, like Virtual Machines but much lighter and efficient, you can read more.
Docker is really awesome, I raise the entire deployment of multi micro-services architecture in a couple of seconds using
docker-compose
a wrapper CLI arounddocker-engine
.IDE - I will be using Visual Studio Code
I will be using Visual Studio Code reference mostly in the Debugging part.
Project set up โ
Now with all of these awesome tools in our hands let's set up everything ๐ช!
- Starting with NestJS -
Pretty self-explanatory and just to get familiar with the tools this CLI grants us.nest --help
So we can create a new app -
nest new <the name of your app>
Nest will bootstrap the project you will be prompted to select your favorite package management CLI.
Your project folder will contain the following files/folders
Let's move to
Dockerfile
and.dockerignore
files -touch Dockerfile .dockerignore
Dockerfile
- we describe a set of instructions that eventually will build our Docker Image..dockerignore
- we describe a set of file/folder paths to be ignored and wouldn't copy into the build context of the Dockerfile.Writing our
Dockerfile
and.dockerignore
-# Dockerfile FROM node:lts WORKDIR /app COPY package*.json ./ RUN npm install COPY . . CMD ["npm","run","start"]
Breaking things up -
FROM node:lts
-FROM
tells our build context which image are we basing/extending on in our build context,node:lts
i choose an official image of NodeJS so basically it isIMAGE:TAG
whereTAG
stands for version typically aligned with SemVer (Semantic Versioning).So
node:lts
is NodeJS precompiled Docker image with Npm & Yarn CLI installed (You can view their Dockerfile here ).IF I was to create a Java project I would have probably use an OpenJDK image to compile my Java project.
WORKDIR
- short term for the working directory, we define in which directory are we going to work on INSIDE THE BUILD CONTEXT AND THE FUTURE CREATED IMAGE.Browsing the Internet you will see diverse answers around where the
WORKDIR
should be, eventually, it is your preference!I use
/app
most of the time.NOTE - it is best practice to not move through directories outside of the WORKDIR in the further instructions given to the Dockerfile (build context...)
COPY package*.json ./
-COPY
would copy files from side to side, the left side is the host computer (where you ran docker build if haven't run it from a different folder using the -f flag), so I tell the build contextCOPY
mypackage.json
andpackage-lock.json
to./
so if you are a bit familiar with "FileSystems".
stands for the current directory and I add the/
just to be sure that I copy multiple files into the current folder.RUN npm install
-RUN
executes the command as you do in your Console (be aware we are always in theWORKDIR
we specified).So I just ran
npm install
to install all dependencies/devDependencies inside the build context.COPY . .
- after installed everything successfully (hopefully) we copy the entire project directory into our working directory.I usually copy just the package*.json and install before copying everything so if something fails with
npm install
it will break the build before I try to copy everything else.CMD ["npm","run","start"]
- the command that will be triggered when we raise the image to a running container.So I will be using a script as specified in the
package.json
.Note for docker images of CLI you better use
ENTRYPOINT
you can read more here.
That's it for now with the Dockerfile.
# .dockerignore
dist
node_modules
*.log
I Specify which file/folders I wouldn't like to be copied into my image.
node_modules
- I would like the node_modules to be installed from the image as all of the modules which require binaries of Linux like ( gcc ) will be compiled using the binaries that are pre-compiled into the node:lts
image.
dist
- Later when we modify the Dockerfile for build/production purposes I wouldn't like that in any case my host dist
folder will be copied and affect my build output.
*.log
- I just don't like log files :).
Building the image and running it ๐ณ
Build -
docker build -t <your image name>:1.0.0 .
We initiate a docker command to build an image with a specified tag (-t) at the
.
current directory (where our project is and the docker files).This can take a couple of seconds/minutes depending on your project dependencies and Ethernet speed.
Running the ready image -
docker run <you image name>:1.0.0
Your Console STDIN will be directed into the newly created container of the image you built.
You can
CTRL+C
to get out but it will shut your container down.To keep the container running you can run it detached using the
-d
flag when using thedocker run
.If running in detached you can initiate commands on the running container using the
docker exec
command.If running in detached in order to stop the container you can use
docker container stop < name of the *container* >/< hash of the *container* >
Docker-Compose orchestrate Docker infrastructure ๐ณ
A bit of overkill for a single service but for 2 and above this is just fantastic.
touch docker-compose.yml
We create a docker-compose.yml
file (default file name by the CLI of docker-compose)
โ docker-compose.yml is a bit long to explain so i documented on the code itself and you can read more here
version: "3.8" # Specify the version of docker-compose we will use
services: # Specify our services
backend: # define our first service
build: # define the build context
dockerfile: ./Dockerfile # the docker file to use by default it looks for Dockerfile in the context dir so we can omit this key :)
context: . # where is the build context folder
image: custom_image_name:1.0.0 # in case we are building the built image name will be from the image entry
environment:
NODE_ENV: development
PORT: 3000
ports:
- 3000:3000 # <host port> : <container port>
- 9229:9229 # 9229 is the default node debug port
volumes:
- "/app/node_modules" # save the compiled node_modules to anonymous volume so make sure we don't attach the volume to our host node_modules
- "./:/app" # link our project directory to the docker directory so any change will get updated in the running container and also we will benefit from sourcemaps for debugging
So now when we have in our hands the docker-compose file ready just:
docker-compose up -d
# '-d' - for detached
The Docker-compose on the first time will build the image and raise it entirely as specified in the docker-compose.yml
Why compose? for our first service without using docker-compose inorder to raise it as specified we had to build it and then run it as so:
docker run -d -p '3000:3000' -p '9229:9229' -e 'NODE_ENV=development' -e 'PORT=3000' -v '${PWD}:/app' -v '/app/node_modules'
and thats for one service imagine your self raising microservice architecture of 8+ services.... INSANITY ๐ต.
Debugging ๐
package.json
// change start:debug script
"start:debug": "nest start --debug 0.0.0.0:9229 --watch",
By using 0.0.0.0 address inside the docker we allow access from the external network (our host) into the debugger in the container.
Docker-compose.yml we must override the default command in order to start with the debug command:
version: "3.8"
services:
backend:
build:
dockerfile: ./Dockerfile
context: .
image: custom_image_name:1.0.0
environment:
NODE_ENV: development
PORT: 3000
ports:
- 3000:3000
- 9229:9229
volumes:
- "/app/node_modules"
- "./:/app"
command: npm run start:debug # override entry command
VSCode:
It will create a launch.json
file where you specify the debug configurations.
And adjust it as so:
Breaking things up:
type
- the type of debugger in our case is node
debugger.
request
- we use attach
to attach into running debug instance, launch
is to raise a debug instance and attach to it.
name
- a name given to the debug configuration.
protocol
- the NodeJS debugger protocol to use (you can read more here ).
address
- the remote address to look out for the debugging instance by default when we raise with docker-compose it uses a network driver that uses the host namespace ( you can read more about host network driver here )
port
- the debugging port to attach to (we use the NodeJS default port 9229)
sourceMaps
- our project is bootstrapped with NestJs by default it ships with typescript so when typescript compiles it by default ships with source maps so when we place breakpoints in our typescript it translates the breakpoint location to the compiled version (JavaScript).
restart
- we set true
so the debugger will try to reattach upon disconnection
localRoot
- where are the local files in the host machine.
remoteRoot
- where are the files in the remote (the running container).
Most of the disconnections are caused by Hot Reloading the code due to code change.
If you bootstrap a project of your own(and not from NestJS) make sure source maps is enabled in
tsconfig.json
in order to enable debugging.
And you are welcome
If you enjoyed this blog post subscribe and enjoy awesome Js/Ts/Development Content that I will release on weekly basis.