# Docker Multi-Stage Builds с AI: от 1.2 GB до 89 MB продакшен-образа

> Пошаговая оптимизация Docker-образов с помощью multi-stage builds и AI-ассистентов. Dockerfile до/после, промпты для оптимизации, layer caching, security scanning.
> Author: Roman Belov · Published: 2026-04-05 · Source: https://futurecraft.pro/ru/blog/docker-multistage-ai/

Docker-образ типичного Node.js-приложения на базе `node:20` весит около 1.2 GB. Внутри — полный Debian с инструментами компиляции, от которых приложение не зависит. Каждый лишний пакет увеличивает attack surface и время деплоя.

Multi-stage builds решают задачу разделением сборки на этапы: один контейнер компилирует, другой запускает. Финальный образ содержит только runtime и артефакты. AI-ассистенты ускоряют этот процесс, генерируя оптимизированные Dockerfile за секунды вместо часов проб и ошибок.

Дальше — конкретные шаги: анализ раздутого образа, multi-stage рефакторинг, промпты для AI-оптимизации, layer caching и security scanning.

## Анатомия раздутого Docker-образа

Типичный Dockerfile для Node.js-приложения выглядит так:

```dockerfile
FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/index.js"]
```

Результат: образ ~1.2 GB. Разберём, что внутри.

```bash
docker images my-app
# REPOSITORY   TAG     IMAGE ID       SIZE
# my-app       latest  a1b2c3d4e5f6   1.24GB

docker history my-app --human --no-trunc | head -20
```

Состав образа по слоям:

| Слой | Размер | Содержимое |
|------|--------|------------|
| Base image (node:20) | ~910 MB | Debian, Python, gcc, make, curl, git |
| npm install | ~280 MB | node_modules (dev + prod) |
| COPY + build | ~50 MB | Исходники, тесты, конфиги IDE |

Три проблемы. Базовый образ тянет полный Debian с инструментами компиляции. `npm install` ставит devDependencies, нужные только для сборки. `COPY .` копирует всё, включая `.git`, тесты и конфигурации редактора.

## Multi-Stage Build: разделение сборки и runtime

Multi-stage build использует несколько инструкций `FROM` в одном Dockerfile. Каждый `FROM` создаёт новый этап. Финальный образ содержит только то, что явно скопировано из предыдущих этапов.

```dockerfile
# Этап 1: сборка
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --include=dev
COPY . .
RUN npm run build
RUN npm prune --omit=dev

# Этап 2: продакшен
FROM node:20-alpine AS production
WORKDIR /app
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
USER appuser
EXPOSE 3000
CMD ["node", "dist/index.js"]
```

Результат: 89 MB. Сокращение на 93%.

Что изменилось:

**Базовый образ.** `node:20-alpine` вместо `node:20`. Alpine Linux занимает ~5 MB вместо ~910 MB Debian. Содержит минимальный набор пакетов: musl libc, busybox, apk.

**Разделение этапов.** Этап `builder` устанавливает devDependencies, компилирует TypeScript, прогоняет линтер. Этап `production` берёт только скомпилированные файлы и production-зависимости.

**Удаление devDependencies.** `npm prune --omit=dev` после сборки убирает typescript, eslint, jest и десятки других пакетов, нужных только при разработке.

**Non-root user.** Контейнер запускается от непривилегированного пользователя. Если атакующий получает RCE, он ограничен правами `appuser`.

## Промпты для AI-оптимизации Dockerfile

AI-ассистенты эффективно анализируют Dockerfile и предлагают оптимизации. Ключ в правильном промпте: конкретная задача, контекст приложения, метрики для оптимизации.

### Промпт 1: анализ существующего Dockerfile

```
Проанализируй этот Dockerfile. Приложение: Node.js REST API на Express
с TypeScript. База: PostgreSQL через Prisma ORM.

Найди:
1. Слои, которые инвалидируют кэш при каждом коммите
2. Файлы и пакеты, ненужные в runtime
3. Security issues (запуск от root, лишние capabilities)
4. Возможности для multi-stage оптимизации

Текущий размер образа: 1.2 GB. Целевой: < 150 MB.

[вставить Dockerfile]
```

AI выявит паттерны, которые легко пропустить вручную — подобно тому, как [AI code review](/ru/blog/ai-code-review-checklist/) находит баги, которые пропускают люди. Например, Prisma ORM генерирует бинарные engine-файлы под несколько платформ. В production нужен только `linux-musl-arm64-openssl-3.0.x` (или аналог для целевой архитектуры). Остальные engine-файлы занимают 50-80 MB.

### Промпт 2: генерация оптимизированного Dockerfile

```
Сгенерируй production Dockerfile для:
- Node.js 20 + TypeScript REST API
- Prisma ORM (PostgreSQL)
- Требования: multi-stage, alpine base, non-root user,
  healthcheck, минимальный attack surface
- Prisma: оставить только linux-musl engine
- Layer caching: package.json и prisma/schema.prisma
  копируются отдельно до npm ci
```

### Промпт 3: оптимизация под конкретный стек

```
Оптимизируй multi-stage Dockerfile для Python FastAPI-приложения:
- Используй python:3.12-slim вместо alpine (musl ломает numpy/pandas)
- Virtual environment копируй целиком во второй stage
- Удали pip cache, __pycache__, .pyc файлы
- Установи только runtime-зависимости через pip --no-deps
- Добавь PYTHONDONTWRITEBYTECODE=1, PYTHONUNBUFFERED=1
```

Для Python multi-stage build выглядит иначе, потому что Alpine с musl libc несовместим с многими научными пакетами. Вместо Alpine используется `slim`-вариант:

```dockerfile
# Этап 1: сборка
FROM python:3.12-slim AS builder
WORKDIR /app
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Этап 2: продакшен
FROM python:3.12-slim AS production
WORKDIR /app
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY ./app ./app
RUN useradd --create-home appuser
USER appuser
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
```

## Layer Caching: порядок инструкций определяет скорость сборки

Docker кэширует каждый слой. Если слой не изменился, Docker использует кэш. При изменении одного слоя все последующие слои инвалидируются.

Неправильный порядок:

```dockerfile
COPY . .
RUN npm ci
RUN npm run build
```

Любое изменение в коде инвалидирует `COPY .`, и `npm ci` выполняется заново. Установка зависимостей при каждом билде — 40-120 секунд впустую.

Правильный порядок:

```dockerfile
COPY package.json package-lock.json ./
RUN npm ci
COPY prisma/schema.prisma ./prisma/
RUN npx prisma generate
COPY . .
RUN npm run build
```

`npm ci` перезапускается только при изменении `package.json` или `package-lock.json`. Prisma client перегенерируется только при изменении схемы. Код приложения меняется чаще всего, поэтому копируется последним.

### Продвинутый кэшинг с BuildKit

Docker BuildKit поддерживает монтирование кэша, который переживает пересборку образа:

```dockerfile
# syntax=docker/dockerfile:1
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY . .
RUN npm run build
```

Директория `/root/.npm` сохраняется между сборками. Даже если `package-lock.json` изменился, npm скачает из кэша пакеты, которые не обновились. На практике это сокращает время повторной сборки при частичном обновлении зависимостей.

Для Python аналогично:

```dockerfile
RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt
```

## .dockerignore: первая линия оптимизации

Файл `.dockerignore` исключает файлы из build context. Без него `COPY .` отправляет в Docker daemon всё содержимое директории, включая `.git` (десятки мегабайт), `node_modules` (сотни мегабайт) и тестовые данные.

```
# .dockerignore
.git
.gitignore
node_modules
npm-debug.log
Dockerfile*
docker-compose*
.dockerignore
.env*
*.md
LICENSE
.vscode
.idea
coverage
__tests__
*.test.ts
*.spec.ts
.husky
.eslintrc*
.prettierrc*
tsconfig.json
jest.config.*
```

Промпт для AI:

```
Сгенерируй .dockerignore для Node.js TypeScript проекта.
Исключи всё, что не нужно в production-образе:
IDE-конфиги, тесты, документацию, CI-файлы, dev-зависимости.
Оставь: package.json, package-lock.json, src/, prisma/, scripts/migrate.sh.
```

## Security Scanning: обнаружение уязвимостей до деплоя

Оптимизация размера напрямую связана с безопасностью. Меньше пакетов в образе — меньше CVE. Подробнее об автоматизированных проверках безопасности — в [чеклисте AI-аудита безопасности](/ru/blog/ai-security-audit/). Образ на базе `node:20` содержит сотни системных пакетов и множество known vulnerabilities. Образ на `node:20-alpine` содержит лишь несколько десятков пакетов.

### Trivy: сканирование образа

```bash
# Установка
brew install aquasecurity/trivy/trivy

# Сканирование образа
trivy image my-app:latest

# Только критические и высокие уязвимости
trivy image --severity CRITICAL,HIGH my-app:latest

# Сканирование Dockerfile (без сборки)
trivy config Dockerfile
```

Пример вывода:

```
my-app:latest (alpine 3.19.1)
Total: 0 (CRITICAL: 0, HIGH: 0)

Node.js (node_modules/package-lock.json)
Total: 2 (CRITICAL: 0, HIGH: 1, MEDIUM: 1)

┌─────────────────┬──────────────────┬──────────┬────────────┐
│     Library      │  Vulnerability   │ Severity │  Version   │
├─────────────────┼──────────────────┼──────────┼────────────┤
│ jsonwebtoken     │ CVE-2024-XXXXX  │ HIGH     │ 9.0.0      │
│ semver           │ CVE-2024-YYYYY  │ MEDIUM   │ 7.5.3      │
└─────────────────┴──────────────────┴──────────┴────────────┘
```

### Docker Scout: встроенное сканирование

```bash
# Анализ образа
docker scout cves my-app:latest

# Рекомендации по обновлению базового образа
docker scout recommendations my-app:latest

# Сравнение двух версий
docker scout compare my-app:latest --to my-app:previous
```

### Промпт для AI-анализа результатов сканирования

```
Вот результат Trivy-сканирования моего Docker-образа.
Для каждой уязвимости определи:
1. Эксплуатируема ли она в контексте Node.js REST API
2. Доступен ли fix (обновление пакета)
3. Приоритет исправления (критично / можно отложить)
Игнорируй уязвимости в пакетах, которые не импортируются напрямую.

[вставить вывод trivy]
```

## Distroless: ещё меньше, ещё безопаснее

Образы Google Distroless не содержат shell, package manager и утилиты ОС. Только runtime. Это минимизирует attack surface до предела.

```dockerfile
# Этап 1: сборка
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
RUN npm prune --omit=dev

# Этап 2: distroless
FROM gcr.io/distroless/nodejs20-debian12
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
EXPOSE 3000
CMD ["dist/index.js"]
```

Результат: ~70 MB. Нет shell для exec-а внутрь контейнера, нет `apt`, нет `curl`. Если атакующий получает code execution, он не может скачать малварь или исследовать систему стандартными утилитами.

Компромисс: дебаг в production усложняется. Решение — debug-вариант distroless-образа для staging:

```dockerfile
# Для staging с shell-доступом
FROM gcr.io/distroless/nodejs20-debian12:debug
```

## Финальный оптимизированный Dockerfile

Собираем всё вместе. Реальный Dockerfile для Node.js + Prisma + TypeScript:

```dockerfile
# syntax=docker/dockerfile:1

# ---------- Этап 1: установка зависимостей ----------
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci

# ---------- Этап 2: генерация Prisma и сборка ----------
FROM deps AS builder
COPY prisma/schema.prisma ./prisma/
RUN npx prisma generate
COPY . .
RUN npm run build
RUN npm prune --omit=dev
# Удаляем лишние Prisma engines
RUN find node_modules/.prisma -name 'libquery_engine-*' \
    ! -name 'libquery_engine-linux-musl-*' -delete 2>/dev/null || true

# ---------- Этап 3: продакшен ----------
FROM node:20-alpine AS production
RUN apk add --no-cache dumb-init
ENV NODE_ENV=production
WORKDIR /app
RUN addgroup -S app && adduser -S app -G app
COPY --from=builder --chown=app:app /app/dist ./dist
COPY --from=builder --chown=app:app /app/node_modules ./node_modules
COPY --from=builder --chown=app:app /app/package.json ./
COPY --from=builder --chown=app:app /app/prisma ./prisma
USER app
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s \
    CMD node -e "fetch('http://localhost:3000/health').then(r=>{if(!r.ok)throw r})"
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/index.js"]
```

Что здесь учтено:

| Оптимизация | Эффект |
|-------------|--------|
| `node:20-alpine` | Базовый образ ~5 MB вместо ~910 MB |
| Multi-stage (deps → builder → production) | Только production-артефакты в финальном образе |
| `npm prune --omit=dev` | Удаление devDependencies |
| Удаление лишних Prisma engines | -50-80 MB бинарных файлов |
| `--mount=type=cache` | Ускорение повторных сборок |
| `dumb-init` | Корректная обработка сигналов (SIGTERM) |
| Non-root user | Ограничение привилегий при RCE |
| HEALTHCHECK | Автоматический рестарт нездоровых контейнеров |
| `--chown` в COPY | Файлы принадлежат appuser, не root |

## CI/CD интеграция: автоматическое сканирование и контроль размера

Добавление проверок в CI гарантирует, что оптимизация не регрессирует:

```yaml
# .github/workflows/docker.yml
name: Docker Build & Scan
on:
  push:
    branches: [main]
  pull_request:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build image
        run: docker build -t my-app:${{ github.sha }} .

      - name: Check image size
        run: |
          SIZE=$(docker image inspect my-app:${{ github.sha }} \
            --format='{{.Size}}')
          MAX_SIZE=150000000  # 150 MB
          if [ "$SIZE" -gt "$MAX_SIZE" ]; then
            echo "Image size ${SIZE} exceeds limit ${MAX_SIZE}"
            exit 1
          fi

      - name: Trivy vulnerability scan
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: my-app:${{ github.sha }}
          format: table
          exit-code: 1
          severity: CRITICAL,HIGH
```

Этот pipeline блокирует merge, если образ превышает 150 MB или содержит критические уязвимости.

## Результаты оптимизации: до и после

| Метрика | До | После |
|---------|-----|-------|
| Размер образа | ~1.24 GB | ~89 MB |
| Время сборки (cold) | ~4 мин | ~2.5 мин |
| Время сборки (cached) | ~4 мин | ~20 сек |
| Время pull (100 Mbps) | ~100 сек | ~7 сек |

Экономия на инфраструктуре масштабируется линейно. На кластере из нескольких нод при каждом rolling update разница в размере образа напрямую влияет на время обновления. Если выбираете архитектуру деплоя, сравнение [монолита и микросервисов](/ru/blog/monolith-vs-microservices/) разбирает компромиссы по стоимости.

## Что дальше

Multi-stage builds покрывают большинство задач по оптимизации Docker-образов. Оставшиеся — специфичные случаи: монорепозитории с shared dependencies, образы с нативными бинарными модулями, мультиархитектурные сборки.

Три действия для старта:

1. Запустить `docker images` и найти образы > 500 MB. Каждый из них — кандидат на multi-stage рефакторинг.
2. Добавить Trivy в CI pipeline. Сканирование занимает 30 секунд и выявляет уязвимости до попадания в production.
3. Использовать AI-ассистент с промптами из этой статьи для анализа и генерации оптимизированных Dockerfile под конкретный стек.

Статья о [circuit breaker в edge functions](/ru/blog/circuit-breaker-deno-edge-functions/) показывает, как защитить приложение на уровне runtime. Оптимизация Docker-образа защищает на уровне инфраструктуры. Вместе они формируют два слоя production-ready системы.

---

*Нужна помощь с оптимизацией Docker? Я помогаю стартапам внедрять AI-решения и строить продукты — [belov.works](https://belov.works).*
