Введение: зачем контейнеризировать TypeScript-приложения?
Современная разработка программного обеспечения немыслима без контейнеризации. Docker стал стандартом де-факто для упаковки, доставки и запуска приложений в изолированных средах. В сочетании с TypeScript — строго типизированным надмножеством JavaScript — Docker обеспечивает предсказуемость, переносимость и упрощает CI/CD процессы.
В этой статье мы разберём, как правильно контейнеризировать TypeScript-приложение: от базового Dockerfile до production-ready сборки с использованием docker-compose и многостадийной сборки. Вы узнаете, как избежать типичных ошибок, уменьшить размер образа и ускорить развёртывание.
Основы: TypeScript и Node.js в Docker
TypeScript — это язык, который компилируется в JavaScript. Это означает, что для запуска TypeScript-приложения в контейнере нам нужно сначала скомпилировать исходный код, а затем выполнить полученный JavaScript с помощью Node.js. Docker идеально подходит для автоматизации этого процесса.
Создание простого TypeScript-приложения
Для начала создадим минимальное приложение на TypeScript. Структура проекта:
my-ts-app/
├── src/
│ └── index.ts
├── package.json
├── tsconfig.json
└── Dockerfile
src/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/**/*"]
}
Базовый Dockerfile
Первый, самый простой вариант Dockerfile:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]
Этот Dockerfile копирует весь проект, устанавливает зависимости, компилирует TypeScript и запускает приложение. Однако у этого подхода есть недостатки: размер образа большой, а кэширование слоёв неэффективно.
Оптимизация: многостадийная сборка и уменьшение размера
Для production-среды рекомендуется использовать многостадийную сборку (multi-stage build). Это позволяет разделить этапы сборки и выполнения, оставляя в финальном образе только необходимые файлы.
Production Dockerfile с многостадийной сборкой
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
# Stage 2: Production
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
EXPOSE 3000
USER node
CMD ["node", "dist/index.js"]
Преимущества такого подхода:
- Меньший размер образа — финальный слой содержит только скомпилированный код и production-зависимости.
- Безопасность — в контейнере не остаётся исходного кода TypeScript, dev-зависимостей и инструментов сборки.
- Быстрое кэширование — изменения в исходном коде не влияют на слой с зависимостями.
- Запуск от непривилегированного пользователя — директива
USER nodeповышает безопасность.
Оптимизация с помощью .dockerignore
Файл .dockerignore предотвращает копирование ненужных файлов в контекст сборки:
node_modules
dist
.git
.gitignore
*.md
.env
.env.*
Dockerfile
docker-compose.yml
Docker Compose: оркестрация для разработки и продакшена
Docker Compose позволяет управлять несколькими контейнерами одновременно. Для TypeScript-приложения это особенно полезно при наличии базы данных, очередей или других сервисов.
docker-compose.yml для разработки
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "3000:3000"
volumes:
- .:/app
- /app/node_modules
environment:
- NODE_ENV=development
- PORT=3000
command: npm run dev
Для разработки используем отдельный Dockerfile (Dockerfile.dev):
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev"]
Монтирование тома (volumes) позволяет автоматически перезагружать приложение при изменении кода (с помощью ts-node-dev или nodemon).
docker-compose.yml для продакшена
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- PORT=3000
restart: unless-stopped
Продвинутые техники и best practices
1. Использование Healthcheck
Добавьте проверку здоровья контейнера:
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/ || exit 1