Введение в Docker и TypeScript: зачем нужна контейнеризация
Современная веб-разработка немыслима без инструментов, обеспечивающих единообразие окружения. Docker стал стандартом де-факто для упаковки, доставки и запуска приложений. В сочетании с TypeScript — строго типизированным языком, компилирующимся в JavaScript — контейнеризация даёт мощный инструмент для создания масштабируемых и надёжных сервисов.
Зачем контейнеризировать TypeScript-приложение? Во-первых, это решает проблему «на моей машине работает». Во-вторых, упрощает развёртывание в production и CI/CD. В-третьих, позволяет изолировать зависимости и версии Node.js. В этой статье мы разберём, как создать Docker-образ для TypeScript-приложения, настроить multi-stage сборку и использовать docker-compose для локальной разработки.
Основы Docker для TypeScript-разработчика
Прежде чем писать код, напомним базовые понятия Docker: образ (image) — шаблон для создания контейнера; контейнер (container) — запущенный экземпляр образа; Dockerfile — инструкция по сборке образа.
Для TypeScript-приложения на Node.js нам понадобятся:
- Node.js runtime (обычно образ
node:18-alpineилиnode:20-alpineдля минимального размера) - Установка зависимостей (
npm installилиyarn install) - Компиляция TypeScript в JavaScript (
tsc) - Запуск скомпилированного приложения
Рассмотрим простой пример TypeScript-приложения (файл 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}`);
});
И tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
Теперь создадим Dockerfile для этого приложения.
Создание Dockerfile для TypeScript-приложения
Базовый Dockerfile может выглядеть так:
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:18-alpine AS runner
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/index.js"]
Здесь используется multi-stage build (многоступенчатая сборка). Первая стадия (builder) собирает TypeScript в JavaScript, вторая (runner) содержит только скомпилированный код и runtime-зависимости. Это уменьшает размер финального образа — в него не попадают исходники TypeScript и dev-зависимости.
Важно: мы выполняем npm ci (чистая установка) вместо npm install для воспроизводимости и скорости.
Оптимизация Dockerfile: кэширование слоёв
Docker кэширует каждый слой (инструкцию). Чтобы ускорить сборку, копируйте файлы, которые меняются редко, раньше. Например, package.json и package-lock.json копируются до всех исходников, поэтому изменения в коде не приводят к переустановке зависимостей.
Дополнительно можно добавить .dockerignore:
node_modules
dist
.git
*.md
.gitignore
.env
Это предотвращает попадание лишних файлов в контекст сборки.
docker-compose для локальной разработки
Для удобства локальной разработки используйте docker-compose.yml. Он позволяет запускать приложение с автоматической перезагрузкой (используя nodemon или ts-node-dev) и подключать базы данных или другие сервисы.
Пример docker-compose.yml:
version: '3.8'
services:
app:
build:
context: .
target: builder
ports:
- "3000:3000"
volumes:
- .:/app
- /app/node_modules
command: npx ts-node-dev --respawn src/index.ts
environment:
- NODE_ENV=development
Объяснение:
build.context— путь к Dockerfilevolumes— монтирование текущей директории в контейнер, что позволяет видеть изменения в реальном времениcommand— используемts-node-devдля горячей перезагрузки
Для production можно создать отдельный compose-файл, использующий стадию runner.
Лучшие практики контейнеризации TypeScript-приложений
1. Используйте минимальные базовые образы
Выбирайте alpine версии (node:18-alpine). Это уменьшает размер образа и поверхность атаки. Для production можно дополнительно использовать distroless образы от Google.
2. Разделяйте dev и production сборки
В development можно устанавливать dev-зависимости (TypeScript, тестовые библиотеки) и использовать ts-node. В production — только скомпилированный JS и runtime-зависимости.
3. Обрабатывайте сигналы остановки
Docker отправляет контейнеру сигнал SIGTERM. Убедитесь, что ваше приложение корректно завершает работу. В Express можно добавить:
process.on('SIGTERM', () => {
console.log('SIGTERM received, shutting down...');
server.close(() => process.exit(0));
});
4. Используйте healthcheck
Добавьте в Dockerfile проверку здоровья контейнера:
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/ || exit 1