DEV Community

Cover image for Setting up Docker + TypeScript + Node (Hot reloading code changes in a running container) 🦄 🚀
Darian Sampare
Darian Sampare

Posted on

Setting up Docker + TypeScript + Node (Hot reloading code changes in a running container) 🦄 🚀

I love TypeScript, and I love Docker. Putting these two together, however, can be a giant pain in the ass.

Today I am going to walk you through a very basic multi-stage Docker setup with a TypeScript/Node project.

This setup addresses the biggest challenge I found when working with this tech stack... getting my TypeScript to compile to JavaScript in production, and being able to develop in a running container that watches for changes made in my TypeScript Code.

All code for this tutorial can be found here :)

GitHub logo justDare / TypeScript-Node-Docker

TypeScript + Node + Docker setup for dev and prod with hot reloading

Prefer YouTube? Check out the video tutorial here:

Step 1: Creating a server with TypeScript & Express

Let's whip up a simple Express server with TypeScript and get it running locally (we'll dockerize it after!).

Make a directory for the project and cd in there:

mkdir ts-node-docker
cd ts-node-docker
Enter fullscreen mode Exit fullscreen mode

Initialize a node project and add whatever values you want when prompted (I just skip everything by mashing enter...):

npm init
Enter fullscreen mode Exit fullscreen mode

Next, install TypeScript as a dev dependancy:

npm i typescript --save-dev
Enter fullscreen mode Exit fullscreen mode

Once that's downloaded, create a tsconfig.json file:

npx tsc --init
Enter fullscreen mode Exit fullscreen mode

Now we should have a tsconfig.json in the root of out project directory, lets edit the following entries in there:

"baseUrl": "./src"
"target": "esnext"
"moduleResolution": "node"
"outdir": "./build"
Enter fullscreen mode Exit fullscreen mode

The baseUrl tells TS that our .ts source code files will be in the ./src folder.

The target can be whatever version of JS you like, I go with esnext.

The moduleResolution must be set to node for node projects.

The outdir tells TS where to put the compiled JavaScript code when the TS files are compiled.

Next, let's install express, and then its typings as a dev dependancy:

npm i --save express
npm i -D @types/express
Enter fullscreen mode Exit fullscreen mode

Cool, we are ready to code up our server. Let's make a src/ folder at the root of our project and add an index.ts file.

In index.ts, add the following code:

import express from 'express';

const app = express();
app.listen(4000, () => {
  console.log(`server running on port 4000`);
});
Enter fullscreen mode Exit fullscreen mode

That's all we'll need to start our server, but now we need to get this thing running and watching for changes we make to the code.

For that, we'll use ts-node and nodemon, intall that now:

npm i -D ts-node nodemon
Enter fullscreen mode Exit fullscreen mode

With nodemon, we can watch files while the code is running, and ts-node just lets us run node projects written in TS very easily.

I like to have my nodemon setup in a config file, so I'll add a nodemon.json file to the root of my project folder and add the following options:

{
  "verbose": true,
  "ignore": [],
  "watch": ["src/**/*.ts"],
  "execMap": {
    "ts": "node --inspect=0.0.0.0:9229 --nolazy -r ts-node/register"
  }
}
Enter fullscreen mode Exit fullscreen mode

The key takeaways here are the watch command (which tells nodemon what files it should watch for), and the ts option in execMap.

This tells nodemon how to handle TS files. We run them with node, throw in some debugging flags, and register ts-node.

Okay, now we can add scripts to our package.json that uses nodemon to start our project. Go ahead and add the following to your package.json:

"scripts": {
    "start": "NODE_PATH=./build node build/index.js",
    "build": "tsc -p .",
    "dev": "nodemon src/index.ts",
}
Enter fullscreen mode Exit fullscreen mode

The dev command starts our project with nodemon. The build command compiles our code into JavaScript, and the start command runs our built project.

We specify the NODE_PATH to tell our built application where the root of our project is.

You should now be able to run the application with hot reloading like so:

npm run dev 
Enter fullscreen mode Exit fullscreen mode

Great! Now let's dockerize this thing 🐳

Step 2: Docker Development & Production Step

If you haven't installed Docker, do that now. I also recommend their desktop app, both of which can be found on their website.

Next, let's add a Dockerfile to the root of our project directory and add the following code for the development step:

FROM node:14 as base

WORKDIR /home/node/app

COPY package*.json ./

RUN npm i

COPY . .
Enter fullscreen mode Exit fullscreen mode

This pulls in a node image, sets a working directory for our container, copies our package.json and installs it, and then copies all of our project code into the container.

Now, in the same file, add the production step:

FROM base as production

ENV NODE_PATH=./build

RUN npm run build
Enter fullscreen mode Exit fullscreen mode

This extends our development step, sets our environment variable, and builds the TS code ready to run in production.

Notice we haven't added any commands to run the development or production build, that's what our docker-compose files will be for!

Create a docker-compose.yml file at the root of our directory and add the following:

version: '3.7'

services:
  ts-node-docker:
    build:
      context: .
      dockerfile: Dockerfile
      target: base
    volumes:
      - ./src:/home/node/app/src
      - ./nodemon.json:/home/node/app/nodemon.json
    container_name: ts-node-docker
    expose:
      - '4000'
    ports:
      - '4000:4000'
    command: npm run dev
Enter fullscreen mode Exit fullscreen mode

This creates a container called ts-node-docker, uses our dockerfile we created, and runs the build step (see the target).

It also creates volumes for our source code and nodemon config, you'll need this to enable hot-reloading!

Finally, it maps a port on our machine to the docker container (this has to be the same port we setup with express).

Once that's done, we can build our docker image:

docker-compose build
Enter fullscreen mode Exit fullscreen mode

You should be able to see the build steps in your terminal.

Next, we can run the container as follows:

docker-compose up -d
Enter fullscreen mode Exit fullscreen mode

Success! You should now have a container running that picks up on any changes you make to your TypeScript source code. I highly recommend using the docker desktop app to view the containers you have running.

Alt Text

You can stop the container like so:

docker-compose down
Enter fullscreen mode Exit fullscreen mode

Now we are also going to want to run this thing in production, so let's create a separate docker-compose.prod.yml for that:

version: '3.7'

services:
  ts-node-docker:
    build:
      target: production
    command: node build/index.js
Enter fullscreen mode Exit fullscreen mode

This file is going to work together with our first docker-compose file, but it will overwrite the commands we want to change in production.

So, in this case, we are just going to target the production step of our Dockerfile instead, and run node build/index.js instead of npm run dev so we can start our compiled project.

To start our container in production, run:

docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d   
Enter fullscreen mode Exit fullscreen mode

This tells docker-compose which files to use, the later files will overwrite any steps in the prior files.

You should now have the built application running just how it would be in production, no hot reloading needed here!

Lastly, I hate typing out all these docker commands, so I'll create a Makefile in the root of my project and add the following commands that can be executed from the command line (eg make up):

up:
    docker-compose up -d

up-prod:
    docker-compose -f docker-compose.yml -f docker-compose.prod.yml up

down: 
    docker-compose down
Enter fullscreen mode Exit fullscreen mode

If you made it all the way to the end, congrats, and thank you. Hopefully, this made somebody's day a lot easier while trying to integrate these two awesome technologies together.

If you liked this, I post tutorials and tech-related videos over on my YouTube channel as well.

We have a growing tech-related Discord Channel too, so feel free to pop by.

Happy coding! 👨‍💻 🎉

Top comments (15)

Collapse
 
tomcrawford profile image
Tom Crawford

In order to make the production container, I had to add --build to the docker up command, so the up-prod Makefile entry became:

docker-compose -f docker-compose.yml -f docker-compose.prod.yml up --build -d

Collapse
 
ryands17 profile image
Ryan Dsouza • Edited

Great article! We would need to restart the container on installing a new module right?

Collapse
 
dariansampare profile image
Darian Sampare • Edited

Thanks Ryan! Yes you would. Most of it gets cached so rebuilding after installing another package should be much faster than the initial build.

Collapse
 
aadit017 profile image
Aadit

💯

Collapse
 
babatunde50 profile image
Babatunde Ololade

Great article, thanks

Collapse
 
dancrtis profile image
Dan Curtis

Thank you! I was wasting so much time flipping between developing with npm scripts and then testing with docker...hot reloading for the win!

Collapse
 
jamiegilmartin profile image
jamie gilmartin

This was very well done. Thanks!

Collapse
 
aadityasiva profile image
Aadityasiva

Nice

Collapse
 
spock123 profile image
Lars Rye Jeppesen

Doesn't this make a production image containing all the devDependencies? That's not what we want

Collapse
 
zenb0t profile image
Zenb0t

Saved me from a massive headache. Thanks a lot!

Collapse
 
santdev404 profile image
santdev404

Excellent article!!