Первое и главное правило: используйте подходящий базовый образ. Не берите `ubuntu:latest` или `node:latest` просто потому, что они первые в поиске. Они огромны. Для production всегда ищите минималистичные варианты:
- Для Alpine Linux: `node:alpine`, `python:alpine`. Их размер в разы меньше. Внимание: Alpine использует musl libc, что иногда может вызвать проблемы со совместимостью бинарных библиотек (например, для Python-пакетов, использующих C-расширения). Тестируйте.
- Официальные «slim»-образы: `python:slim`, `node:slim`. Они немного больше Alpine, но основаны на glibc, что обеспечивает лучшую совместимость.
- Для финального образа с уже собранным приложением рассмотрите `scratch` (пустой образ) или `distroless` (от Google — содержит только ваше приложение и его зависимости, без shell и пакетного менеджера). Это вершина оптимизации безопасности и размера.
- Кэширование слоев: Располагайте инструкции, которые меняются реже (например, копирование файлов зависимостей), выше, а те, что меняются часто (копирование исходного кода), — ниже. Классический пример для Node.js:
RUN npm ci --only=production
COPY . .
```
Благодаря этому, при изменении кода, но не `package.json`, слой с `npm ci` будет взят из кэша, что ускорит сборку в десятки раз.
- Объединяйте команды: Вместо нескольких `RUN` используйте один, объединяя команды с `&&` и очищая кэш пакетного менеджера в той же строке.
RUN apt-get update
RUN apt-get install -y python3
RUN rm -rf /var/lib/apt/lists/*
```
Хорошо:
```
RUN apt-get update && apt-get install -y python3 && rm -rf /var/lib/apt/lists/*
```
Это уменьшит количество слоев и итоговый размер.
Третья практика — multi-stage сборки. Это мощнейший инструмент. Идея в том, что вы используете один образ (например, с компилятором и всеми dev-зависимостями) для сборки приложения, а результат копируете в чистый, минимальный финальный образ.
Пример для Go-приложения:
```
# Стадия сборки (builder)
FROM golang:alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp .
# Финальная стадия
FROM alpine:latest
WORKDIR /root/
COPY --from=builder /app/myapp .
CMD ["./myapp"]
```
Итоговый образ будет содержать только бинарник `myapp` и базовый Alpine, без всего инструментария Go. Это сокращает размер с сотен МБ до десятков.
Четвертый совет — игнорируйте ненужное. Файл `.dockerignore` работает так же, как `.gitignore`. В него нужно добавить всё, что не должно попасть в контейнер: `.git`, `node_modules`, логи, временные файлы, файлы IDE (`.idea`, `.vscode`), документацию. Это ускоряет сборку (меньше файлов копировать) и повышает безопасность (в образ не попадут секреты или исходный код, если он не нужен для runtime).
Пятый аспект — управление ресурсами. Даже оптимизированный образ может съесть всю память, если не установить лимиты. Всегда запускайте контейнеры с флагами, ограничивающими ресурсы:
```
docker run -d --name my-app --memory="512m" --cpus="1.5" my-image
```
Или укажите это в `docker-compose.yml` в секции `deploy.resources.limits`. Это предотвратит «захват» памяти одним контейнером и падение всего хоста.
Шестое — следите за тегами. Избегайте тега `latest` в production. Он плавающий, и сегодня вы запустили один образ, а завтра — другой, что может сломать приложение. Используйте семантическое версионирование (`myapp:v1.2.3`) или хэши коммитов (`myapp:sha-abc123`). Это гарантирует воспроизводимость.
Начинайте применять эти практики с самого первого своего Dockerfile. Оптимизация — это не разовая акция, а культура. Как только вы привыкнете писать эффективные Dockerfile, это станет второй натурой и сэкономит вам и вашей команде часы времени, гигабайты диска и нервы при развертывании.
Комментарии (5)