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.