Docker + Node (Typescript)

19 April 2019 — Written by HeberGB
#tutorial#docker#node#typescript

The goal of this example is to show you how to get a Node.js with Typescript application into a Docker container and make a docker-compose to develop environment. I will try to explain how to pass from develop to production with docker. So I'll assume you know what is docker and you have it installed.

As an example, let's create a simple web server with Express, which you can replace with your favorite node package.

Getting node dependencies

First, create a new directory where all the files would live. In this directory run the next command to create a new node package.

npm init -y

Now we need to install some dependencies to build Typescript-Node app

npm install -D nodemon ts-node typescript @types/express @types/node
npm install -S express

Then we concat the next npm scripts to package.json

...
  "scripts": {
    "build": "tsc",
    "start": "nodemon src/index.ts"
  },
...

Typescript needs a json file with basic settings to compiler, you can see examples here

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "sourceMap": true,
    "outDir": "build"
  }
}

This configuration sets:

  1. target Javascript version to es5 (ECMAScript 5).
  2. modules handling to commonjs.
  3. sourceMap to true because it helps to track errors from production code (JS) to development code (TS).
  4. outdir to build which means that our compiled code will be stored in the build directory.

As you can see we use nodemon, so we need to set up the configuration file just like that

{
  "verbose": true,
  "ignore": ["src/**/*.spec.ts"],
  "watch": ["src/**/*.ts"],
  "execMap": {
    "ts": "node --inspect=0.0.0.0:9229 --nolazy -r ts-node/register"
  }
}
  • Well the verbose is to indicates to nodemon that you have a descriptive log of what it's happening.
  • ignore is an array of files to ignore by watcher.
  • watcher is an array with files that nodemon must watch.
  • Finally, execMap is an object that defines what interpreter executes which files (now nodemon will know what use to run ts files).

Making example application

To make an application with express we will create two files:

  • First the router named example.router.ts:
import { Router, Request, Response, NextFunction } from "express";

const router = Router();

router.get(
  "/hello/:name",
  (req: Request, res: Response, next: NextFunction) => {
    const { name } = req.params;
    res.send(`Hello ${name} from express`);
  },
);

export const exampleRouter = router;
  • And the entry point of app, index.ts:
import * as express from "express";
import { exampleRouter } from "./routes/example.router";

const PORT = 8080;

let app = express();

app.use(exampleRouter);

app.listen(PORT, () => {
  console.log(`Server listening on port ${PORT}`);
});

Preparing develop environment with docker-compose

Now we will build our develop environment, we will make the next Dockerfile.dev:

FROM node:10.15.3-alpine

WORKDIR /home/node/app
ADD package*.json ./

RUN npm i

USER node

This configuration means:

  1. Get node image.
  2. Set up work directory /home/node/app inside container.
  3. Copy package.json to container.
  4. Install all dependencies.
  5. Change to node user

And we will use a docker-compose like that:

version: "3.7"

services:
  web-server:
    build:
      context: .
      dockerfile: Dockerfile.dev
    container_name: example-web-server
    volumes:
      - ./src:/home/node/app/src
      - ./nodemon.json:/home/node/app/nodemon.json
    expose:
      - "8080"
    ports:
      - "8080:8080"
      - "9229:9229"
    command: npm start

We set up with:

  1. Version 3.7.
  2. A service named web-server with next configuration:

    1. Set current directory as context.
    2. Get the file Dockerfile.dev.
    3. Name container as example-web-server.
    4. Set volumes src and nodemon.json.
    5. Expose port 8080 to other services.
    6. Bind host ports with container ports 8080 and 9229.
    7. Run npm script start.

NOTE: You also can set up more services

At this point you can exec next command to up the services:

docker-compose up -d

Get node_modules from container (optional)

You can copy the node_modules directory to the Typescript interpreter works correctly if you have not installed the packages locally.

docker cp example-web-server:/app/node_modules .

Set up debugger in VSCode (optional)

You can set up a remote debugger in VSCode appending the next configuration into the file launch.json.

{
  "type": "node",
  "request": "attach",
  "name": "Docker node",
  "address": "localhost",
  "port": 9229,
  "localRoot": "${workspaceFolder}",
  "remoteRoot": "/app",
  "protocol": "inspector"
}

Now you can start the remote debugger.

Preparing production environment with docker

Finally we build the production docker image with next settings:

FROM node:10.15.3-alpine

WORKDIR /home/node/app
ADD . .

ENV NODE_ENV=production
RUN npm ci

USER node

EXPOSE 8080

CMD [ "node", "build/index.js" ]

As you can see Dockerfile looks to similar to Dockerfile.dev just have a few differences:

  1. Set up NODE_ENV to production.
  2. We replaced npm i for npm ci because npm ci was maked to be used in automated environments such as test platforms, continuous integration, and deployment (you can see more here).
  3. We don't use npm to start the server because "first off this reduces the number of processes running inside of your container. Secondly it causes exit signals such as SIGTERM and SIGINT to be received by the Node.js process instead of npm swallowing them" (best practices).

Build code code to production

In this part, we will delegate this job to the CI/CD tool such as Travis, Jenkins, Circle CI, etc.

For this example we will use Travis

language: node_js

node_js:
  - 10

branches:
  only:
    - master
    - develop
    - "/^travis-.*$/"

services:
  - docker

before_install:
  # Install dependencies
  - gem update --system
  - npm install
  - npm run build
  # Other jobs to prepare your environment
  # like AWS settings, GCP settings, etc
  - docker info
  - echo $DOCKERHUB_PASSWORD | docker login -u $DOCKERHUB_LOGIN --password-stdin

install:
  - docker build -t your_docker_id/example-image .

script: "true"

after_success:
  # Scripts to tag and push images to docker hub
  # Scripts to deploy to cloud service like GCP, AWS, Azure, etc

As you can see in this part we transpile Typescript to Javascript with command npm run build that runs tsc which get settings from tsconfig.json.

Summary

Docker can allow you develop in any where. You just need to set up Dockerfile.dev and docker-compose.yml to develop environment. Then you even can set up Dockerfile and a CI/CD tool to deploy production environment to some cloud service.

Maybe looks hard to set up but its just at begins and later to every developer is enough with runs docker-compose up

Finally I invite you to try Docker in your Typescript projects because have more pros than cons. If you have some questions just message me.

Have fun 🙂

Github repository https://github.com/HeberGB/example-typescript-docker