Introduction: Why Containerize TypeScript Applications?
Modern software development is unthinkable without containerization. Docker has become the de facto standard for packaging, delivering, and running applications in isolated environments. Combined with TypeScript — a strictly typed superset of JavaScript — Docker ensures predictability, portability, and simplifies CI/CD processes.
In this article, we will break down how to properly containerize a TypeScript application: from a basic Dockerfile to a production-ready build using docker-compose and multi-stage builds. You will learn how to avoid common mistakes, reduce image size, and speed up deployment.
Basics: TypeScript and Node.js in Docker
TypeScript is a language that compiles to JavaScript. This means that to run a TypeScript application in a container, we first need to compile the source code, and then execute the resulting JavaScript using Node.js. Docker is ideal for automating this process.
Creating a Simple TypeScript Application
To start, let's create a minimal TypeScript application. Project structure:
my-ts-app/├── src/│ └── index.ts├── package.json├── tsconfig.json└── Dockerfilesrc/index.ts:
import express, { Request, Response } from 'express';
const app = express();const PORT = process.env.PORT || 3000;
app.get('/', (req: Request, res: Response) => { res.json({ message: 'Hello from TypeScript + Docker!' });});
app.listen(PORT, () => { console.log(`Server is running on port ${PORT}`);});package.json:
{ "name": "my-ts-app", "version": "1.0.0", "scripts": { "build": "tsc", "start": "node dist/index.js", "dev": "ts-node src/index.ts" }, "dependencies": { "express": "^4.18.2" }, "devDependencies": { "@types/express": "^4.17.21", "@types/node": "^20.11.0", "typescript": "^5.3.3", "ts-node": "^10.9.2" }}tsconfig.json:
{ "compilerOptions": { "target": "ES2020", "module": "commonjs", "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true }, "include": ["src/**/*"]}Basic Dockerfile
The first, simplest Dockerfile option:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./RUN npm install
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]This Dockerfile copies the entire project, installs dependencies, compiles TypeScript, and runs the application. However, this approach has drawbacks: the image size is large, and layer caching is inefficient.
Optimization: Multi-Stage Builds and Size Reduction
For a production environment, it is recommended to use multi-stage builds. This allows you to separate the build and execution stages, leaving only the necessary files in the final image.
Production Dockerfile with Multi-Stage Build
# Stage 1: BuildFROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./RUN npm ci --only=production
COPY . .RUN npm run build
# Stage 2: ProductionFROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./distCOPY --from=builder /app/node_modules ./node_modulesCOPY --from=builder /app/package.json ./
EXPOSE 3000
USER node
CMD ["node", "dist/index.js"]Advantages of this approach:
- Smaller image size — the final layer contains only the compiled code and production dependencies.
- Security — the TypeScript source code and dev