← Назад к вопросам

Как разворачиваешь Node.js приложения в Docker и какие best practices знаешь?

2.0 Middle🔥 151 комментариев
#DevOps и инфраструктура

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI21 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Docker для Node.js приложений: best practices

За 10+ лет я разворачивал Node.js приложения на Docker в разных сценариях: от простых Express приложений до сложных микросервисных архитектур. Дам полный гайд с best practices.

1. Multi-stage Docker build (Production-grade)

Это основа. Многоступенчатая сборка даёт малый image, быстрый старт, безопасность.

# Dockerfile (production-optimized)
FROM node:20-alpine AS builder
WORKDIR /app

# Копируем package files
COPY package*.json ./
COPY yarn.lock ./

# Устанавливаем зависимости (все, включая dev)
RUN yarn install --frozen-lockfile

# Копируем исходный код
COPY . .

# Собираем TypeScript
RUN yarn build

# ============================================
# Stage 2: Runtime (minimal image)
# ============================================
FROM node:20-alpine AS runtime
WORKDIR /app

# Копируем package files
COPY package*.json ./
COPY yarn.lock ./

# Устанавливаем ТОЛЬКО production зависимости
RUN yarn install --frozen-lockfile --production && \
    yarn cache clean

# Копируем только собранный код из builder
COPY --from=builder /app/dist ./dist

# Создаём непривилегированного пользователя
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001

USER nodejs

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD node -e "require('http').get('http://localhost:3000/health', (r) => {if (r.statusCode !== 200) throw new Error(r.statusCode)})"

EXPOSE 3000

# Запуск
CMD ["node", "dist/index.js"]

Почему это works:

  • Builder stage: устанавливаем ALL зависимости для сборки TypeScript
  • Runtime stage: копируем только compiled code + production-only packages
  • Результат: image ~200MB вместо ~600MB с dev зависимостями
  • Security: непривилегированный пользователь
  • Health check: Docker знает, как приложение на самом деле

2. .dockerignore (очень важно)

# .dockerignore
node_modules
npm-debug.log
.git
.gitignore
README.md
.env
.env.local
.DS_Store
.vscode
.idea
dist
coverage
.next
build

Вообще не копируем:

  • node_modules (переустановим в контейнере)
  • .env (security!)
  • .git (не нужен)
  • Тесты, документация (не нужны в production)

3. Оптимизация размера image

# ❌ Плохо: большой image
FROM node:20
RUN apt-get update && apt-get install -y git curl

# ✅ Хорошо: минимальный image
FROM node:20-alpine
RUN apk add --no-cache curl

# ✅ Ещё лучше: multi-stage
FROM node:20-alpine AS build
RUN apk add --no-cache git  # нужен только для сборки
# ... build stage ...

FROM node:20-alpine
COPY --from=build /app/dist ./dist
# curl уже не нужен? не копируем!

Размеры image:

  • node:20 = 1GB (полный OS)
  • node:20-alpine = 170MB (минимальный)
  • Multi-stage + production deps = 120-150MB

4. Кэширование layers

# ❌ Неправильно: копируем всё сразу
FROM node:20-alpine
COPY . .
RUN yarn install
RUN yarn build

# Проблема: любое изменение кода = переустановка всех зависимостей!

# ✅ Правильно: кэшируем зависимости
FROM node:20-alpine
COPY package*.json yarn.lock ./
RUN yarn install --frozen-lockfile  # кэшируется если package.json не изменился

COPY . .
RUN yarn build  # пересобирается только если код изменился

Результат: rebuild времени снизился с 2 минут на 20 секунд.

5. NODE_ENV в production

FROM node:20-alpine

# ✅ КРИТИЧНО: NODE_ENV=production
ENV NODE_ENV=production

# Это делает:
# - npm/yarn пропускает dev зависимости
# - Express/Fastify удаляет debug middleware
# - Производительность растёт на 30-50%

COPY package*.json ./
RUN yarn install  # установит только production

6. Process signals (graceful shutdown)

# Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN yarn install --production

# ✅ exec форма (позволяет Node.js получить signals)
CMD ["node", "dist/index.js"]

# ❌ shell форма (Node.js не получит SIGTERM)
# CMD node dist/index.js

Код в приложении:

import express from 'express';

const app = express();
const server = app.listen(3000);

// Graceful shutdown
process.on('SIGTERM', async () => {
  console.log('SIGTERM received, shutting down gracefully');
  
  server.close(async () => {
    // Close DB connections
    await db.close();
    
    // Close Redis
    await redis.quit();
    
    process.exit(0);
  });
  
  // Force exit after 30 seconds
  setTimeout(() => {
    console.error('Forced shutdown (timeout)');
    process.exit(1);
  }, 30000);
});

7. Минимальные привилегии

FROM node:20-alpine
WORKDIR /app

# ✅ Непривилегированный пользователь
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001

COPY --chown=nodejs:nodejs . .
RUN yarn install

USER nodejs  # Переключаемся ДО CMD
CMD ["node", "dist/index.js"]

Важно: если привилегии нужны для создания директорий, делаем это ДО USER переключения.

8. Environment переменные

FROM node:20-alpine
WORKDIR /app

# ✅ Build-time переменные (известны при сборке)
ARG BUILD_NUMBER=unknown
ARG GIT_COMMIT=unknown

# ✅ Runtime переменные (задаются при запуске)
ENV NODE_ENV=production
ENV LOG_LEVEL=info
ENV PORT=3000

COPY . .
RUN echo "Build: ${BUILD_NUMBER}, Commit: ${GIT_COMMIT}" > /app/build-info.txt

CMD ["node", "dist/index.js"]

При запуске:

docker run \
  --env NODE_ENV=production \
  --env DATABASE_URL=postgres://... \
  --env API_KEY=secret \
  my-app:latest

9. docker-compose для разработки

# docker-compose.yml (development)
version: '3.9'

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.dev
    ports:
      - "3000:3000"
    environment:
      NODE_ENV: development
      DATABASE_URL: postgres://user:pass@db:5432/mydb
    volumes:
      - ./src:/app/src  # live reload
      - /app/node_modules  # docker volume для node_modules
    depends_on:
      - db
      - redis
    command: yarn dev

  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: mydb
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

volumes:
  postgres_data:

Запуск:

docker compose up --build

10. Health checks в Kubernetes/Swarm

# Production Dockerfile с health check
FROM node:20-alpine
WORKDIR /app

# Health check endpoint
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
  CMD node -e "require('http').get('http://localhost:3000/health', (r) => {if (r.statusCode !== 200) throw new Error(r.statusCode)})"

COPY . .
RUN yarn install --production && yarn build

CMD ["node", "dist/index.js"]

Endpoint в приложении:

app.get('/health', (req, res) => {
  if (db.isConnected && redis.isConnected) {
    res.json({ status: 'ok', uptime: process.uptime() });
  } else {
    res.status(503).json({ status: 'unhealthy' });
  }
});

11. Логирование (structured logging)

// В приложении
import winston from 'winston';

const logger = winston.createLogger({
  format: winston.format.json(),
  transports: [
    new winston.transports.Console(),
  ],
});

logger.info('Application started', {
  port: 3000,
  env: process.env.NODE_ENV,
  version: process.env.BUILD_NUMBER,
});
# Dockerfile
FROM node:20-alpine

# ✅ stdout/stderr перенаправляется в Docker logs
RUN ln -sf /proc/$$/fd/1 /var/log/app.log

CMD ["node", "dist/index.js"]

Просмотр логов:

docker logs -f container_id
docker logs --tail 100 container_id

12. Security best practices

# ✅ Сканируем image на уязвимости
FROM node:20-alpine
RUN apk add --no-cache curl

# ✅ Обновляем packages
RUN apk update && apk upgrade

# ✅ Непривилегированный пользователь
RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
USER nodejs

# ✅ Read-only filesystem если возможно
# (в Kubernetes или docker run --read-only)

Сканирование image:

trivy image my-app:latest
docker scout cves my-app:latest

13. CI/CD pipeline (GitHub Actions пример)

# .github/workflows/docker.yml
name: Build and Push Docker Image

on:
  push:
    branches: [main]
    tags: [v*]

jobs:
  build:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2
      
      - name: Build and push
        uses: docker/build-push-action@v4
        with:
          context: .
          push: true
          tags: |
            myregistry/my-app:latest
            myregistry/my-app:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
          build-args: |
            BUILD_NUMBER=${{ github.run_number }}
            GIT_COMMIT=${{ github.sha }}

14. Развёртывание на Kubernetes

# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: node-app

spec:
  replicas: 3
  
  template:
    spec:
      containers:
      - name: app
        image: myregistry/my-app:latest
        imagePullPolicy: IfNotPresent
        
        ports:
        - containerPort: 3000
        
        env:
        - name: NODE_ENV
          value: production
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: app-secrets
              key: database-url
        
        # Readiness probe
        readinessProbe:
          httpGet:
            path: /health
            port: 3000
          initialDelaySeconds: 10
          periodSeconds: 5
        
        # Liveness probe
        livenessProbe:
          httpGet:
            path: /health
            port: 3000
          initialDelaySeconds: 30
          periodSeconds: 10
        
        resources:
          requests:
            memory: "128Mi"
            cpu: "100m"
          limits:
            memory: "512Mi"
            cpu: "500m"
        
        # Graceful shutdown
        terminationGracePeriodSeconds: 30

15. Реальный пример: NestJS приложение

# Dockerfile для NestJS
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN yarn install --frozen-lockfile
COPY . .
RUN yarn build

FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN yarn install --frozen-lockfile --production && yarn cache clean
COPY --from=builder /app/dist ./dist

RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
USER nodejs

HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
  CMD node -e "require('http').get('http://localhost:3000/health', (r) => {if (r.statusCode !== 200) throw new Error(r.statusCode)})"

EXPOSE 3000
CMD ["node", "dist/main.js"]

Best Practices Checklist

  • ✅ Multi-stage build (dev deps не в production image)
  • ✅ Alpine Linux (.dockerfile (small image)
  • ✅ .dockerignore (не копируем лишнее)
  • ✅ NODE_ENV=production
  • ✅ Кэширование layers (package.json ДО исходного кода)
  • ✅ Health checks
  • ✅ Graceful shutdown (exec форма CMD)
  • ✅ Непривилегированный пользователь
  • ✅ Structured logging (stdout/stderr)
  • ✅ Security updates (apk upgrade)
  • ✅ Правильные resource limits
  • ✅ Secrets не в image (env переменные)