How to Build TypeScript for Production in Docker


Running TypeScript in Docker for development is fairly the same process as running TypeScript locally on the host machine.

We would still need configuration files such as tsconfig.json and maybe even a nodemon.json.

We would need the same devDependencies such as ts-node, typescript, and any relevant @types/* packages.

We would have to mount our source code to enable hot reloading.

But how can we properly compile our TypeScript into JavaScript for our production builds?

We will have to use a multi-stage build to achieve this. In my examples, I’ll be demonstrating a simple Express application with this project structure.

📂 typescript-express-app
 ┗ 📂 src
    ┗ 📜 index.ts 
 ┣ 📜 .dockerignore
 ┣ 📜 Dockerfile
 ┣ 📜 package.json
 ┗ 📜 tsconfig.json

Multi-stage Build

Multi-stage builds allow us to have multiple stages for our build process and a single stage at the end for our production image, all in a single Dockerfile.

Stage 1: TypeScript Compiler

In our first stage, we need to install all the TypeScript-related dependencies in order to compile our TypeScript.

FROM node:14-alpine3.10 as ts-compiler
WORKDIR /usr/app
COPY package*.json ./
COPY tsconfig*.json ./
RUN npm install
COPY . ./
RUN npm run build

After we copy over source into the container in our Dockerfile, we want to do this compilation with npm run build.

In our package.json, we can add a build script that will simply run tsc, which compiles our TypeScript into JavaScript.

"scripts": {
  ...
  "build": "tsc",
  ...
}

Note that the output location of the JavaScript files depends on the specified rootDir and outDir in tsconfig.json.

// tsconfig.json
{
  "compilerOptions": {
    ...
    "rootDir": "./src",
    "outDir": "./build",
    ...
  }
}

Now, we have a new /build folder with our beautiful JavaScript files.

This is what we want to run in our production image.

The thing is, however, that we want to get rid of all the TypeScript dependencies.

Stage 2: TypeScript Remover

The purpose of this stage is just to strip all things TypeScript from our image.

FROM node:14-alpine3.10 as ts-remover
WORKDIR /usr/app
COPY --from=ts-compiler /usr/app/package*.json ./
COPY --from=ts-compiler /usr/app/build ./
RUN npm install --only=production

We can copy all the production-ready files over to the root of the container, and then install only the dependencies needed to run the application (no devDependencies).

Stage 3: Distroless Production

Lastly, we can run this all on a distroless image, a very small, secure base image developed by Google.

FROM gcr.io/distroless/nodejs:14
WORKDIR /usr/app
COPY --from=ts-remover /usr/app ./
USER 1000
CMD ["index.js"]

We can copy everything from Stage 2 into this container. We need this extra stage because distroless images don’t have npm installed, so we are using node:alpine in Stage 2 to run npm install, and then using Stage 3 purely for the distroless image.

Finally, we can put all three stages one after another in a single Dockerfile, and we should be ready to go with TypeScript compilation for a production environment.