Introduction to Docker and TypeScript: Why Containerization is Needed
Modern web development is unthinkable without tools that ensure environment consistency. Docker has become the de facto standard for packaging, delivering, and running applications. Combined with TypeScript — a strictly typed language that compiles to JavaScript — containerization provides a powerful tool for creating scalable and reliable services.
Why containerize a TypeScript application? First, it solves the "it works on my machine" problem. Second, it simplifies deployment to production and CI/CD. Third, it allows isolating dependencies and Node.js versions. In this article, we will break down how to create a Docker image for a TypeScript application, configure multi-stage builds, and use docker-compose for local development.
Docker Basics for a TypeScript Developer
Before writing code, let's recall the basic concepts of Docker: an image is a template for creating a container; a container is a running instance of an image; a Dockerfile is the instruction set for building an image.
For a TypeScript application on Node.js, we will need:
- Node.js runtime (usually the
node:18-alpineornode:20-alpineimage for minimal size) - Installing dependencies (
npm installoryarn install) - Compiling TypeScript to JavaScript (
tsc) - Running the compiled application
Let's look at a simple example of a TypeScript application (file src/index.ts):
import express from 'express';
const app = express();const PORT = process.env.PORT || 3000;
app.get('/', (req, res) => { res.send('Hello from TypeScript + Docker!');});
app.listen(PORT, () => { console.log(`Server running on port ${PORT}`);});And tsconfig.json:
{ "compilerOptions": { "target": "ES2020", "module": "commonjs", "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true }, "include": ["src"], "exclude": ["node_modules", "dist"]}Now let's create a Dockerfile for this application.
Creating a Dockerfile for a TypeScript Application
A basic Dockerfile might look like this:
FROM node:18-alpine AS builderWORKDIR /appCOPY package*.json ./RUN npm ciCOPY . .RUN npm run build
FROM node:18-alpine AS runnerWORKDIR /appCOPY --from=builder /app/dist ./distCOPY --from=builder /app/package*.json ./COPY --from=builder /app/node_modules ./node_modulesEXPOSE 3000CMD ["node", "dist/index.js"]Here, a multi-stage build is used. The first stage (builder) compiles TypeScript into JavaScript, the second (runner) contains only the compiled code and runtime dependencies. This reduces the size of the final image — TypeScript sources and dev dependencies do not end up in it.
Important: we run npm ci (clean install) instead of npm install for reproducibility and speed.
Optimizing the Dockerfile: Layer Caching
Docker caches each layer (instruction). To speed up the build, copy files that change infrequently earlier. For example, package.json and package-lock.json are copied before all source files, so code changes do not lead to reinstalling dependencies.
Additionally, you can add a .dockerignore:
node_modulesdist.git*.md.gitignore.envThis prevents unnecessary files from entering the build context.
docker-compose for Local Development
For convenient local development, use docker-compose.yml. It allows you to run the application with automatic reloading (using nodemon or