How to Dockerize Your TypeScript Application With Multi-Stage Build: A Step-By-Step Guide

Chinwendu E. Onyewuchi
6 min readFeb 15, 2024

Introduction

Typescript and Docker are essential tools to include in your tech stack when building a Nodejs application especially if you're concerned about early bug detection and quick code spin-up and delivery.

In this tutorial, you will learn how to dockerize your Node.js/TypeScript application using a Multi-Stage Build approach. Multi-stage build allows you to create streamlined Docker images using multiple build stages, resulting in smaller and more optimized containers.

Prerequisite

To follow along with this tutorial, you should have the following installed on your system:

Also, you should have a basic understanding of TypeScript, Docker, and Docker compose.

You can find the code used in this tutorial here.

Step 1 — Setting Up The Project

To begin, open your terminal and create a work directory for your project

# Create and enter the project directory
$ mkdir typescript-docker-demo && cd typescript-docker-demo

Initialize the project to create a package.json file

$ npm init -y

Step 2— Installing The Dependencies

Express will be needed for creating the server. You can install Express by running:

$ npm i express

Install other development dependencies

$ npm i @types/express typescript ts-node-dev rimraf --save-dev

ts-node-dev — restarts the server automatically when we make changes.

@types/express — provides the typescript definitions for express.

rimraf — will be used to delete previous build files to avoid stale ones. This is considered best practice when working with Typescript.

Next, generate a tsconfigfile.

$ npx tsc --init

With the tsconfigfile included in the project directory, open the project in your preferred text editor and configure the tsconfig file accordingly.

P.S: You can add more configurations, however, for this illustration, we will need just these:


#Edit the following compilerOptions in the tsconfig file

"target": "es2016",
"rootDir": "./src" ,
"outDir": "./dist"

target — specifies the Javascript language you want to build the Typescript files to.

rootDir — points Typescript to where our source code is located, in this case, the srcfolder.

outDir— specifies where the build JS file will be stored.

Step 2 — Spin up a basic Express Server

Create a src folder in the project’s root directory with an index.ts file inside and add the following lines of code.

src/index.ts

import express, { Express, Request, Response } from "express";

const app: Express = express();

app.get("/", (req: Request, res: Response) => {
res.status(200).json("Hello from the server!!!");
});

app.listen(4000, () => {
console.log(`App is listening on port 4000`);
});

Open the package.jsonfile and add this script:

"scripts": {
"dev": "ts-node-dev --poll ./src/index.ts",
"build": "rimraf ./dist && tsc",
"start": "npm run build && node dist/index.js"
},

The devcommand runs the code in the development environment, while the buildand startcommands will be useful in the production environment. The --poll flag appended to ts-node-devcontinuously monitors files for changes, ensuring automatic server restarts, which is especially beneficial in a containerized environment.

Good, now we are through with the development setup. You can start the server by running:

$ npm run dev

Visit http://localhost:4000 to be sure the app is running.

You should see:

localhost:4000

Step 3 — Set up the Dockerfile With Multi-Stage Build

To dockerize this application, first, you will create a Dockerfilein the root directory and add the following:

Dockerfile

#Build stage
FROM node:16-alpine AS build

WORKDIR /app

COPY package*.json .

RUN npm install

COPY . .

RUN npm run build

#Production stage
FROM node:16-alpine AS production

WORKDIR /app

COPY package*.json .

RUN npm ci --only=production

COPY --from=build /app/dist ./dist

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

This Dockerfile comprises two different stages tagged build and production . Here’s a breakdown of each line in the file:

  • FROM node:16-alpine — Imports a base Nodejs image from the docker repository.
  • Build Stage (AS build) — implies that this stage is for building and compiling the TypeScript code.
  • WORKDIR /app — specifies the working directory in the container from which the app will be served.
  • COPY package*.json ./— copies the package.json and package-lock.jsonfiles into the container’s working directory.
  • RUN npm install— installs the project dependencies.
  • COPY . . — copies the source code into the container’s work directory.
  • RUN npm run build— builds the TypeScript code.
  • Production Stage (AS production) — used to create the final, optimized production image.
  • RUN npm ci --only=production — installs only production dependencies when creating the production image.
  • COPY --from=build /app/dist ./dist—copies the compiled code from the build stage into the dist folder in the production environment.
  • CMD ["node", "dist/index.js"]—Executes the command to run the compiled app in the production environment.

Because we do not want to copy unnecessary files into our production container, we will create a .dockerignore file and add the following:

.dockerignore

node_modules
dist/

Step 4 — Setup Docker Compose File

With theDockerfile configured, next is to set up two Docker Compose files. One will be used to run the code in the development environment, while the other will manage the execution of the code in the production environment.

In the project root directory, create adocker-compose.dev.yml file and add the following:

docker-compose.dev.yml

version: "3.7"
services:
api:
build:
context: .
target: build
volumes:
- ./:/app
- /app/node_modules
ports:
- 4000:4000
command: npm run dev

In this Docker Compose configuration:

  • version: "3.7" — specifies the Docker Compose version used.
  • services — define the services (containers) to be managed.
  • api — is the name of the service.
  • build — provides build-related configurations.
  • context: . — sets the build context to the current directory.
  • target: build — selects the build target named "build" from the Dockerfile.
  • volumes — define shared volumes between the host and the container.
  • .:/app — mounts the current directory to the "/app" directory in the container.
  • /app/node_modules — creates a volume for the "node_modules" directory.
  • ports — maps the host's port 4000 to the container's port 4000.
  • command: npm run dev — specifies the command to run inside the container, initiating the "npm run dev" script.

These configurations will be used to run the container in the development environment. Now let’s proceed to create another docker-compose file for the production environment.

Create another file in the root directory of our project, docker-compose.prod.yml and add the following:

docker-compose.prod.yml

version: "3.7"
services:
api:
build:
context: .
target: production

ports:
- 4000:4000

Step 5— Build the Image

With the docker-compose files, you can build the image and start the container using any of the environments.

To build the image and start the container in the development environment, run the following command in your terminal:

$  docker-compose -f docker-compose.dev.yml up

-f docker-compose.dev.yml — specifies the docker-compose file to run.

Note that if you name the docker-compose file as docker-compose.yml , then the -f flag can be omitted when you want to start the container.

Here’s the output in the terminal.

code output

Similarly, to start the container in the production environment, run the following in the terminal:

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

Suppose you make changes in your development environment that you’d want to reflect in the production container, you can rebuild the production image by appending a --build flag to the start command. For instance:

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

To view all images on your system, run:

$ docker images

To view the running instances, run:

$ docker ps

To stop the container, run:

$ docker-compose -f docker-compose.dev.yml down 

And that wraps it. If you read to this point, congratulations!!! You now know how to dockerize your Typescript application.

If you found this article helpful, check out more of my articles here.

--

--