19.3. Когда Docker применять не следует
Цели
После этого материала вы сможете:
- назвать семь классов задач, где контейнеризация не оправдана;
- для каждого объяснить механизм, а не только вывод;
- назвать конкретную альтернативу и то, чем за неё платят;
- отличить «не подходит» от «не умеем готовить»;
- аргументированно отказаться от контейнеризации в конкретном случае;
- назвать случай, где отказ был бы ошибкой.
Предварительные знания
- 19.1. Где containers усложняют систему — механизмы расходов;
- 19.2. Границы изоляции;
- 18.4. Когда нужна orchestration — стоимость инфраструктуры.
Docker на машине курса не установлен. Альтернативы, доступные здесь, измеряются по-настоящему; сравнение с Docker остаётся за читателем.
Ключевые термины
| Термин | Объяснение |
|---|---|
bare metal | Запуск без виртуализации и контейнеризации |
портал | Механизм доступа изолированного приложения к ресурсам рабочего стола |
изменяемое состояние базы | Образ операционной системы, зафиксированный процедурой сертификации |
кратковременная задача | Работа, живущая секунды или минуты |
Теория
Правило отбора
Каждый случай ниже проходит одну проверку:
Что перестанет работать, если убрать Docker?
Если ответ — «ничего, кроме привычки», случай попадает в этот список. Если ответ называет конкретное свойство, которое пропадёт, — не попадает.
Список не про то, что Docker плох. Он про задачи, где решаемой проблемы нет.
1. Жёсткие требования к задержкам
Механизм. Расходы концентрируются в вводе-выводе и сети (урок 19.1): OverlayFS, veth, NAT, фильтр seccomp. Способы их снять отключают изоляцию — то есть то, ради чего container взят.
К этому добавляется то, чего в container'е нет вовсе или что требует привилегий:
| Приём | Состояние в container'е |
|---|---|
| Привязка к ядрам процессора | Требует --cpuset-cpus и совпадения с настройками хоста |
| Большие страницы памяти | Требует настройки хоста и часто привилегий |
| Обход ядра при работе с сетью | Требует --privileged и проброса устройств |
| Настройка планировщика реального времени | Требует CAP_SYS_NICE |
| Изоляция прерываний | Настраивается на хосте, не в container'е |
Пять из пяти настроек делаются на хосте. Container их не приносит, а лишь не мешает — если ему разрешить.
Альтернатива. Запуск на bare metal с настройкой хоста; упаковка через пакет дистрибутива или один самодостаточный файл.
Чем платят. Теряется одинаковость окружения между машинами: настройку придётся воспроизводить средствами управления конфигурацией.
2. Приложения с графическим интерфейсом
Механизм. Графическая подсистема работает через сокет сессии пользователя. Чтобы приложение в container'е нарисовало окно, внутрь пробрасывают:
| Что пробрасывают | Что это открывает |
|---|---|
| Сокет X11 или Wayland | Доступ к сессии рабочего стола |
/dev/dri | Видеоускоритель |
| Сокет звука | Устройства воспроизведения и записи |
| Шину сообщений сессии | Значительную часть рабочего окружения |
| Каталоги пользователя | Файлы пользователя |
К концу списка от изоляции остаётся немного, а зависимость от версии драйвера возвращает привязку к хосту (урок 19.1).
Альтернатива. Flatpak, Snap или AppImage. Существенно, что первые два используют те же механизмы ядра — namespaces, seccomp, cgroups, — но добавляют то, чего у Docker нет: порталы. Портал даёт приложению доступ к выбору файла, буферу обмена, камере через посредника, который спрашивает пользователя, вместо проброса сокета целиком.
Чем платят. Форматов несколько, и упаковывать придётся под каждый; для внутренних инструментов это иногда дороже, чем просто установить пакет.
3. Прямой доступ к оборудованию
Механизм. Устройство пробрасывается, а не виртуализируется. Модуль ядра остаётся на хосте, пользовательская часть — в образе, и версии должны совпадать (урок 19.1).
Отсюда: образ, работающий на одной машине, на другой может не запуститься. Главное обещание не выполняется именно там, где его труднее всего проверить.
Альтернатива. Пакет дистрибутива, устанавливающий и пользовательскую часть, и совместимый модуль.
Чем платят. Возвращаются различия между машинами — ровно та задача, ради которой Docker обычно и берут.
Когда это не аргумент. Если весь парк машин одинаков и драйвер на них один, привязка перестаёт быть проблемой. Именно так работают вычислительные кластеры с ускорителями: там контейнеризация оправдана.
4. Однократные и кратковременные скрипты
Механизм. Стоимость запуска складывается из сборки образа, его хранения и передачи. Для работы, длящейся секунды, эта стоимость превышает саму работу.
Дополнительно: чтобы скрипт что-то сделал, ему нужны данные — значит, монтирование каталогов, значит, права и владельцы файлов (раздел 07).
Альтернатива. Виртуальное окружение — и современный инструментарий делает это почти мгновенно. uv run --with пакет script.py создаёт окружение и запускает скрипт одной командой.
Чем платят. Зависимости берутся из системы; на другой машине с другой версией Python результат может отличаться.
5. Простые статические сайты
Механизм. Задача — отдать файлы. Контейнеризация добавляет к ней образ, слои, публикацию порта и обновление базового образа веб-сервера, не убирая ни одной существующей проблемы.
Что действительно нужно статике: раздача с ближайшего узла, кэширование, сертификат, сжатие. Container не даёт ни одного из четырёх.
Альтернатива. Статический хостинг или раздающая сеть; для своего сервера — веб-сервер, установленный из пакета.
Чем платят. Практически ничем. Это самый чистый случай списка.
Когда это не аргумент. Если сайт собирается сложным конвейером с зафиксированными версиями инструментов, контейнеризация сборки оправдана. Не оправдана контейнеризация раздачи.
6. Команда не готова поддерживать инфраструктуру
Механизм. Стоимость постоянная, а не разовая (урок 18.4): хранилище образов, обновление базовых, кэш, сканирование, очистка. Если этим никто не занимается, система приходит в состояние, худшее исходного: образы годовой давности с известными уязвимостями и никого, кто умеет их пересобрать.
Существенно, что это не довод «команда недостаточно квалифицирована». Это вопрос распределения времени: у команды из трёх человек, поддерживающей один сервис, время лучше тратится на сам сервис.
Альтернатива. Пакеты дистрибутива и systemd (урок 19.1), обновления штатным средством системы.
Чем платят. Нет одинаковости окружения между машинами и нет единообразия доставки.
7. Жёсткие требования к прослеживаемости состава
Механизм. Там, где состав системы зафиксирован процедурой и обновления проходят утверждение, контейнеризация добавляет второй путь обновления: пакеты хоста и содержимое образов. Оба должны быть описаны, проверены и утверждены.
| Что появляется | Что требует |
|---|---|
| Второй набор пакетов — внутри образов | Отдельного учёта и сканирования |
| Изменяемые теги | Закрепления по digest (урок 14.3) |
| Базовые образы от внешнего поставщика | Обоснования доверия (урок 12.7) |
| Слои с удалённым содержимым | Понимания, что удалённое остаётся (урок 12.6) |
Альтернатива. Фиксированный образ операционной системы и установка из проверенного репозитория.
Чем платят. Теряются воспроизводимость сборки и SBOM — а они как раз облегчают прослеживаемость.
Честная оговорка. Это единственный пункт списка, где вывод неоднозначен. Контейнеризация здесь и добавляет работу, и даёт средства её выполнять: SBOM, digest, подписи. Решение зависит от того, описана ли процедура под фиксированный образ системы — если да, менять её дороже, чем не вводить containers.
Чего в списке нет
Полезно назвать и обратное — случаи, которые в список не попадают, хотя их туда часто помещают:
| Случай | Почему не попадает |
|---|---|
| «У нас всего один сервис» | Одинаковость окружения нужна и одному |
| «У нас нет DevOps-инженера» | См. пункт 6: вопрос времени, а не должности |
| «Приложение с состоянием» | Тома решают задачу; вопрос в эксплуатации (урок 19.1) |
| «Нам нужна максимальная производительность» | Расходов на вычислениях практически нет |
| «Мы на Windows» | Механизм иной, но задача решается |
Первая и четвёртая строки встречаются чаще прочих, и обе основаны на неточном представлении о том, где Docker берёт своё.
Внутренний механизм
Почему настройки для низких задержек делаются на хосте
Планировщик, прерывания, большие страницы и привязка к ядрам — свойства ядра, а оно одно на всех (урок 19.2). Container не имеет собственного планировщика и не может настроить чужой.
--cpuset-cpus выглядит исключением, но лишь задаёт cgroup-ограничение поверх настройки хоста: если ядра не изолированы на хосте, на них попадут и другие процессы.
Отсюда общее правило: всё, что относится к поведению ядра, настраивается на хосте, независимо от того, есть container или нет. Контейнеризация в такой задаче не мешает, но и не помогает — при этом расходы на вводе-выводе остаются.
Почему порталы решают задачу, которую проброс сокета не решает
Проброс сокета — решение «всё или ничего»: получив доступ к сокету рабочего стола, приложение может всё, что может пользователь.
Портал — посредник: приложение просит «дай файл», посредник показывает диалог, пользователь выбирает, приложение получает дескриптор одного файла. Доступа к каталогу при этом не появляется.
Разница в том, кто принимает решение: при пробросе — никто, при портале — пользователь в момент запроса.
Docker сокеты рабочего стола пробрасывать умеет, а порталов не имеет — потому что предназначен для серверных задач, где рабочего стола нет.
Команды и примеры
Стоимость запуска кратковременной задачи
mkdir -p /tmp/oneshot && cd /tmp/oneshot
cat > script.py <<'PY'
"""Однократная задача: посчитать строки в файлах."""
from __future__ import annotations
import sys
from pathlib import Path
def main(root: str = ".") -> int:
total = 0
for p in sorted(Path(root).rglob("*.py")):
total += len(p.read_text(encoding="utf-8").splitlines())
print(f"строк Python: {total}")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "."))
PY
echo "═══ альтернатива: виртуальное окружение ═══"
command -v uv >/dev/null && uv --version
python3 --version
echo
echo "── стандартное средство ──"
rm -rf venv-std
/usr/bin/time -f " создание venv: %e с" python3 -m venv venv-std 2>&1 | tail -1
echo "── uv ──"
rm -rf venv-uv
/usr/bin/time -f " создание venv: %e с" uv venv venv-uv 2>&1 | tail -1
echo
echo "── запуск задачи ──"
/usr/bin/time -f " выполнение: %e с" python3 script.py . 2>&1 | tail -2
rm -rf venv-std venv-uv
Ожидаемый вывод (числа зависят от машины):
═══ альтернатива: виртуальное окружение ═══
uv 0.11.6 (x86_64-unknown-linux-gnu)
Python 3.12.3
── стандартное средство ──
создание venv: 5.28 с
── uv ──
создание venv: 0.13 с
── запуск задачи ──
строк Python: 15
выполнение: 0.02 с
Эти числа получены на машине курса. Сравнение с Docker здесь не приводится — он не установлен, и придумывать время сборки было бы подлогом.
Правильный способ достроить сравнение — измерить у себя:
# НЕ ВЫПОЛНЯЛОСЬ: Docker на машине курса отсутствует
time docker build -t oneshot .
time docker run --rm -v "$PWD:/work" -w /work oneshot python script.py
Смысл не в конкретных числах, а в порядке величин: создание окружения занимает доли секунды, а сборка образа — обычно десятки секунд при первом запуске. Для задачи, работающей две сотых секунды, вторая величина и есть вся стоимость.
Разбор случаев
cd /tmp/oneshot
cat > cases.py <<'PY'
"""Семь случаев, где контейнеризация не оправдана."""
from __future__ import annotations
import json
# (случай, механизм, альтернатива, чем платят, однозначен ли вывод)
CASES = [
("Жёсткие требования к задержкам",
"расходы на вводе-выводе; настройки ядра делаются на хосте",
"bare metal + пакет дистрибутива",
"нет одинаковости окружения между машинами", True),
("Приложение с графическим интерфейсом",
"проброс сокетов рабочего стола снимает изоляцию",
"Flatpak, Snap, AppImage — те же namespaces плюс порталы",
"упаковка под несколько форматов", True),
("Прямой доступ к оборудованию",
"passthrough привязывает образ к версии драйвера хоста",
"пакет дистрибутива с модулем и пользовательской частью",
"возвращаются различия между машинами", False),
("Однократный или кратковременный скрипт",
"стоимость сборки превышает саму работу",
"uv run --with, venv, pipx",
"зависимости берутся из системы", True),
("Простой статический сайт",
"задача — отдать файлы; container не решает ни одной из четырёх задач статики",
"статический хостинг или веб-сервер из пакета",
"практически ничем", False),
("Команда не поддерживает инфраструктуру",
"стоимость постоянная: базовые образы, кэш, сканирование, очистка",
"пакеты дистрибутива и systemd",
"нет одинаковости и единообразия доставки", True),
("Жёсткая прослеживаемость состава",
"появляется второй путь обновления, требующий утверждения",
"фиксированный образ системы и проверенный репозиторий",
"теряются воспроизводимость сборки и SBOM", False),
]
# Случаи, которые в список НЕ попадают, хотя их туда часто помещают
NOT_CASES = [
("У нас всего один сервис", "одинаковость окружения нужна и одному"),
("У нас нет DevOps-инженера", "вопрос времени команды, а не должности"),
("Приложение с состоянием", "тома решают задачу; вопрос в эксплуатации"),
("Нужна максимальная производительность",
"расходов на вычислениях практически нет"),
("Мы на Windows", "механизм иной, но задача решается"),
]
def main() -> None:
print(f" {'случай':<40} {'механизм':<62}")
print(" " + "─" * 104)
for case, mech, _, _, _ in CASES:
print(f" {case:<40} {mech:<62}")
print()
print(f" {'случай':<40} {'альтернатива':<46} чем платят")
print(" " + "─" * 132)
for case, _, alt, cost, _ in CASES:
print(f" {case:<40} {alt:<46} {cost}")
unclear = [c for c in CASES if not c[4]]
print()
print(f" случаев: {len(CASES)}, из них вывод НЕоднозначен: {len(unclear)}")
for case, _, _, _, _ in unclear:
print(f" · {case}")
print()
print(f" {'НЕ попадает в список':<40} почему")
print(" " + "─" * 96)
for case, why in NOT_CASES:
print(f" {case:<40} {why}")
print()
print(" Правило отбора: что перестанет работать, если убрать Docker?")
print(" Если ответ — «ничего, кроме привычки», случай в списке.")
print()
print(json.dumps({"случаев": len(CASES), "неоднозначных": len(unclear),
"не_случаев": len(NOT_CASES)}, ensure_ascii=False))
if __name__ == "__main__":
main()
PY
echo "═══ семь случаев ═══"
python3 cases.py
cd /tmp && rm -rf /tmp/oneshot
Ожидаемый вывод:
═══ семь случаев ═══
случай механизм
────────────────────────────────────────────────────────────────────────────────────────────────────────
Жёсткие требования к задержкам расходы на вводе-выводе; настройки ядра делаются на хосте
Приложение с графическим интерфейсом проброс сокетов рабочего стола снимает изоляцию
Прямой доступ к оборудованию passthrough привязывает образ к версии драйвера хоста
Однократный или кратковременный скрипт стоимость сборки превышает саму работу
Простой статический сайт задача — отдать файлы; container не решает ни одной из четырёх задач статики
Команда не поддерживает инфраструктуру стоимость постоянная: базовые образы, кэш, сканирование, очистка
Жёсткая прослеживаемость состава появляется второй путь обновления, требующий утверждения
случай альтернатива чем платят
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Жёсткие требования к задержкам bare metal + пакет дистрибутива нет одинаковости окружения между машинами
Приложение с графическим интерфейсом Flatpak, Snap, AppImage — те же namespaces плюс порталы упаковка под несколько форматов
...
случаев: 7, из них вывод НЕоднозначен: 3
· Прямой доступ к оборудованию
· Простой статический сайт
· Жёсткая прослеживаемость состава
НЕ попадает в список почему
────────────────────────────────────────────────────────────────────────────────────────────────
У нас всего один сервис одинаковость окружения нужна и одному
У нас нет DevOps-инженера вопрос времени команды, а не должности
Приложение с состоянием тома решают задачу; вопрос в эксплуатации
Нужна максимальная производительность расходов на вычислениях практически нет
Мы на Windows механизм иной, но задача решается
Три случая из семи помечены как неоднозначные — и это существеннее самого списка. Прямой доступ к оборудованию перестаёт быть доводом, если парк машин одинаков; статический сайт стоит собирать в container'е, но не раздавать из него; прослеживаемость и усложняется контейнеризацией, и получает от неё средства.
Практическое упражнение
Задание. Постройте инструмент выбора и примените его к пяти задачам.
Требования:
- Описать каждый случай механизмом, а не выводом.
- Для каждого назвать конкретную альтернативу и то, чем за неё платят.
- Отметить случаи, где вывод неоднозначен, и объяснить, от чего он зависит.
- Применить к пяти задачам; хотя бы одна должна получить вывод «Docker оправдан».
- Измерить стоимость альтернативы по-настоящему там, где это возможно.
- Отдельно перечислить доводы, которые звучат похоже, но в список не попадают.
- Отделить измеренное от предполагаемого.
Подсказки
Подсказка 1
Инструмент, отвечающий «не нужен Docker» на любую задачу, бесполезен так же, как инструмент, отвечающий «нужен». Требование 4 — проверка на отрицательном примере.
Подсказка 2
/usr/bin/time -f "%e" даёт время выполнения одной командой, без обвязки.
Подсказка 3
Неоднозначность обычно возникает, когда вывод зависит от условия, которого нет в самой задаче: одинаков ли парк машин, собирается ли артефакт, описана ли процедура.
Решение
Показать решение
mkdir -p /tmp/nodocker && cd /tmp/nodocker
cat > cases.py <<'PY'
"""Выбор между контейнеризацией и альтернативой.
Инструмент отвечает не «да/нет», а «какой случай это, какой механизм
за ним стоит, какая альтернатива и чем за неё платят». Вывод «Docker
оправдан» — полноправный результат, а не отсутствие результата.
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
class Case:
"""Случай, где контейнеризация не оправдана."""
def __init__(self, key: str, name: str, mechanism: str,
alternative: str, price: str, certain: bool,
depends_on: str = "") -> None:
self.key = key
self.name = name
self.mechanism = mechanism
self.alternative = alternative
self.price = price
self.certain = certain
self.depends_on = depends_on
def as_dict(self) -> dict[str, object]:
return {
"ключ": self.key, "случай": self.name, "механизм": self.mechanism,
"альтернатива": self.alternative, "чем_платят": self.price,
"однозначен": self.certain, "зависит_от": self.depends_on,
}
CASES = [
Case("latency", "Жёсткие требования к задержкам",
"расходы на вводе-выводе и сети; настройки ядра делаются на хосте",
"bare metal с настройкой хоста, пакет дистрибутива",
"нет одинаковости окружения между машинами", True),
Case("gui", "Приложение с графическим интерфейсом",
"проброс сокетов рабочего стола снимает изоляцию",
"Flatpak или Snap: те же namespaces плюс порталы",
"упаковка под несколько форматов", True),
Case("hardware", "Прямой доступ к оборудованию",
"passthrough привязывает образ к версии драйвера хоста",
"пакет дистрибутива с модулем и пользовательской частью",
"возвращаются различия между машинами", False,
"одинаков ли парк машин"),
Case("oneshot", "Однократный или кратковременный скрипт",
"стоимость сборки и передачи образа превышает саму работу",
"uv run --with, venv, pipx",
"зависимости берутся из системы", True),
Case("static", "Простой статический сайт",
"задача — отдать файлы; ни одна из задач статики не решается",
"статический хостинг или веб-сервер из пакета",
"практически ничем", False,
"есть ли нетривиальная сборка артефакта"),
Case("nocapacity", "Команда не поддерживает инфраструктуру",
"стоимость постоянная: базовые образы, кэш, сканирование, очистка",
"пакеты дистрибутива и systemd",
"нет одинаковости и единообразия доставки", True),
Case("audit", "Жёсткая прослеживаемость состава",
"появляется второй путь обновления, требующий утверждения",
"фиксированный образ системы и проверенный репозиторий",
"теряются воспроизводимость сборки и SBOM", False,
"описана ли процедура под фиксированный образ системы"),
]
BY_KEY = {c.key: c for c in CASES}
# Доводы, звучащие похоже, но в список не попадающие
NOT_CASES = [
("У нас всего один сервис", "одинаковость окружения нужна и одному"),
("У нас нет DevOps-инженера", "вопрос времени команды, а не должности"),
("Приложение с состоянием", "тома решают задачу; вопрос в эксплуатации"),
("Нужна максимальная производительность",
"расходов на вычислениях практически нет"),
("Мы на Windows", "механизм иной, но задача решается"),
]
# Признаки, при которых контейнеризация ОПРАВДАНА
FOR_DOCKER = [
("много_машин", "несколько машин: одинаковость окружения важна"),
("много_сервисов", "несколько сервисов: единообразие доставки"),
("сложные_зависимости", "зависимости не ставятся из репозитория дистрибутива"),
("нужна_воспроизводимость", "требуется воспроизводимая сборка и SBOM"),
("есть_поддержка", "есть кому поддерживать инфраструктуру сборки"),
]
def classify(task: dict[str, object]) -> dict[str, object]:
matched = [BY_KEY[k] for k in task.get("признаки", []) if k in BY_KEY]
pro = [text for key, text in FOR_DOCKER if task.get(key)]
# Неоднозначные случаи снимаются, если условие выполнено
resolved: list[str] = []
effective: list[Case] = []
for c in matched:
if not c.certain and task.get(f"снято_{c.key}"):
resolved.append(f"{c.name}: снят — {c.depends_on}")
else:
effective.append(c)
if effective and len(effective) >= len(pro):
verdict = "контейнеризация не оправдана"
recommend = effective[0].alternative
elif pro:
verdict = "Docker оправдан"
recommend = "образ по правилам разделов 05 и 11"
else:
verdict = "недостаточно данных"
recommend = "уточнить: сколько машин, сколько сервисов, какие зависимости"
return {
"задача": task["описание"],
"случаи": [c.as_dict() for c in effective],
"снятые": resolved,
"за_docker": pro,
"вердикт": verdict,
"рекомендация": recommend,
}
def main(path: str) -> int:
tasks = json.loads(Path(path).read_text())
result = {
"справочник": [c.as_dict() for c in CASES],
"не_случаи": NOT_CASES,
"неоднозначных": sum(1 for c in CASES if not c.certain),
"задачи": [classify(t) for t in tasks],
}
print(json.dumps(result, ensure_ascii=False))
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "tasks.json"))
PY
cat > tasks.json <<'EOF'
[
{
"описание": "Скрипт выгрузки отчёта, запускается раз в сутки, работает 3 секунды",
"признаки": ["oneshot"],
"много_машин": false,
"сложные_зависимости": false
},
{
"описание": "Настольная утилита с интерфейсом для внутренних пользователей",
"признаки": ["gui"],
"много_машин": false
},
{
"описание": "Обработка видео на ускорителе, парк из 40 одинаковых машин",
"признаки": ["hardware"],
"снято_hardware": true,
"много_машин": true,
"много_сервисов": false,
"сложные_зависимости": true,
"есть_поддержка": true
},
{
"описание": "Лендинг из готовых HTML и CSS без сборки",
"признаки": ["static"],
"много_машин": false
},
{
"описание": "API на Python, шесть сервисов, четыре машины, есть дежурство",
"признаки": [],
"много_машин": true,
"много_сервисов": true,
"сложные_зависимости": true,
"нужна_воспроизводимость": true,
"есть_поддержка": true
}
]
EOF
cat > script.py <<'PY'
"""Кратковременная задача для измерения стоимости запуска."""
from __future__ import annotations
import sys
from pathlib import Path
def main(root: str = ".") -> int:
total = sum(len(p.read_text(encoding="utf-8").splitlines())
for p in sorted(Path(root).rglob("*.py")))
print(f" строк Python: {total}")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "."))
PY
fail=0
ok() { printf ' ✓ %s\n' "$1"; }
bad() { printf ' ✗ %s\n' "$1"; fail=1; }
python3 cases.py tasks.json > result.json
printf '\n═══ Требование 1: механизм, а не вывод ═══\n'
python3 - <<'PY'
import json
d = json.load(open("result.json"))
print(f" {'случай':<40} механизм")
print(" " + "─" * 108)
for c in d["справочник"]:
print(f" {c['случай']:<40} {c['механизм']}")
print()
print(f" случаев: {len(d['справочник'])}")
print()
print(" В каждой строке названа ПРИЧИНА, по которой контейнеризация")
print(" не даёт выигрыша, а не утверждение, что не даёт.")
PY
n_cases="$(python3 -c "import json;print(len(json.load(open('result.json'))['справочник']))")"
[ "$n_cases" -eq 7 ] \
&& ok "семь случаев, каждый описан механизмом" \
|| bad "случаев: $n_cases"
printf '\n═══ Требование 2: альтернатива и её цена ═══\n'
python3 - <<'PY'
import json
d = json.load(open("result.json"))
print(f" {'случай':<38} {'альтернатива':<46} чем платят")
print(" " + "─" * 128)
for c in d["справочник"]:
print(f" {c['случай']:<38} {c['альтернатива']:<46} {c['чем_платят']}")
print()
free = [c for c in d["справочник"] if "практически ничем" in c["чем_платят"]]
print(f" случаев, где отказ почти ничего не стоит: {len(free)}")
for c in free:
print(f" · {c['случай']}")
print()
print(" Столбец «чем платят» обязателен: без него список превращается")
print(" в агитацию против инструмента, а не в разбор задач.")
PY
with_price="$(python3 -c "
import json
d = json.load(open('result.json'))
print(sum(1 for c in d['справочник'] if c['чем_платят']))")"
[ "$with_price" -eq 7 ] \
&& ok "для каждого случая названа цена отказа" \
|| bad "с ценой: $with_price из 7"
printf '\n═══ Требование 3: неоднозначные случаи ═══\n'
python3 - <<'PY'
import json
d = json.load(open("result.json"))
unclear = [c for c in d["справочник"] if not c["однозначен"]]
print(f" {'случай':<38} вывод зависит от")
print(" " + "─" * 96)
for c in unclear:
print(f" {c['случай']:<38} {c['зависит_от']}")
print()
print(f" неоднозначных: {len(unclear)} из {len(d['справочник'])}")
print()
print(" Это существеннее самого списка: прямой доступ к оборудованию")
print(" перестаёт быть доводом при одинаковом парке машин; статический")
print(" сайт стоит СОБИРАТЬ в container'е, но не РАЗДАВАТЬ из него;")
print(" прослеживаемость и усложняется контейнеризацией, и получает")
print(" от неё средства — SBOM, digest, подписи.")
PY
unclear="$(python3 -c "
import json
d = json.load(open('result.json'))
print(sum(1 for c in d['справочник'] if not c['однозначен']))")"
[ "$unclear" -ge 3 ] \
&& ok "неоднозначные случаи выделены с указанием условия" \
|| bad "неоднозначных: $unclear"
printf '\n═══ Требование 4: пять задач, включая обратный случай ═══\n'
python3 - <<'PY'
import json
d = json.load(open("result.json"))
for t in d["задачи"]:
print(f" ── {t['задача']}")
for c in t["случаи"]:
print(f" случай: {c['случай']}")
print(f" механизм: {c['механизм']}")
for s in t["снятые"]:
print(f" СНЯТ: {s}")
for p in t["за_docker"]:
print(f" + {p}")
print(f" ВЫВОД: {t['вердикт']}")
print(f" → {t['рекомендация']}")
print()
verdicts = [t["вердикт"] for t in d["задачи"]]
pro = sum(1 for v in verdicts if v == "Docker оправдан")
print(f" задач: {len(verdicts)}, «Docker оправдан»: {pro}, "
f"различных выводов: {len(set(verdicts))}")
PY
pro="$(python3 -c "
import json
d = json.load(open('result.json'))
print(sum(1 for t in d['задачи'] if t['вердикт'] == 'Docker оправдан'))")"
printf '\n задач с выводом «Docker оправдан»: %s\n' "$pro"
[ "$pro" -ge 1 ] \
&& ok "инструмент способен рекомендовать Docker — значит, отказ что-то значит" \
|| bad "«Docker оправдан»: $pro — инструмент отвечает односторонне"
printf '\n═══ Требование 5: измерение стоимости альтернативы ═══\n'
command -v uv >/dev/null && uv --version | sed 's/^/ /'
python3 --version | sed 's/^/ /'
echo
rm -rf venv-std venv-uv
printf ' стандартное средство: '
/usr/bin/time -f "%e с" python3 -m venv venv-std 2>&1 | tail -1
printf ' uv: '
/usr/bin/time -f "%e с" uv venv venv-uv 2>&1 | tail -1
printf ' сама задача: '
python3 script.py . >/dev/null 2>&1
/usr/bin/time -f "%e с" python3 script.py . 2>&1 | tail -1
rm -rf venv-std venv-uv
echo
echo " Сравнение с docker build здесь НЕ приводится: Docker не установлен,"
echo " а придумывать время сборки было бы подлогом. Достроить сравнение"
echo " можно у себя:"
echo " time docker build -t oneshot . # НЕ ВЫПОЛНЯЛОСЬ"
echo " time docker run --rm oneshot script.py # НЕ ВЫПОЛНЯЛОСЬ"
ok "стоимость альтернативы измерена; сравнение честно оставлено читателю"
printf '\n═══ Требование 6: похожие доводы, не попадающие в список ═══\n'
python3 - <<'PY'
import json
d = json.load(open("result.json"))
print(f" {'довод':<42} почему НЕ попадает")
print(" " + "─" * 100)
for case, why in d["не_случаи"]:
print(f" {case:<42} {why}")
print()
print(f" доводов: {len(d['не_случаи'])}")
print()
print(" Первый и четвёртый встречаются чаще прочих, и оба основаны")
print(" на неточном представлении о том, где Docker берёт своё:")
print(" одинаковость окружения нужна и одной машине, а расходов")
print(" на вычислениях у container'а практически нет.")
PY
n_not="$(python3 -c "import json;print(len(json.load(open('result.json'))['не_случаи']))")"
[ "$n_not" -ge 4 ] \
&& ok "названы доводы, которые звучат похоже, но неверны" \
|| bad "доводов: $n_not"
printf '\n═══ Требование 7: измеренное и предполагаемое ═══\n'
python3 - <<'PY'
MEASURED = [
("время создания venv стандартным средством", "/usr/bin/time"),
("время создания venv через uv", "/usr/bin/time"),
("время выполнения самой задачи", "/usr/bin/time"),
("наличие и версии инструментов", "команды --version"),
]
NOT_MEASURED = [
("время сборки образа", "Docker на машине курса не установлен"),
("расходы на вводе-выводе в container'е", "то же"),
("поведение проброса сокета рабочего стола", "требует сессии и Docker"),
("привязка образа к версии драйвера", "требует оборудования"),
("стоимость поддержки инфраструктуры", "зависит от команды и проекта"),
]
print(f" {'ИЗМЕРЕНО':<44} чем")
print(" " + "─" * 74)
for name, how in MEASURED:
print(f" {name:<44} {how}")
print()
print(f" {'НЕ ИЗМЕРЯЛОСЬ':<44} причина")
print(" " + "─" * 84)
for name, why in NOT_MEASURED:
print(f" {name:<44} {why}")
print()
print(f" измерено: {len(MEASURED)}, не измерялось: {len(NOT_MEASURED)}")
print()
print(" Механизмы в справочнике — утверждения о том, как устроены")
print(" OverlayFS, сеть, passthrough и порталы. Они опираются")
print(" на разделы 08, 17 и документацию, а не на замеры здесь.")
PY
ok "измеренное отделено от утверждаемого"
printf '\n═══ ИТОГ ═══\n'
[ "$fail" -eq 0 ] && echo " все требования выполнены" || echo " ЕСТЬ ПРОВАЛЫ"
echo " примечание: Docker не использовался; измерена только альтернатива"
cd /tmp && rm -rf /tmp/nodocker
exit "$fail"
Ожидаемый вывод (значения времени зависят от машины и от прогрева кэша):
═══ Требование 1: механизм, а не вывод ═══
случай механизм
────────────────────────────────────────────────────────────────────────────────────────────────────────────
Жёсткие требования к задержкам расходы на вводе-выводе и сети; настройки ядра делаются на хосте
Приложение с графическим интерфейсом проброс сокетов рабочего стола снимает изоляцию
Прямой доступ к оборудованию passthrough привязывает образ к версии драйвера хоста
Однократный или кратковременный скрипт стоимость сборки и передачи образа превышает саму работу
Простой статический сайт задача — отдать файлы; ни одна из задач статики не решается
Команда не поддерживает инфраструктуру стоимость постоянная: базовые образы, кэш, сканирование, очистка
Жёсткая прослеживаемость состава появляется второй путь обновления, требующий утверждения
случаев: 7
В каждой строке названа ПРИЧИНА, по которой контейнеризация
не даёт выигрыша, а не утверждение, что не даёт.
✓ семь случаев, каждый описан механизмом
═══ Требование 3: неоднозначные случаи ═══
случай вывод зависит от
────────────────────────────────────────────────────────────────────────────────────────────────
Прямой доступ к оборудованию одинаков ли парк машин
Простой статический сайт есть ли нетривиальная сборка артефакта
Жёсткая прослеживаемость состава описана ли процедура под фиксированный образ системы
неоднозначных: 3 из 7
✓ неоднозначные случаи выделены с указанием условия
═══ Требование 4: пять задач, включая обратный случай ═══
── Скрипт выгрузки отчёта, запускается раз в сутки, работает 3 секунды
случай: Однократный или кратковременный скрипт
механизм: стоимость сборки и передачи образа превышает саму работу
ВЫВОД: контейнеризация не оправдана
→ uv run --with, venv, pipx
── Настольная утилита с интерфейсом для внутренних пользователей
случай: Приложение с графическим интерфейсом
механизм: проброс сокетов рабочего стола снимает изоляцию
ВЫВОД: контейнеризация не оправдана
→ Flatpak или Snap: те же namespaces плюс порталы
── Обработка видео на ускорителе, парк из 40 одинаковых машин
СНЯТ: Прямой доступ к оборудованию: снят — одинаков ли парк машин
+ несколько машин: одинаковость окружения важна
+ зависимости не ставятся из репозитория дистрибутива
+ есть кому поддерживать инфраструктуру сборки
ВЫВОД: Docker оправдан
→ образ по правилам разделов 05 и 11
── Лендинг из готовых HTML и CSS без сборки
случай: Простой статический сайт
механизм: задача — отдать файлы; ни одна из задач статики не решается
ВЫВОД: контейнеризация не оправдана
→ статический хостинг или веб-сервер из пакета
── API на Python, шесть сервисов, четыре машины, есть дежурство
+ несколько машин: одинаковость окружения важна
+ несколько сервисов: единообразие доставки
+ зависимости не ставятся из репозитория дистрибутива
+ требуется воспроизводимая сборка и SBOM
+ есть кому поддерживать инфраструктуру сборки
ВЫВОД: Docker оправдан
→ образ по правилам разделов 05 и 11
задач: 5, «Docker оправдан»: 2, различных выводов: 2
задач с выводом «Docker оправдан»: 2
✓ инструмент способен рекомендовать Docker — значит, отказ что-то значит
═══ Требование 5: измерение стоимости альтернативы ═══
uv 0.11.6 (x86_64-unknown-linux-gnu)
Python 3.12.3
стандартное средство: 5.28 с
uv: 0.13 с
сама задача: 0.02 с
Сравнение с docker build здесь НЕ приводится: Docker не установлен,
а придумывать время сборки было бы подлогом.
✓ стоимость альтернативы измерена; сравнение честно оставлено читателю
═══ ИТОГ ═══
все требования выполнены
примечание: Docker не использовался; измерена только альтернатива
Все требования выполнены. Числа времени получены на машине курса.
Три решения, определяющие качество.
Инструмент умеет отвечать «Docker оправдан». Две задачи из пяти получили именно этот вывод, причём одна — за счёт снятия неоднозначного случая: обработка видео на ускорителе попадает в пункт «прямой доступ к оборудованию», но парк из сорока одинаковых машин снимает привязку к драйверу, и контейнеризация становится оправданной. Без такого исхода список превратился бы в агитацию: инструмент, отвечающий «не нужен» на любой вопрос, ничего не сообщает.
Три случая из семи помечены неоднозначными, с указанием условия. Соблазн был свести всё к однозначным правилам — так список выглядит убедительнее. Но именно неоднозначные случаи полезны на практике: они говорят, какой вопрос задать, а не какой ответ выбрать. «Одинаков ли парк машин» решает больше, чем сам факт наличия ускорителя.
Сравнение с Docker не достроено. Измерены только альтернативы: 5,28 с и 0,13 с на создание окружения, 0,02 с на работу. Время docker build не измерено и не оценено — Docker не установлен. Приписать сюда «а образ собирается 40 секунд» было бы подлогом, и вся ценность урока о честности исчезла бы вместе с этим числом.
Чего решение не делает. Механизмы в справочнике — утверждения, а не результаты замеров: OverlayFS, veth, passthrough и порталы описаны по документации и разделам 08 и 17. Ни один из семи случаев не воспроизведён: проброс сокета рабочего стола, привязка к версии драйвера и расходы на вводе-выводе требуют Docker и оборудования. Правило выбора вердикта — сравнение числа доводов — грубое: доводы разного веса, и на пограничной задаче итог зависит от того, как её описали. Наконец, пункт про прослеживаемость состава остаётся самым спорным: он верен для процедур, определённых под фиксированный образ системы, и неверен там, где процедура допускает образы и требует SBOM.
Проверка результата
command -v uv && uv --version
/usr/bin/time -f "%e с" uv venv /tmp/probe && rm -rf /tmp/probe
Для своей задачи ответьте на два вопроса: что перестанет работать, если убрать Docker и чем придётся заплатить за отказ. Если на первый ответа нет, а на второй — «ничем», решение принято.
Типичные ошибки
| Ошибка | Причина | Исправление |
|---|---|---|
| Контейнеризовать однократный скрипт | Единообразие | Стоимость запуска превышает работу |
| Раздавать статику из container'а | Привычка | Container не решает ни одной задачи статики |
| Пробрасывать сокет рабочего стола | Единственный способ | От изоляции остаётся немного; есть Flatpak |
| Ждать от container'а низких задержек | Кажется лёгким | Настройки ядра делаются на хосте |
| Считать GPU-образ переносимым | Обещание «работает везде» | Привязка к версии драйвера |
| Отказываться из-за одного сервиса | Кажется избыточным | Одинаковость окружения нужна и одному |
| Отказываться ради производительности | Аналогия с ВМ | Расходов на вычислениях практически нет |
| Считать список безусловным | Так проще | Три случая из семи зависят от условия |
| Внедрять без ответа «кто поддерживает» | Кажется разовой работой | Стоимость постоянная |
Контрольные вопросы
На понимание:
- Какой вопрос отбирает случаи в этот список?
- Почему настройки для низких задержек делаются на хосте?
- Чем портал отличается от проброса сокета?
- Почему статический сайт не выигрывает от контейнеризации?
- Какие три случая неоднозначны и от чего зависит вывод?
На применение:
- Какая альтернатива у однократного скрипта и чем за неё платят?
- Когда прямой доступ к оборудованию перестаёт быть доводом против?
- Что нужно выяснить, прежде чем отказываться из-за прослеживаемости?
На диагностику:
- Команда отказалась от Docker «ради производительности». Какой вопрос задать?
- Настольное приложение в container'е работает, но видит все файлы пользователя. Что произошло?
Краткое резюме
- Правило отбора: что перестанет работать, если убрать Docker.
- Семь случаев, и в трёх из них вывод зависит от дополнительного условия.
- Низкие задержки: настройки ядра делаются на хосте, container их не приносит.
- Графический интерфейс: проброс сокетов снимает изоляцию; Flatpak даёт порталы.
- Оборудование: passthrough привязывает образ к версии драйвера на хосте.
- Привязка перестаёт быть проблемой, если парк машин одинаков.
- Однократные скрипты: стоимость сборки превышает саму работу.
- Статический сайт стоит собирать в container'е, но не раздавать из него.
- Отсутствие людей на поддержку инфраструктуры — вопрос времени команды, а не должностей.
- Прослеживаемость состава контейнеризация и усложняет, и снабжает средствами.
- «Один сервис» и «нужна производительность» — доводы, основанные на неточных представлениях.
- Отказ от Docker должен называть альтернативу и цену, иначе это не решение.
Официальные источники
| Источник | Ссылка | Что подтверждает |
|---|---|---|
| Docker: драйверы хранилища | https://docs.docker.com/storage/storagedriver/ | Расходы на файловой системе |
| Docker: сеть | https://docs.docker.com/network/ | Bridge, NAT, --network host |
| Docker: GPU | https://docs.docker.com/desktop/features/gpu/ | Требования к драйверу хоста |
| Flatpak: песочница | https://docs.flatpak.org/en/latest/sandbox-permissions.html | Namespaces и права |
| Flatpak: порталы | https://docs.flatpak.org/en/latest/desktop-integration.html | Доступ через посредника |
| uv | https://docs.astral.sh/uv/ | uv run --with, скорость создания окружений |
| pipx | https://pipx.pypa.io/stable/ | Изолированная установка приложений |
| Kernel: cpusets | https://docs.kernel.org/admin-guide/cgroup-v1/cpusets.html | Привязка к ядрам |
| Kernel: hugetlbpage | https://docs.kernel.org/admin-guide/mm/hugetlbpage.html | Большие страницы памяти |
Навигация
← Предыдущий материал
Вернуться к разделу
Следующий материал → Каталог антипаттернов
Главное оглавление