Введение: от простого к сложному в мире контейнеризации
На начальном уровне работа с Docker и JavaScript (Node.js) часто сводится к созданию простого Dockerfile и запуску контейнера. Однако в реальных продакшн-системах этого недостаточно. Продвинутый уровень подразумевает глубокое понимание механизмов сборки, оптимизации размера образов, управления зависимостями, сетевой изоляции и безопасности.
В этой статье мы разберём ключевые аспекты профессиональной контейнеризации Node.js-приложений. Вы узнаете, как использовать многостадийную сборку (multi-stage builds), настраивать Docker Compose для микросервисной архитектуры, управлять переменными окружения и минимизировать поверхность атаки. Материал ориентирован на разработчиков, уже знакомых с основами Docker и JavaScript.
1. Многостадийная сборка (Multi-stage builds) для Node.js
Одна из главных проблем при контейнеризации Node.js-приложений — избыточный размер финального образа. Стандартный подход с одним Dockerfile включает в себя инструменты сборки (TypeScript, Webpack, Babel), которые не нужны в рантайме. Многостадийная сборка позволяет разделить процесс на этапы: сборка и выполнение.
1.1. Пример Dockerfile с двумя стадиями
# Этап сборки (build)
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build # Предполагается, что build-скрипт собирает проект
# Этап выполнения (production)
FROM node:20-alpine AS production
WORKDIR /app
# Копируем только node_modules и собранные файлы
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY package*.json ./
EXPOSE 3000
USER node
CMD ["node", "dist/index.js"]
Ключевые моменты:
node:20-alpine— легковесный образ на основе Alpine Linux (около 30 МБ).npm ci— чистая установка зависимостей строго поpackage-lock.json.--only=production— исключаем dev-зависимости (TypeScript, тесты).USER node— отказ от root-пользователя для безопасности.
В результате финальный образ содержит только необходимые для запуска файлы, а размер сокращается с 300-400 МБ до 80-120 МБ.
1.2. Оптимизация кэширования слоёв
Docker кэширует каждый слой (команду). Чтобы ускорить повторные сборки, копируйте package*.json до копирования всего исходного кода. Если зависимости не изменились, слой с npm ci будет взят из кэша.
COPY package*.json ./
RUN npm ci --only=production
COPY . . # Копируется только если исходники изменились
2. Docker Compose для микросервисной архитектуры
В продакшне Node.js-приложение редко работает изолированно. Обычно требуется база данных (PostgreSQL, MongoDB), кэш (Redis) или очередь сообщений (RabbitMQ). Docker Compose позволяет описать всю инфраструктуру в одном YAML-файле.
2.1. Пример docker-compose.yml для Node.js + Redis
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile.prod
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- REDIS_URL=redis://redis:6379
depends_on:
- redis
networks:
- backend
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
networks:
- backend
volumes:
redis_data:
networks:
backend:
driver: bridge
Важные аспекты:
- Сетевые изоляция: сервисы общаются через внутреннюю сеть
backend. Redis не доступен извне (если не пробросить порт). - Именованные тома:
redis_dataсохраняет данные при перезапуске контейнера. - Переменные окружения:
REDIS_URLиспользует имя сервиса (redis) как DNS-имя. - Зависимости:
depends_onгарантирует порядок запуска (но не готовность — используйте healthcheck).
2.2. Healthcheck для Node.js
Чтобы Compose ждал, пока приложение действительно запустится, добавьте healthcheck в Dockerfile или сам сервис:
# В Dockerfile
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
Или в docker-compose.yml:
healthcheck:
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"]
interval: 30s
timeout: 10s
retries: 3
3. Безопасность и ограничение ресурсов
Контейнеры не являются изолированной средой по умолчанию. Продвинутая контейнеризация требует настройки прав и лимитов.
3.1. Запуск от непривилегированного пользователя
Node.js-образы по умолчанию работают от root. Это опасно: если злоумышленник получит доступ к контейнеру, он получит root-права на хосте (при маппинге томов). Всегда переключайте пользователя:
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
3.2. Read-only файловая система
В продакшне приложение не должно писать в файловую систему контейнера (используйте тома или S3). Запустите контейнер с флагом --read-only:
docker run --read-only -v /tmp:/tmp my-node-app
В docker-compose.yml: