Docker and TypeScript: Containerizing Applications from A to Z
Containerization has become the standard of modern development. Docker allows you to package a TypeScript application along with all its dependencies and environment, guaranteeing its stable operation on any platform — from a developer's local machine to a production server. In this article, we will cover the full cycle: from a basic Dockerfile to an optimized production build.
1. Docker Basics for a TypeScript Developer
Docker is a platform for creating, delivering, and running applications in isolated environments (containers). For TypeScript applications that compile to JavaScript and run on Node.js, Docker solves three key tasks:
- Environment Reproducibility: Forget the phrase "it works on my machine." Versions of Node.js, npm, and system packages are fixed in the image.
- Isolation: Each application runs in its own container without affecting other services.
- Scalability: Containers are easily replicated and orchestrated.
Before starting, make sure you have Docker and Docker Compose installed. You can check with the commands:
docker --versiondocker-compose --version2. Creating a Dockerfile for a TypeScript Application
Let's start with a simple TypeScript application. Suppose we have a 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 Docker + TypeScript!');});
app.listen(port, () => { console.log(`Server started on port ${port}`);});Now let's create a Dockerfile in the project root:
# ---- Base image for development ----FROM node:18-alpine AS baseWORKDIR /appCOPY package*.json ./RUN npm ci --only=production
# ---- Build TypeScript ----FROM base AS buildCOPY tsconfig.json ./COPY src/ ./src/RUN npm ci && npm run build
# ---- Production image ----FROM node:18-alpine AS productionWORKDIR /appCOPY --from=build /app/dist ./distCOPY --from=base /app/node_modules ./node_modulesEXPOSE 3000CMD ["node", "dist/index.js"]This is a multi-stage build. It allows you to:
- Install all dependencies (including devDependencies) only during the build stage
- Only compiled JavaScript and production dependencies end up in the final image
- The image size is reduced by 2-3 times
Build the image and run the container:
docker build -t my-typescript-app .docker run -p 3000:3000 my-typescript-app3. Setting up docker-compose for Development and Production
Docker Compose simplifies working with multi-container applications. Let's create a docker-compose.yml:
version: '3.8'
services: app: build: context: . target: development ports: - "3000:3000" volumes: - .:/app - /app/node_modules environment: - NODE_ENV=development command: npm run dev
# Production version app-prod: build: context: . target: production ports: - "80:3000" environment: - NODE_ENV=productionFor convenience, let's add a separate Dockerfile for development (you can extend the base image):
# Dockerfile.devFROM node:18-alpineWORKDIR /appCOPY package*.json ./RUN npm installCOPY . .EXPOSE 3000CMD ["npm", "run", "dev"]Running in development mode:
docker-compose up appThanks to volumes, code changes are picked up immediately (hot-reload), and /app/node_modules is excluded to avoid overwriting dependencies from the container.