Каталог антипаттернов
62 записей в восьми группах. Учебная версия с разбором — урок 19.4; здесь тот же каталог в виде справочника для ревью.
Как устроена запись
| Поле | Зачем |
|---|---|
| Антипаттерн | Что именно сделано |
| Почему выглядит разумным | Главное поле: люди не делают заведомо глупого |
| Чем оборачивается | Последствие, а не нарушенное правило |
| Как правильно | Действие, а не пожелание |
Второе поле важнее остальных. Антипаттерн живёт не потому, что о нём не знают, а потому, что он решает настоящую проблему негодным способом. Пока не назван способ лучше, совет «не делайте так» не работает.
Как пользоваться при ревью
- Пройти механически проверяемое линтером — см. security checklist и production checklist.
- Пройти глазами то, что линтер не находит.
- Записать находки с последствием, а не с нарушенным правилом.
- Приоритизировать: риск, делённый на стоимость исправления.
- Назвать не менее двух вещей, сделанных хорошо.
Пятый пункт не вежливость: отчёт из одних находок читается как обвинение и приводит к спору вместо исправлений.
Сборка образов
| № | Антипаттерн | Почему выглядит разумным | Чем оборачивается | Как правильно |
|---|---|---|---|---|
| 1 | FROM ubuntu:latest | Всегда свежий базовый образ | Сборка невоспроизводима: вчера и сегодня разные образы | Конкретный тег, а лучше digest (14.3) |
| 2 | COPY . . до установки зависимостей | Одна инструкция вместо двух | Любое изменение кода сбрасывает кэш установки | Сначала файл зависимостей, потом код (5.7) |
| 3 | RUN apt-get update отдельно от install | Логическое разделение шагов | Кэшированный update даёт установку из устаревшего индекса | Одна инструкция update && install |
| 4 | Кэш пакетного менеджера не удалён в том же слое | Удалили следующей строкой | Файлы остались в предыдущем слое; образ больше | Удаление в той же RUN (3.2) |
| 5 | COPY secret и RUN rm secret | Секрет удалён | Он остаётся в слое и извлекается | RUN --mount=type=secret (12.6) |
| 6 | Отладочные инструменты в финальном образе | Пригодятся при разборе | Площадь атаки растёт; образ тяжелее | Отдельная стадия или временный container (13.5) |
| 7 | ADD для локальных файлов | Делает то же и больше | Распаковывает архивы и качает URL неявно | COPY, а загрузку — явной командой |
| 8 | Нет .dockerignore | Не выглядит обязательным | Контекст раздувается; .git и секреты попадают в образ | .dockerignore с первого дня (5.4) |
| 9 | pip install без фиксации версий | Всегда последние версии | Две сборки подряд дают разные образы | Файл блокировки (6.4) |
| 10 | Сборка в финальном образе | Проще один Dockerfile | Компиляторы и заголовки едут в эксплуатацию | Multi-stage (5.9) |
| 11 | RUN pip install --user от root, затем USER | Кажется безопаснее | Пакеты в домашнем каталоге root, недоступны | Установка после смены пользователя или в общий префикс |
| 12 | Каждая команда — своя RUN | Читается лучше | Слоёв больше, кэш дробится | Группировать связанные операции |
Запуск и жизненный цикл
| № | Антипаттерн | Почему выглядит разумным | Чем оборачивается | Как правильно |
|---|---|---|---|---|
| 13 | CMD python app.py (shell-форма) | Короче и привычнее | PID 1 — оболочка; SIGTERM не доходит до приложения | Exec-форма (4.6) |
| 14 | Запуск от root | Так работает по умолчанию | UID 0 внутри равен UID 0 снаружи (19.2) | USER с числовым идентификатором |
| 15 | USER app вместо USER 10001 | Читается понятнее | Kubernetes не может проверить runAsNonRoot | Числовой UID (18.2) |
| 16 | Несколько процессов под супервизором | Похоже на привычный сервер | Отказ одного не виден снаружи; масштабирование невозможно | Один процесс на container (2.4) |
| 17 | sleep infinity как точка входа | Container перестаёт «падать» | Скрывает отказ; сервис мёртв, container жив | Устранить причину выхода |
| 18 | Нет обработки SIGTERM | Приложение и так завершается | Через 10 секунд SIGKILL; незавершённые операции | Обработчик и мягкое завершение (6.7) |
| 19 | restart: always вместо диагностики | Сервис «сам поднимается» | Цикл падений маскирует причину | Разобрать причину, затем политика перезапуска |
| 20 | tail -f /dev/null рядом с сервисом | Удобно заходить внутрь | То же, что выше: container живёт без сервиса | Отдельный container для отладки |
| 21 | Долгая инициализация без startup-пробы | Работает же | Liveness убивает приложение до готовности | Startup-проба (11.3) |
Данные
| № | Антипаттерн | Почему выглядит разумным | Чем оборачивается | Как правильно |
|---|---|---|---|---|
| 22 | Данные в записываемом слое | Работает без настройки | Исчезают при пересоздании container'а | Том (7.2) |
| 23 | Bind mount каталога хоста в эксплуатации | Файлы видно снаружи | Права, владельцы, привязка к машине | Именованный том |
| 24 | Нет резервного копирования тома | «Данные в Docker» | Docker копированием не занимается (19.1) | Внешняя процедура копирования и проверка восстановления |
| 25 | chmod 777 на томе | Решает проблему прав немедленно | Любой процесс может всё | Совпадающие UID и GID (7.5) |
| 26 | Логи в файл внутри container'а | Как на обычном сервере | Не видны сборщику; растут в слое | stdout и stderr (13.1) |
| 27 | База данных в эксплуатации без ответа «как восстановим» | Compose поднимает её одной строкой | Потеря данных при первом же отказе | Ответить на шесть вопросов (19.1) |
| 28 | Том для кэша, который не жаль потерять | Единообразие | Лишняя сущность в эксплуатации | tmpfs или без тома |
Сеть
| № | Антипаттерн | Почему выглядит разумным | Чем оборачивается | Как правильно |
|---|---|---|---|---|
| 29 | --network host для производительности | Убирает NAT и veth | Изоляция снята целиком (19.1) | Измерить, действительно ли сеть узкое место |
| 30 | Обращение по IP-адресу | Адрес известен и стабилен | Адреса меняются при пересоздании | Обращение по имени сервиса (8.4) |
| 31 | Публикация портов «на всякий случай» | Пригодится для отладки | Сервис доступен снаружи без надобности | Публиковать только нужное |
| 32 | links вместо сетей | Встречается в старых примерах | Устаревший механизм | Пользовательские сети (8.3) |
| 33 | Все сервисы в одной сети | Проще настроить | Сервис базы доступен всем | Раздельные сети (9.4) |
| 34 | Публикация на 0.0.0.0 вместо 127.0.0.1 | Так по умолчанию | Порт открыт наружу, а не только локально | Явный адрес привязки |
Конфигурация и секреты
| № | Антипаттерн | Почему выглядит разумным | Чем оборачивается | Как правильно |
|---|---|---|---|---|
| 35 | ENV DB_PASSWORD=... в Dockerfile | Просто и работает | Виден в docker history и в inspect | Секрет при запуске (12.6) |
| 36 | Секрет через ARG при сборке | Не попадает в ENV | Остаётся в метаданных сборки | RUN --mount=type=secret |
| 37 | .env внутри образа | Конфигурация «едет с приложением» | Секреты в образе; один образ на окружение | Файл монтируется или задаётся окружением |
| 38 | Отдельный образ на каждое окружение | Кажется надёжнее | Проверенный образ и запущенный — разные | Один образ, разная конфигурация (11.2) |
| 39 | Секреты в compose.yaml в репозитории | Удобно и всё в одном месте | Утечка при первом же клонировании | Внешний файл вне репозитория |
| 40 | Конфигурация правится docker exec | Быстрее, чем пересобирать | Изменение исчезнет при пересоздании | Изменить источник конфигурации |
| 41 | Один общий секрет на все сервисы | Меньше сущностей | Компрометация одного равна компрометации всех | Отдельный секрет на сервис |
Compose
| № | Антипаттерн | Почему выглядит разумным | Чем оборачивается | Как правильно |
|---|---|---|---|---|
| 42 | depends_on без condition | Выглядит как порядок запуска | Ждёт запуска, а не готовности | condition: service_healthy и повторы в приложении (9.3) |
| 43 | image: myapp:latest | Всегда актуальная версия | Непонятно, что именно запущено | Конкретный тег или digest |
| 44 | Один файл на все окружения | Меньше файлов | Отладочные настройки попадают в эксплуатацию | Базовый файл плюс override (9.5) |
| 45 | container_name у масштабируемого сервиса | Удобно обращаться по имени | --scale перестаёт работать: имя занято | Обращение по имени сервиса |
| 46 | build: в файле для эксплуатации | Один файл на всё | В эксплуатации собирается вместо запуска готового | Раздельные файлы: сборка и запуск |
| 47 | version: в начале файла | Так во всех старых примерах | Поле устарело и игнорируется | Убрать (9.2) |
| 48 | Тома объявлены, но не в volumes: | Работает и так | Создаётся анонимный том; данные теряются | Объявить именованный том |
Эксплуатация
| № | Антипаттерн | Почему выглядит разумным | Чем оборачивается | Как правильно |
|---|---|---|---|---|
| 49 | Исправление через docker exec | Быстро чинит проблему | Исчезнет при пересоздании; расхождение с образом | Исправить образ и выкатить |
| 50 | Нет ограничений ресурсов | Приложение и так не жрёт | Одна утечка останавливает всю машину | mem_limit и cpus (11.4) |
| 51 | docker system prune -a в эксплуатации | Кончилось место | Удаляет образы, нужные для отката | Целевая очистка с пониманием, что удаляется |
| 52 | Нет healthcheck | Container же запущен | «Запущен» не значит «работает» | Healthcheck (11.3) |
| 53 | Перезаписываемый тег prod | Одно имя, всегда актуальное | Откатиться некуда: старого образа нет | Неизменяемые теги плюс digest |
| 54 | Логи не ограничены по размеру | Драйвер по умолчанию | Диск заполняется логами | Ротация в настройке драйвера (13.1) |
| 55 | Монтирование docker.sock в сервис | Нужно управлять container'ами | Равно правам root на хосте (19.2) | Отдельный агент с ограниченными правами |
| 56 | Сборка образа на машине разработчика | Быстрее, чем ждать CI | Образ невоспроизводим; зависит от машины | Сборка в CI (16.1) |
Организационные
| № | Антипаттерн | Почему выглядит разумным | Чем оборачивается | Как правильно |
|---|---|---|---|---|
| 57 | «Завернём всё в Docker» | Единообразие как ценность | Часть задач не решается контейнеризацией (19.3) | Вопрос: что перестанет работать без Docker |
| 58 | Один человек знает, как собирается образ | Так сложилось | Точка отказа в людях, а не в системе | Dockerfile в репозитории, сборка в CI |
| 59 | Базовые образы не обновляются | Работает — не трогай | Известные уязвимости накапливаются | Регулярная пересборка (12.7) |
| 60 | Сканирование настроено, результаты не читают | Формально требование выполнено | Отчёт без действий равен отсутствию отчёта | Пороги и остановка сборки (16.4) |
| 61 | Копирование Dockerfile из статьи без разбора | Экономит время | Переносятся и антипаттерны из статьи | Понимать каждую инструкцию |
| 62 | Kubernetes «потому что так делают» | Индустриальная практика | Постоянная стоимость без выгоды (18.4) | Чек-лист принятия решения |
Что проверяется механически, а что нет
| Группа | Линтером | Требует разговора |
|---|---|---|
| Сборка образов | Почти всё | «Понимать каждую инструкцию» |
| Запуск и жизненный цикл | Форма CMD, USER, супервизор | Обработка SIGTERM в коде |
| Данные | Отсутствие тома, chmod 777 | Есть ли процедура восстановления |
| Сеть | network_host, links, публикация | Нужен ли порт снаружи |
| Конфигурация и секреты | Секрет в ENV, .env в образе | Один ли секрет на все сервисы |
| Compose | latest, container_name, version | Разделены ли окружения по смыслу |
| Эксплуатация | Лимиты, healthcheck, ротация | Исправления через exec |
| Организационные | Ничего | Всё |
Последняя строка — главная. Шесть организационных антипаттернов наносят наибольший ущерб и не обнаруживаются ни одним инструментом. Линтер с нулём находок не отвечает на вопрос, знает ли кто-нибудь ещё, как собрать этот образ.
Приоритизация находок
риск = последствие × вероятность
приоритет = риск / стоимость исправления
Сводить всё к одной оценке «серьёзности» — обычная ошибка. Тогда chmod 777 в образе для локальной разработки и секрет в ENV эксплуатационного образа получают одинаковый вес, хотя первый почти безвреден, а второй уже произошёл.
| Находка | Последствие | Вероятность | Стоимость | Приоритет |
|---|---|---|---|---|
Секрет в ENV эксплуатации | Высокое | 1 (уже) | Низкая | Наивысший |
Нет .dockerignore | Среднее | Высокая | Почти нулевая | Высокий |
| Нет healthcheck | Среднее | Средняя | Низкая | Высокий |
latest в Compose | Среднее | Средняя | Низкая | Средний |
chmod 777 в образе разработки | Низкое | Низкая | Низкая | Не исправлять |
Последняя строка обязательна в любом отчёте: каталог перечисляет антипаттерны, а не приговоры.
Границы каталога
| Чего здесь нет | Почему |
|---|---|
| Качество кода приложения | Курс про контейнеризацию |
| Архитектурные решения | Контейнеризация их не исправляет |
| Уместность самой контейнеризации | Отдельный вопрос: урок 19.3 |
| Исчерпывающая полнота | 62 записей покрывают частое, не всё |
Навигация
Вернуться к справочникам
Учебная версия с разбором
Security checklist
Production checklist
Карта решений
Главное оглавление