Введение: Почему продвинутая контейнеризация TypeScript — это необходимость
Контейнеризация TypeScript-приложений с помощью Docker давно перестала быть просто трендом — это стандарт современной разработки. Однако базовые Dockerfile'ы, которые просто копируют весь проект и запускают npm start, далеки от идеала. На продвинутом уровне мы сталкиваемся с задачами оптимизации размера образов, обеспечения безопасности, организации микросервисной архитектуры и настройки автоматического восстановления.
В этой статье мы разберём техники, которые превратят ваш Docker-пайплайн для TypeScript в промышленный стандарт: многоэтапные сборки (multi-stage builds), минимизация слоёв, использование .dockerignore, Docker Compose для микросервисов, healthchecks и работа с переменными окружения. Все примеры написаны на TypeScript и готовы к интеграции в реальные проекты.
1. Многоэтапная сборка: от разработки до production
Главная проблема базовых Dockerfile — они включают в финальный образ всё: исходники, node_modules для разработки, инструменты сборки. Для TypeScript это критично, так как tsc и devDependencies не нужны в production.
Многоэтапная сборка (multi-stage build) решает эту задачу: первый этап собирает и компилирует TypeScript, второй — создаёт минимальный образ только с необходимыми файлами.
Пример Dockerfile с двумя этапами
# Этап 1: Сборка TypeScript
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY tsconfig.json ./
COPY src/ ./src/
RUN npm run build
# Этап 2: Production-образ
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package.json ./
EXPOSE 3000
CMD ["node", "dist/index.js"]
Ключевые моменты:
- Используем
npm ci— точное воспроизведение зависимостей из lock-файла. - Копируем только
dist/иnode_modules— исходники TypeScript не попадают в production. - Alpine-образ — минимальная основа (около 5 MB).
Такой образ весит в 3-5 раз меньше, чем наивная сборка. Для большого проекта с десятками зависимостей разница может достигать сотен мегабайт.
Трёхэтапная сборка с dev-зависимостями
Иногда нужно разделить dev- и prod-зависимости. Добавляем третий этап для установки только production-зависимостей:
# Этап 1: Установка всех зависимостей
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
# Этап 2: Сборка
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# Этап 3: Production
FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=deps /app/node_modules ./node_modules
COPY package.json ./
EXPOSE 3000
CMD ["node", "dist/index.js"]
2. Оптимизация образа: .dockerignore, кэширование и слои
Каждая инструкция COPY, RUN и ADD создаёт новый слой в Docker. Чем меньше слоёв и чем эффективнее они кэшируются, тем быстрее происходит сборка.
.dockerignore — ваш первый помощник
Создайте файл .dockerignore в корне проекта:
node_modules
dist
.git
.gitignore
*.md
.env
.env.*
Dockerfile
.dockerignore
coverage
.vscode
.idea
__tests__
Это предотвращает копирование мусора в контекст сборки и ускоряет передачу файлов демону Docker.
Правильный порядок инструкций
Docker кэширует слои. Если вы сначала копируете package.json, а потом запускаете npm ci, этот слой будет переиспользован, пока не изменится package.json. Затем копируйте исходники — это последний слой, который меняется чаще всего.
# Правильный порядок (кэширование зависимостей)
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
Если вы скопируете все файлы до установки зависимостей, кэш будет сбрасываться при любом изменении любого файла.
Минимизация количества слоёв
Объединяйте связанные команды в один RUN:
# Плохо (3 слоя)
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
# Хорошо (1 слой)
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
3. Docker Compose для TypeScript микросервисов
Когда ваше приложение состоит из нескольких сервисов (API, база данных, кэш, очередь), Docker Compose становится незаменимым. Рассмотрим пример микросервисной архитектуры на TypeScript.
Пример docker-compose.yml
version: '3.8'
services:
api:
build:
context: ./api
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DB_HOST=postgres
- REDIS_HOST=redis
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/health', r => process.exit(r.statusCode === 200 ? 0 : 1))"]
interval: 30s
timeout: 5s
retries: 3
postgres:
image: postgres:15-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d myapp"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
vol