Setting up Docker + TypeScript + Node (Hot reloading code changes in a running container) 🦄 🚀
Tags: typescript, docker, node, javascript
Published: 2021-01-31 02:11:56

>*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 justDare/TypeScript-Node-Docker no-readme %}

Prefer YouTube? Check out the video tutorial here:
{% youtube 3o_DEJ9tB5A %}


# **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
```

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

Next, install TypeScript as a dev dependancy:
```
npm i typescript --save-dev
```

Once that's downloaded, create a tsconfig.json file:
```
npx tsc --init
```

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

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
```

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`);
});
```

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
```

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"
  }
}
```

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",
}
```

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 
```

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 . .
```

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
```

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
```

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
```

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

Next, we can run the container as follows:
```
docker-compose up -d
```

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](https://dev-to-uploads.s3.amazonaws.com/i/alnfxwyb0g6cy77zu6ut.png)

You can stop the container like so: 
```
docker-compose down
```

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
```

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   
```

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
```

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](https://www.youtube.com/channel/UC8YPqDf6j9YA7ckEIYYytKA) as well.

We have a growing tech-related [Discord Channel](https://discord.gg/UXZHTx4pXY) too, so feel free to pop by.

Happy coding! 👨‍💻 🎉