Главная/О курсе и репозитории/Документ

Статус создания курса

Файл отражает готовность материалов курса, а не личный прогресс учащегося. Личный прогресс отмечается в таблице «Прогресс курса» в MAIN.md.

Обновляется после завершения каждого раздела.

Дата последнего обновления: 2026-07-31


Правила отметки готовности

Раздел получает статус «Готов» только когда выполнены все условия:

  1. Созданы все запланированные файлы раздела.
  2. Проверены relative links (каждая ссылка указывает на существующий файл).
  3. Проверены команды на синтаксическую корректность и применимость в Linux.
  4. Проверен source code примеров (Python компилируется, YAML парсится, Dockerfile валиден).
  5. Добавлены exercises.
  6. Добавлены официальные источники с прямыми ссылками.

Статусы: Готов / В работе / Не начат.


Сводная таблица

РазделФайлы созданыLinks провереныCode проверенСтатус
Корень (MAIN.md, README.md, COURSE_STATUS.md)3/3Готов
00. Обзор курса5/5Готов
01. Подготовка среды8/8Готов
02. Основы Containerization9/9Готов
03. Работа с Images7/7Готов
04. Containers и lifecycle8/8Готов
05. Dockerfile11/11Готов
06. Python внутри Container15/15Готов
07. Storage8/8Готов
08. Networking8/8Готов
09. Docker Compose9/9Готов
10. Development workflow7/7Готов
11. Production8/8Готов
12. Security9/9Готов
13. Observability8/8Готов
14. Registry6/6Готов
15. Testing7/7Готов
16. CI/CD7/7Готов
17. Docker internals7/7Готов
18. Docker и Kubernetes6/6Готов
19. Ограничения Docker6/6Готов
Projects13/13Готов
Assessments7/7Готов
References15/15Готов
Resources (diagrams + source links)8/8Готов
Resources: рабочие примеры12/12Готов
Итого225 md (план — 208) + 151 файл кодаГотов

Курс завершён. Все разделы, проекты, материалы проверки, справочники, диаграммы и рабочие примеры на месте.

Файлов больше планового числа (225 против 208): в план не входили README.md четырёх последних примеров и шесть файлов СИМПТОМ.md в debug-scenarios.

Результаты фактической проверки (2026-08-04)

Проверка выполняется скриптом validate_all.py при завершении каждого раздела, а не декларативно.

Что проверялосьМетодРезультат
Относительные ссылкиобход всех .md, разрешение пути от каталога файла2498 ссылок указывают на существующие файлы
Якоря (#заголовок)сверка со списком заголовков целевого файла0 битых якорей
Ссылки на ненаписанные файлытот же обход320 ссылок ведут на файлы со статусом «Не начат»; 0 вне планового состава
Shell-примерыbash -n для каждого скрипта с шебангом41 из 41 без синтаксических ошибок
Python в блоках ```pythoncompile()42 из 42 валидны
Python внутри heredocизвлечение из shell-блоков; тег heredoc обязан быть в кавычках563 из 563 валидны; 18 незакавыченных пропущено
JSON-примерыjson.loads, с поддержкой JSON Lines28 из 28 в блоках ```json + 13 из 13 внутри heredoc
YAML-примерызагрузчик с тегами Compose (!reset, !override)111 из 111 в блоках ```yaml + 164 из 164 внутри heredoc
Dockerfile-блокиблок из 4+ инструкций обязан начинаться с FROM/ARG; heredoc не считается набором инструкций130 блоков: 57 собираемых, 73 иллюстрации синтаксиса, 0 подозрительных
Служебные артефактыпоиск остатков разметки инструментов0 найдено
Парность HTML-тегов<details> и <summary>0 непарных
Команды Dockerсверка с официальной документацией (июль 2026)выполнено для разделов 00–11
Код рабочих примеровcompile(), tomllib, yaml, первая инструкция Dockerfile22 Python / 4 TOML / 3 YAML / 8 Dockerfile — валидны
Тесты примеровфактический прогон pytest20 тестов пройдено (python-cli 9, pytest-container 11)
Версии пакетовсверка с PyPI и Docker Hub11 версий подтверждены

Объём готового материала: 175 файлов Markdown (около 684 000 слов) и 77 файлов кода в восьми рабочих примерах.

Что нашла и исправила проверка:

  • невалидный JSON-блок в 03-images/01-image-architecture.md — пара «ключ: значение» внутри массива;
  • битый якорь из-за неоднозначного слага с длинным тире: разные Markdown-рендереры дают -разные и --разные. Заголовок переименован так, чтобы слаг совпадал везде;
  • четыре фрагмента Dockerfile без FROM — дополнены до собираемых, чтобы читатель мог проверить утверждение сам;
  • два heredoc-примера без директивы # syntax=docker/dockerfile:1 — без неё heredoc не распознаётся и сборка падает;
  • служебный артефакт разметки инструмента, попавший в текст урока 5.8;
  • устаревшие версии пакетов в 8 файлах: приводились как актуальные fastapi==0.120.1, uvicorn==0.38.0, pytest==8.4.2, pydantic==2.12.3, httpx==0.29.0, ruff==0.15.1. Сверены с PyPI и заменены на реальные текущие;
  • два ложных срабатывания самого валидатора — правила уточнены (см. ниже);
  • при написании раздела 06 детектор смешанных алфавитов поймал ещё три опечатки вида оverhead и пytest — латинские буквы внутри кириллических слов и наоборот. Такие слова выглядят нормально, но не находятся поиском.

Что изменилось в разделе 06 по результатам сверки с документацией. Урок 6.10 был запланирован вокруг связки Gunicorn с UvicornWorker. Текущая документация FastAPI называет её устаревшей и рекомендует fastapi run, а образ tiangolo/uvicorn-gunicorn-fastapi объявлен deprecated. Урок переписан под актуальную рекомендацию; для Flask Gunicorn остаётся правильным выбором, поскольку это WSGI.

Что нашла проверка в разделе 07. Ссылка на ../07-volumes-and-data/MAIN.md — каталога с таким именем не существует (раздел называется 07-storage). Валидатор её пропускал: ссылка на несуществующий файл выглядела как обычная ссылка на ещё не написанный материал, а таких в скелете сотни. Добавлена сверка с плановым составом разделов из этого файла: ссылка на несозданный файл законна, только если файл в плане либо его каталог существует. Правило самопроверено на трёх случаях — плановый файл, существующий файл, опечатка в каталоге.

Второе уточнение: детектор смешанных алфавитов срабатывал на \nзначение внутри Python-строк, где n принадлежит escape-последовательности. Правило теперь не начинает слово сразу после обратного слэша.

Что нашла проверка в разделе 11. Детектор служебных артефактов сработал на моей собственной разметке инструмента, попавшей в текст урока 11.6, — ровно тот случай, ради которого он добавлялся после инцидента в уроке 5.8. Детектор смешанных алфавитов поймал ещё одну опечатку (slив). Оба правила работают без изменений.

Отдельно о структуре раздела 15. Уроки 15.3 и 15.4 описывают альтернативные подходы к одной задаче — Compose и Testcontainers. В обоих реализован один и тот же набор тестов над одним и тем же кодом: так разница видна на сравнении, а не на описании. Урок 15.4 заканчивается таблицей выбора, проверенной на двух проектах с противоположными вердиктами.

Новое в разделе, чего нет в других: стадия test в multi-stage сборке не выполняется, если на неё нет ссылок, — сборка при этом успешна; docker compose up возвращает 0 при упавших тестах; pg_isready сообщает «готов» раньше, чем проходит запрос; Ryuk удаляет объекты после kill -9, где trap не срабатывает в принципе; статическая проверка USER не отличает «пользователь объявлен» от «пользователь существует».

Отдельно о структуре раздела 16. Раздел построен вокруг одного проектного решения, проходящего через все пять уроков: логику проверок выносят в скрипты ci/*.sh, а не пишут в шагах YAML. Из него следует три вещи, каждая проверена отдельно: pipeline отлаживается локально (цикл в секунды вместо минут через push), шесть элементов из одиннадцати переносятся между платформами механически, и те же скрипты служат проверкой образа в уроке 15.5.

Новое в разделе: порядок этапов выведен сортировкой по время / вероятность отказа и проверен полным перебором 120 перестановок; RUN --mount=type=cache в CI не работает вовсе, потому что cache-to экспортирует слои, но не содержимое cache mount; mode=min — значение по умолчанию — теряет стадию с зависимостями, и это самая частая причина «кэш настроен, а сборка медленная»; результат сканирования меняется без изменения образа, поэтому нужен третий код возврата, отличающий «не проверено» от «чисто».

Что нашла проверка в разделе 16. Валидатор неверно обрабатывал heredoc с незакавыченным тегом. <<'PY' и <<PY — разные вещи: во втором случае тело подставляет оболочка, и статически оно невалидно. Прежнее правило принимало кавычки за необязательные и компилировало такое тело как есть, выдавая ложные ошибки синтаксиса. Теперь кавычки тега — значащий признак: без них блок пропускается с отдельным счётчиком в отчёте. Самопроверено на четырёх случаях: одинарные кавычки, двойные кавычки, без кавычек, сломанный Python в кавычках.

Заодно выяснилось, что незакавыченный heredoc — приём хрупкий: любой $ в теле Python ломает код. В уроке 16.3 четыре таких блока переписаны на передачу значений через переменные окружения, в уроке 14.2 два блока просто закрыты кавычками — они и так читали окружение. Проверяемых heredoc стало 425; оставшиеся 18 незакавыченных — не Python.

Что нашла проверка в разделе 15. Самая полезная находка за все раунды. В уроке 15.1 я написал код Python с идентификаторами на кириллице — class SqlХранилище, def зарегистрировать(хранилище, ...). Детектор смешанных алфавитов дал тринадцать срабатываний на SqlХранилище, и это заставило перепроверить соглашение. Оказалось, что во всех 44 предыдущих уроках идентификаторы латиницей, а русский — только в комментариях, документирующих строках и выводе программ. Урок переписан целиком. Заодно вскрылась синтаксическая ошибка в heredoc решения, которую я бы иначе не заметил.

Ещё две находки того же раздела. Валидатор неверно разбирал пустой heredoc: регулярное выражение требовало непустого тела, и на идиоматичном пустом __init__.py склеивало два соседних блока в один, выдавая ложную ошибку про from __future__. Тело теперь описано как «ноль или более полных строк, затем тег на своей строке»; самопроверено на трёх случаях. И третье: число файлов Markdown внезапно выросло на четыре — это оказались README.md из каталогов .pytest_cache, созданных при фактическом прогоне тестов в примерах. Каталоги удалены, .gitignore добавлен во все восемь примеров, а валидатор теперь исключает служебные каталоги инструментов из обхода.

Вывод, который стоит записать: правило про смешанные алфавиты добавлялось для ловли опечаток вида пytest, но оно же поймало нарушение соглашения об именовании — то, ради чего не писалось. В урок 15.1 добавлена явная строка о соглашении перед первым блоком кода, а в «Типичные ошибки» — строка про смешение алфавитов в идентификаторе.

Проверка на настоящем Docker (2026-08-04). Docker Engine 29.7.1 установлен; поднят кластер kind v1.36.1 из трёх узлов; поставлены trivy, syft, cosign, hadolint, kind. Полный отчёт — verify/FACTS.md, воспроизводится скриптами verify/*.sh.

Найдено и исправлено 40 дефектов. Три из первых тринадцати меняли содержание курса:

Код возврата после docker stop — 0, а не 143. Курс утверждал обратное в восьми местах и при этом противоречил сам себе: урок 4.4 объяснял 143 как «завершение по действию по умолчанию без обработчика», а урок 4.5 учит, что для PID 1 действие по умолчанию не применяется. Замер подтвердил урок 4.5: без обработчика — 10 секунд и 137. Ошибка возникла из замера вне container'а, где uvicorn не PID 1.

fastapi run ломает машинно разбираемый лог. 7 не-JSON строк из 10 против 0 из 9 у python -m uvicorn. Причин две, вторая неочевидна: баннер печатается до импорта приложения, а логирование uvicorn перенастраивается после импорта, отменяя программную настройку. Точка входа в двух примерах заменена, в урок 6.10 добавлен разбор размена.

time namespace описан неверно. Он не хостовый, но одинаков у всех container'ов: своего container не получает.

Ещё восемь: кириллица в именах container'ов запрещена демоном (все такие примеры не работали бы); выдуманный digest не собирался; оговорка про размер образа сначала была «опровергнута» ошибочно — см. ниже; утверждение про NetworkPolicy оказалось слишком сильным (на kindnet политика действует); ReadWriteOnce отказывает не с Multi-Attach error, а с конфликтом nodeAffinity при планировании; 37 абсолютных путей машины автора; три дефекта в скриптах конвейера.

Прогон проектов, упражнений, всех блоков «Ожидаемый вывод» и сплошная проверка разделов 01–04 (2026-08-05) добавили ещё 27 дефектов — итого 40. Все четыре одного рода: код или конфигурация, которые никогда не выполнялись так, как выполняются в образе.

.dockerignore исключал каталог, который копирует стадия test — системно. Пять рабочих примеров из шести никогда не собирали свою стадию test: docker build --target test падал с "/tests": not found. Собирались только стадии runtime, поэтому дефект и дожил до прогона. Тот же дефект нашёлся в упражнениях уроков 5.5, 6.3, 6.8 и 6.10 и в SOLUTION.md проекта 2 — причём в трёх местах напечатанный «Ожидаемый вывод» содержал строку «тесты прошли», получить которую было нельзя. Исключение при этом ничего не давало: ни одна стадия runtime в курсе не делает COPY . .. В урок 5.1 добавлен раздел «.dockerignore действует на все стадии» с воспроизведением.

pytest и python -m pytest дают разный sys.path. Всплыло сразу после исправления предыдущего: консольная команда не кладёт текущий каталог в sys.path, и from app.main import app не находит пакет. Курс этого различия не объяснял.

Проект 2: три требования из одиннадцати не выполнялись. Кроме стадии test — точка входа fastapi run давала 15 не-JSON строк из 20 при требовании «каждая строка JSON», а функция load(), отвергающая опечатку в имени переменной, была написана, покрыта тестом и не вызывалась приложением. Общее у всех трёх: тест проверял тот путь, который сам и вызывал. Чек-лист прогнан целиком — verify/61-project2.sh, 27 подтверждений из 27; образ собирается из блоков SOLUTION.md, то есть проверяется ровно то, что напечатано.

Размер образа мерился не той величиной — и я сам на этом ошибся. Под containerd image store docker images --format '{{.Size}}' возвращает DISK USAGE, а docker image inspect --format '{{.Size}}'CONTENT SIZE; для образа FastAPI это 293 MB против 69 MB, разница вчетверо. Первая редакция отчёта записала «оговорка про 200 MB опровергнута, фактически 66 MB» — число верное, величина не та. По занятому месту порог 200 MB не только не выполняется (293 MB), но и недостижим: один python:3.13-slim занимает 178 MB. В урок 3.4 добавлен разбор двух чисел; ориентир проекта 2 переписан на «меньше 350 MB по DISK USAGE»; в рубрике, production-checklist и системе оценивания указано, какой командой мерить.

make lock в примере multistage-uv не работал. Образ ghcr.io/astral-sh/uv содержит только бинарник uv, и запущенный отдельно он не может определить libc: Failed to find any common binaries to determine libc from: /bin/sh, /usr/bin/env, /bin/dash, /bin/ls. Без uv.lock не собирался и сам пример. Заодно исправлено, что файл создавался с владельцем root.

Проверка, которая не могла провалиться. В упражнении урока 6.10 требование «все строки лога — JSON» проверялось командой docker logs | grep '^{' — фильтр отбрасывал не-JSON строки до подсчёта. Заменено на подсчёт всех строк.

Проект 3: стек не поднимался вовсе — шесть дефектов. Чек-лист прогнан целиком (verify/62-project3.sh, 35 из 35, включая десять запусков подряд); стек собирается из блоков SOLUTION.md проектов 2 и 3.

Три дефекта не давали стартовать. Главный: PGPASSWORD_FILE не читал никто — libpq знает PGPASSWORD и PGPASSFILE, но не PGPASSWORD_FILE; соглашение «<ИМЯ>_FILE» реализует entrypoint образа postgres, и только для себя. Стек падал на fe_sendauth: no password supplied. Второй: compose.yaml передавал EVENTAPI_DATABASE_URL и EVENTAPI_REDIS_URL, которых не было в модели настроек, — строгая проверка имён из проекта 2 отвергала их, и это был правильный отказ. Третий: стадия test прогоняла тесты, которым нужна база, а у стадии сборки нет доступа к сети Compose — покрытие складывалось из одних пропусков (20 %) и сборка падала.

Ещё три: --cov=app считал файлы проекта 2 без их тестов (40 % вместо 80 %); у сервиса tests не было EVENTAPI_REQUIRE_DB=1, то есть недоступная база дала бы зелёный прогон из пропусков; формулировка про internal: true не учитывала, что internal — свойство сети, а не сервиса: api состоит ещё и во frontend, и выход в интернет у него есть.

Тот же дефект — в примере production-fastapi. Прогон поведенческих проверок повис на опечатке в имени переменной: create_app вызывал Settings() вместо load(), и сервис с EVENTAPI_LOG_LEVE=info работал, пока его не убьют по таймауту. Пример и эталонное решение проекта 2 писались по одному образцу, и ошибка размножилась вместе с ним; остальные примеры проверены — копий больше нет. В прогонщик добавлен timeout: проверка, способная повиснуть, не отличается от непройденной.

Полный прогон блоков «Ожидаемый вывод» (2026-08-05). Прогнаны все блоки курса, где за командой следует блок вывода: 250 совпало, 305 разошлось, 37 ошибок, 85 не выполнялись (sudo, разрушительные, интерактивные). Разбирать 305 расхождений глазами по одному не нужно: verify/52-triage.py раскладывает их по причинам, видным из самого вывода, и оставляет остаток — 68 штук, прочитанных целиком.

Дефектами из 305 расхождений оказались шесть, то есть 2 %. Правило «расхождение — ещё не дефект» подтвердилось на цифрах. Но эти 2 % включают два неверных утверждения о безопасности:

--cap-add=SYS_ADMIN не включает mount. Урок утверждал «заработало, одна capability изменила результат». На Ubuntu операцию запрещает AppArmor независимо от capabilities; заработало только с --security-opt apparmor=unconfined. Переписано на более важный вывод: ограничения складываются, снятие одного ничего не гарантирует.

--cap-drop=NET_BIND_SERVICE не защищает привилегированные порты. Ожидался PermissionError, порт занялся. Docker выставляет внутри container'а net.ipv4.ip_unprivileged_port_start=0, и привилегированных портов там просто нет — capability ни на что не влияет. Проверено: порт 80 занимается под uid 10001 с --cap-drop=ALL.

ps нет в python:*-slim — и это давало правдоподобный ноль. ps -o args= \| grep -c gunicorn печатал 0 вместо 3: не ошибку, а неверное число. Найдено в семи местах; везде заменено на чтение /proc. В alpine ps есть, но busybox не знает -p.

ARG до FROM подставляет значение базового образа. Урок обещал пустую строку; подставилось 3.13.14ENV PYTHON_VERSION из python:3.13-slim. На alpine было бы пусто: поведение зависит от базы. Там же исправлено утверждение про приоритет ENV над ARG — оно верно только при порядке ARGENV, что измерено на трёх вариантах.

--init собирает не всех zombie. Урок обещал ноль; фактически один — тот, чей родитель само приложение, а не PID 1. Три прогона дают одно и то же.

tail на ошибке docker run ловит подсказку, а не ошибку. Docker 29 печатает ошибку, пустую строку и Run 'docker run --help'. Четыре блока показывали ожидаемый текст ошибки, а давали подсказку.

Плюс устаревшие числа: python:3.13-slim пересобран на Debian trixie — 9 шагов истории вместо 12, 178 MB вместо 126 MB. Обновлено в шести местах с оговоркой, что смотреть нужно на доли.

Сплошная проверка разделов 01–04 (2026-08-05). 32 файла, 650 bash-блоков, 10 dockerfile-блоков — проверены четырьмя способами вдобавок к прогону «Ожидаемого вывода»: сборка каждого dockerfile-блока (72-dockerfiles.py, 8 из 8), сверка всех флагов Docker с установленным Docker (71-flags.py, 752 употребления) и выполнение блоков, за которыми нет «Ожидаемого вывода» (73-silent-blocks.py, 279 блоков).

Третья проверка новая по смыслу: прогонщик блоков такие места пропускал — сверять не с чем. Но критерий есть, и это код возврата. Именно она дала самый неприятный дефект: docker pull принимает ровно один образ, а многообразный docker pull -q A B C стоял в блоке «Подготовка» девяти файлов exercises.md. Первая команда, которую читатель выполняет в разделе, не работала — и пережила все предыдущие проверки, потому что за ней не было блока вывода.

Ещё шесть: .NetworkSettings.IPAddress в Docker 29 нет (отказ шаблонизатора вместо адреса, три места); docker inspect КОНТЕЙНЕР --format '{{json .GraphDriver.Data}}' — то же самое, причём урок оговаривал containerd для образов, но не для container'ов; docker run ОБРАЗ --version аргументы Cmd не дополняет, а заменяет — урок 3.1 утверждал обратное и противоречил уроку 5.4; выдуманный digest sha256:9f8e7d6c… в восьми файлах, в том числе в FROM и compose.yaml; --keep-storage переименован в --reserved-space; плейсхолдер <container-id> в исполняемом блоке урока 1.4 даёт синтаксическую ошибку оболочки.

Уборка после раздела удаляла все container'ы машины. Строка docker ps -aq | xargs -r docker rm -f стояла в разделе «Очистка после раздела» десяти файлов exercises.md. Найдено дорого: при прогоне она снесла кластер kind, поднятый для проверки раздела 18. Курс при этом сам предупреждает об этом классе команд — урок 3.4, «Опасные варианты очистки». Исправлено везде: удаляется только созданное из образов этого раздела. У прогонщика была парная дыра — список запрещённых команд не знал формы через xargs; закрыто с самопроверкой.

Чек-листы проектов. Проект 1 дал двенадцатый дефект: в одиннадцати командах CHECKLIST.md и TASK.md стандартный ввод читался без аргументов, тогда как образ задаёт CMD ["--help"] — без аргументов печатается справка, а stdin не читается. Из-за этого же не срабатывала проверка кода 2. Показательно, что рабочий пример python-cli делает это правильно: расхождение возникло только в проекте. После правки чек-лист даёт 20 подтверждений из 20.

RedisQueue проверен впервые. Тринадцатый пункт — не дефект, а снятая пометка: код очереди на Redis был написан по документации и не выполнялся ни разу. Прогнан против настоящего Redis 8 в container'е — 6 тестов, включая главное свойство BLMOVE: задача перемещается в список «в работе» атомарно и не теряется между извлечением и подтверждением.

Одна «находка» оказалась ложной, и это стоит записать. Разбирая расхождение с блоком «Ожидаемый вывод», я счёл дефектом отсутствие GraphDriver в docker inspect на Docker 29 и уже вписал его в отчёт. Проверка показала, что курс это уже покрывал: урок 17.5 содержит раздел про containerd image store, а скрипт урока 3.2 написан с запасной ветвью. Расхождение возникло потому, что блок показывает ветвь для классического overlay2, а машина пошла по второй.

Отсюда правило для разбора прогона: расхождение ожидаемого с фактическим — ещё не дефект. В калибровочной выборке половина расхождений оказалась блоками под sudo и различиями формата вывода BuildKit между версиями.

Отдельно стоит записать цену честности. Шесть проверок из написанных мной оказались сломанными и давали правдоподобный, но неверный результат: метка на pod после создания, nodeName в обход планировщика, PATH=/usr/bin:/bin для имитации отсутствия docker, затем пустой PATH, кириллические имена, чтение /proc/PID/ns без прав. Каждая из них, оставшись непроверенной, внесла бы в курс ложный вывод.

Что подтвердилось: два запроса API при docker run; runc завершается, родитель — containerd-shim; copy-up копирует 64 МиБ ради одного байта; EXPOSE не публикует; секрет извлекается из слоя после rm; в pod'е общий сетевой namespace и раздельные PID; runAsNonRoot отвергает имя пользователя; Secret — обычный base64; классы QoS; pod принадлежит ReplicaSet; все шесть сценариев отладки воспроизводятся; конвейер даёт «не проверено: 0» при наличии инструментов и «4» при их отсутствии.

Числа, которых раньше не было: 23 HIGH/CRITICAL во всех образах — все до единой в 87 deb-пакетах базового образа, ни одной в 45 python-пакетах; SBOM из 145 пакетов; размеры образов от 41 до 73 MB.

Что нашла проверка при написании диаграмм и последних примеров (2026-08-04). Две находки, обе — про ссылки.

Диаграммы никем не использовались. Семь файлов в resources/diagrams/ значились в плане, но ни один раздел на них не ссылался: файлы существовали бы и были бы недостижимы. Добавлены ссылки из семи MAIN.md соответствующих разделов.

Глубина относительных путей в debug-scenarios была на один уровень меньше нужной. Файлы сценариев лежат на пять уровней ниже корня (resources/examples/debug-scenarios/scenarios/NN-имя/), а ссылки были написаны с ../../../../. Валидатор поймал все шесть.

Отдельно стоит записать, чем проверены четыре последних примера. Docker отсутствует, поэтому проверено следующее: ci-pipeline — 28 тестов и полный прогон конвейера, дающий код 3; production-fastapi — 63 теста и покрытие 96 %; local-registry — синтаксис пяти скриптов и то, что они корректно отказывают (код 1 при недоступном registry, код 3 при отсутствующем Docker); debug-scenarios — валидность шести программ на Python, двух compose.yaml и отказ check.sh с кодом 3.

Сборка образов и запуск container'ов не выполнялись нигде. В README каждого примера это указано явно, с перечнем того, что осталось непроверенным, — включая то, что вывод команд в примерах является реконструкцией, а не записью сеанса.

Что нашла проверка при написании справочников (2026-08-04). Три находки, и все три — расхождения между тем, что написано, и тем, что есть.

Число антипаттернов в каталоге не совпадало с заявленным. references/antipatterns.md я собрал программно из таблиц урока 19.4, а не переписал руками — и генератор насчитал 62 записи там, где в тексте урока говорилось «сорок четыре». Число попало в урок при написании и с тех пор не сверялось с таблицами, которые росли. Исправлено в пяти местах: в самом уроке, в его MAIN.md и в references/MAIN.md.

Сборка справочника из урока — не экономия усилий, а способ сделать расхождение невозможным впредь: два списка теперь физически один.

Якорь #симптом--проверка — тот самый класс, о котором предупреждает правило, добавленное часом раньше. Заголовок ## Симптом → проверка содержит стрелку, окружённую пробелами: после её удаления остаются два пробела, и рендереры дают разные якоря. Я написал ссылку в форме GitHub, валидатор ожидал схлопнутую — и поймал.

Заодно выяснилось, что правило было недостаточно широким: оно сравнивало якорь только со схлопнутой формой, поэтому ссылку, написанную в форме GitHub, пропускало. А именно её и пишут, копируя якорь с отрендеренной страницы. Правило доработано: теперь сравниваются обе формы. Заголовки переименованы в «От симптома к проверке».

Детектор смешанных алфавитов поймал якорь заголовка «Container'ы». Заголовок ## Container'ы даёт слаг из латиницы и кириллицы в одном слове — то есть двусмысленность, которую поиск не находит. Заголовок разнесён на два слова: ## Container: запуск и управление.

Общее у трёх находок: ни одна не про содержание справочников. Все три — про то, что ссылка, число или якорь перестают соответствовать тому, на что указывают, и заметить это можно только проверкой, а не чтением.

Все пятнадцать файлов references готовы.

Что нашла проверка при написании итоговых экзаменов (2026-08-04). Для практического экзамена написано отдельное приложение linkcheck (~180 строк, только стандартная библиотека) с девятью заложенными препятствиями к эксплуатации. Приложение запущено и проверено целиком: endpoint'ы отвечают, задачи выполняются, отчёты пишутся.

Оба исправления, которые предлагает разбор — чтение окружения поверх файла настроек и обработчик SIGTERM, — тоже проверены запуском. И второе дало находку.

Отсутствие обработчика SIGTERM вне container'а не воспроизводится. Я собирался показать симптом «docker stop длится десять секунд», послав сигнал локальному процессу. Процесс умер за 0.01 секунды с кодом −15: обычный процесс завершается от SIGTERM по действию ядра по умолчанию.

Симптом существует только там, где приложение имеет PID 1: для PID 1 действия по умолчанию не применяются, и сигнал без обработчика игнорируется до прихода SIGKILL. То есть локальная проверка этого требования обманывает — проверять нужно time docker stop.

Это ровно то, о чём спрашивает вопрос 7 теоретического теста, и я едва не написал в разбор утверждение, которое не воспроизвёл бы ни один читатель с локальным запуском. В экзамен добавлены отдельная врезка и строка в таблицу типичных ошибок; локально проверяется теперь другое — что обработчик работает: с ним код возврата 0 и запись о завершении есть, без него код −15 и записи нет.

Все семь файлов assessments готовы.

Что нашла проверка при написании quizzes (2026-08-04). Одна находка, зато системная: слаг заголовка со знаком препинания, окружённым пробелами, разные рендереры считают по-разному.

Заголовок ## Quiz 01 — Подготовка среды после удаления длинного тире оставляет два пробела подряд. GitHub превращает каждый пробел в дефис и даёт quiz-01--подготовка-среды; рендереры, схлопывающие пробелы, дают quiz-01-подготовка-среды. Ссылка работает в одном месте и не работает в другом, и увидеть это можно, только кликнув.

Мой валидатор использовал схлопывающее правило, а девятнадцать ссылок в assessments/MAIN.md были написаны под правило GitHub — поэтому расхождение и всплыло. Оба варианта «правильны»; неправилен сам заголовок, допускающий два прочтения.

Исправление вышло за рамки одного файла: заголовки quizzes переписаны на Quiz 01. Подготовка среды, и 27 ссылок в 27 файлах приведены к однодефисной форме. Заодно переписаны семь заголовков grading-rubric.md.

В валидатор добавлено правило ambiguous_slug, самопроверенное на шести случаях. Устроено оно осторожно: неоднозначный заголовок — предупреждение (сейчас таких 40), а ошибка — только если на него уже кто-то ссылается. Иначе отчёт утонул бы в шуме, а шумный отчёт перестают читать. Правило проверено на настоящем случае: временная ссылка на ### Container — это процесс была поймана, после удаления пробы отчёт снова чист.

Отдельно проверено, что все 3053 существующие якорные ссылки открываются и по правилам GitHub — то есть текущей поломки нет, правило ловит будущую.

Что нашла проверка в проекте 4 (2026-08-04). Три находки, из них две — в моём собственном коде проверок, а одна пришла от линтера, стоящего первым этапом конвейера.

git diff не видит неотслеживаемых файлов. Первая редакция вычисления тега проверяла чистоту дерева через git diff и помечала чистым дерево с новым, ещё не добавленным файлом. Такой файл входит в контекст сборки и меняет образ — то есть образ переставал соответствовать коммиту, а тег об этом молчал. Это худший вид расхождения: он не проявляется никак. Исправление — git status --porcelain.

В bash ширина поля у printf считается в БАЙТАХ. Сводка конвейера с русскими названиями этапов разъезжалась: каждая кириллическая буква занимает два байта, и столбец сдвигался на длину слова. Переносимого способа выровнять в bash нет, поэтому печать сводки вынесена в отдельный файл на Python, а тест test_table_columns_line_up_with_cyrillic закрепляет исправление.

Тест, зависящий от состояния своего репозитория, ломается от собственной правки. Тесты вычисления тега работали с рабочим деревом самого проекта и падали каждый раз, когда я редактировал файл теста: дерево становилось грязным. Исправление — временный репозиторий в tmp_path. После этого набор проходит и при грязном дереве, что проверено отдельно.

Находка от линтера. ruff отверг l как имя переменной. Претензия справедливая, и поймал её первый этап конвейера — ровно то, ради чего он стоит первым и занимает секунды.

Отдельно стоит записать исход прогона: на машине курса конвейер даёт код 3 — два этапа пройдено, четыре не выполнялось (нет Docker, trivy, syft). Это правильный результат, а не неудача демонстрации. Проект ровно об этом: конвейер, у которого «не проверено» сливается с «пройдено», становится зелёным в тот момент, когда проверять перестали.

Все четыре проекта готовы (13/13 файлов).

Что нашла проверка в проекте 3 (2026-08-04). Впервые за курс проверка велась против настоящей базы данных: системный PostgreSQL был недоступен, поэтому отдельный экземпляр 16.10 поднят вручную на порту 55432. Это не заглушка — миграции применялись к нему, ограничения схемы срабатывали, FOR UPDATE SKIP LOCKED проверялся двумя одновременными обработчиками. Docker по-прежнему отсутствует, поэтому compose.yaml разобран как документ и не запускался.

row_factory на соединении из пула переживает возврат в пул. Первая редакция хранилища выставляла conn.row_factory = dict_row. Тест test_count_reflects_inserts упал с KeyError: 0: соединение вернулось в пул с изменённым свойством, и метод, ожидавший кортеж, получил словарь. Ошибка зависела от порядка тестов — то есть проявлялась бы через раз и выглядела бы случайной. Исправление — создавать курсор с нужным row_factory явно в каждом запросе. Проверено откатом: с прежним кодом тест падает снова.

Счётчик, считавший не то, вешал цикл навсегда. Worker.run(max_batches=N) считал только непустые пачки. Тест с двумя обработчиками повис: тому, кому строк не досталось, process_batch() возвращал 0, счётчик не рос, предел не достигался никогда. Ошибка не в тесте — в эксплуатации она дала бы обработчик, крутящийся вхолостую, чего никто бы не заметил. Исправление: считать проходы цикла, а условие «остановиться на пустой очереди» вынести отдельным параметром.

Пропуск интеграционных тестов при отсутствии базы — та же ловушка, что отсутствующий сканер. Локально пропуск удобен, в конвейере «33 skipped» в отчёте выглядит как успех. Добавлена переменная EVENTAPI_REQUIRE_DB, превращающая пропуск в отказ; три состояния проверены запуском: с базой — 37 пройдено, без базы — 4 пройдено и 33 пропущено, без базы с требованием — 33 ошибки с внятным сообщением. Это ровно приём из урока 16.4, применённый к другой задаче.

Отдельно стоит отметить, чего проверка не дала: Redis на машине курса нет, поэтому RedisQueue написан и не выполнен ни разу, и покрытие этого файла — 66 %. Поднимать заглушку и получать красивое число было бы хуже честного признания.

Что нашла проверка в проекте 2 (2026-08-04). Три находки, и все три получены запуском процесса, а не чтением кода или прогоном через TestClient.

Опечатка в имени переменной окружения проходит молча. Первая редакция конфигурации полагалась на extra="forbid" в pydantic-settings. Тест показал, что этого недостаточно: переменную, которой не соответствует поле, библиотека просто не видит, и EVENTAPI_LOG_LEVE=info даёт тихое умолчание. Пришлось писать отдельную сверку имён с полями модели. Тест фиксирует оба факта — что pydantic опечатку пропускает и что своя проверка её ловит; если библиотека когда-нибудь начнёт ловить сама, тест об этом сообщит.

Настройка логирования в lifespan даёт смешанный поток. Живой запуск показал, что первые две строки вывода — не JSON: uvicorn печатает Started server process и Waiting for application startup до входа в lifespan. Сборщик логов разобрал бы половину потока. Перенос вызова в create_app() дал 13 строк JSON из 13. Заодно обнаружилось, что uvicorn кладёт в запись поле color_message с ANSI-кодами раскраски — его пришлось исключить явно.

Код возврата 143 после docker stop — признак штатного завершения, а не отказа. Тест утверждал returncode == 0 и упал: процесс умирает с -15. Разбор исходников uvicorn показал, что так и задумано — Server.capture_signals после мягкого завершения вызывает signal.raise_signal(captured_signal), чтобы родитель увидел настоящую причину смерти. Отсюда практический вывод, записанный в решение и в checklist: код возврата непригоден как признак успешного завершения; отличать мягкое от жёсткого нужно по времени (доли секунды против ровно grace period) и по наличию записей shutdown_begin/shutdown_done.

Общее у трёх находок: ни одна не видна ни при чтении кода, ни через TestClient, который не поднимает сервер. Файл tests/test_process.py, запускающий настоящий процесс и посылающий ему сигнал, — то, что отличает проект от упражнения.

Что нашла проверка при работе над проектами (2026-08-04). Самая крупная находка за все раунды — и найдена она была не проверкой материала, а уточнением правила в валидаторе.

Правило проверки ссылок содержало лазейку. Ссылка на несуществующий файл считалась законной, если каталог существует: предполагалось, что это задел на ещё не написанный материал. Но каталоги всех девятнадцати разделов существуют давно, а состав их известен пофайлово — значит, внутри них ссылка на файл вне плана является опечаткой, а не заделом.

После уточнения правила обнаружилось 88 битых ссылок в 31 файле. Все они — ссылки на правдоподобные, но несуществующие имена: 04-container-lifecycle/06-signals-and-shutdown.md вместо 04-signals-and-shutdown.md, 08-networking/04-dns-and-service-discovery.md вместо 04-docker-dns.md, 15-testing/05-image-validation.md вместо 05-build-validation.md. Имя угадывалось по смыслу урока и не сверялось с каталогом.

Отображение старых имён на настоящие составлено вручную по фактическому составу каталогов: автоматическое сопоставление по похожести дало несколько неверных пар (05-dockerfile/09-build-secrets.md оно отнесло к 05-build-cache.md, тогда как речь о 06-buildkit.md), и довериться ему было нельзя.

Вывод, который стоит записать: правило «ссылка на несозданный файл законна» работало ровно до тех пор, пока в курсе были несозданные файлы. По мере заполнения разделов оно из полезного послабления превратилось в дыру. Проверки нужно пересматривать вместе с тем, что они проверяют, — иначе они устаревают молча.

Отдельно о структуре раздела 19. Курс из восемнадцати разделов о том, как пользоваться инструментом, обязан закончиться разделом о том, когда им пользоваться не нужно, — иначе обучение даёт не инженера, а сторонника технологии. Раздел построен так, чтобы вывод «Docker здесь не нужен» был полноправным результатом, а не признаком неудачи.

Неожиданная особенность: Docker не установлен, а проверять оказалось что. systemd версии 255 на машине курса есть, поэтому утверждение «systemd даёт ту же изоляцию теми же механизмами ядра» проверено не рассуждением, а systemd-analyze verify на трёх сгенерированных unit-файлах. Площадь атаки измерена: 373 системных вызова из заголовка ядра и 41 capability из /proc/self/status — числа читаются из системы, а не задаются в коде, потому что весь смысл в том, что площадь принадлежит вашему ядру. Стоимость альтернативы контейнеризации для кратковременных задач тоже измерена: python3 -m venv — около 5 с, uv venv — около 0,1 с.

Числовые значения расходов самого Docker в разделе не приводятся намеренно. Измерить их здесь нечем, а называть проценты «сеть медленнее на N %» без замера означало бы ровно тот подлог, против которого раздел и написан. Названы механизмы: они от стенда не зависят.

Новое в разделе, чего нет в других: тринадцать свойств Docker из четырнадцати имеют прямой аналог в systemd, причём десять — на буквально одном механизме ядра, и граница между инструментами проходит по упаковке, а не по изоляции; MemoryHigh (торможение до OOM) есть у systemd и отсутствует у Docker; каждый способ ускорить container отключает то, ради чего container взят; образ с оборудованием привязывается к версии драйвера на хосте и перестаёт быть переносимым; ReadWriteOnce не решает задачу общего состояния, а меняет явный отказ на тихое повреждение; порталы Flatpak решают задачу, которую проброс сокета не решает в принципе — потому что решение принимает пользователь в момент запроса, а не никто.

Что нашла проверка в разделе 19. Три находки, все — в моих собственных инструментах, и все три обнаружены проверкой инструмента на входе, который обязан пройти.

Первая: systemd-analyze verify возвращает код 0 и на корректном unit-файле, и на сломанном — то есть код возврата непригоден как признак. Выяснилось только после того, как я подал заведомо испорченный файл; признаком пришлось сделать наличие строк с именем файла в выводе.

Вторая: тот же systemd-analyze verify проверяет существование исполняемого файла, а не только синтаксис, и два unit'а из трёх получили замечание Command ... is not executable. Соблазн был подставить существующие пути и получить три чистые строки. Это скрыло бы находку, которая сама по себе содержательна: у Docker аналога такой проверки нет — опечатка в command: внутри compose.yaml разбор конфигурации проходит и выявляется только при запуске. Замечания разделены на два класса, и второй остался в отчёте.

Третья и самая показательная: линтер антипаттернов из урока 19.4 в первой редакции отмечал как утечку строку DB_PASSWORD_FILE: /run/secrets/db_password — то есть правильный приём передачи пути к секрету вместо самого секрета. На плохой конфигурации линтер вёл себя безупречно и давал двадцать четыре осмысленные находки; правило проявило себя только там, где находок быть не должно. Исправление — исключить ключи с суффиксом _FILE; вывод, записанный в урок, шире: ложное срабатывание дороже пропуска, потому что после нескольких таких срабатываний отчёты перестают читать целиком.

Эти три находки задним числом объясняют, почему приём «проверить проверяющий» встречается в курсе начиная с урока 16.4: он ловит не ошибки в проверяемом, а ошибки в проверке, и никаким другим способом они не находятся.

Что нашла проверка в разделе 17. Сам раздел ошибок не дал, но валидатор получил новое правило по итогам предыдущего раунда: файл без секции «Навигация» считается оборванным. Причина — в уроке 13.5 в текст попала моя собственная служебная разметка, обрезавшая файл посередине упражнения. Артефакт нашёлся при осмотре, а не проверкой, и это неприемлемо: обрыв файла не должен зависеть от того, дочитал ли я его до конца. Правило исключает README.md, COURSE_STATUS.md и каталоги resources/config, где раздела навигации нет по устройству.

Отдельно о структуре раздела 17. Раздел разбирает механизмы, часть которых доступна только с root. Он построен так, чтобы основное выполнялось без привилегий: через Engine API, через показания ядра изнутри container'а, через штатные команды. Там, где привилегии нужны — runc, nsenter, ctr, — задание требует отметить шаг невыполненным, а не обойти его. Это то же правило трёх состояний, что в уроке 16.4.

Новое в разделе, чего нет в других: docker run — это два запроса API, и между ними container существует, но не запущен; runc завершается после настройки, поэтому pgrep -c runc даёт 0 при работающих container'ах, а родителем процесса оказывается shim; Docker создаёт шесть namespaces из восьми, и сравнивать их нужно по inode, а не по поведению; memory.high (торможение) и memory.max (OOM) — разные вещи, и Docker задаёт только второй; запись одного байта в файл образа копирует его целиком, поэтому один sed -i по файлу в 128 МиБ даёт слой в 128 МиБ.

Главное различение раздела — запрошено против применено. docker inspect показывает намерение, показания ядра изнутри container'а показывают результат, и эти два ответа могут не совпадать: отключённый контроллер cgroup, несовместимое ядро, отсутствующая возможность. Задание 8 отрабатывает это различение, задание 9 превращает его в инструмент сверки трёх источников.

Отдельно о структуре раздела 18. Раздел обзорный, но кластер в нём не используется нигде — и это не вынужденное ограничение, а следствие главного утверждения: готовность к Kubernetes определяется устройством образа и приложения, а не наличием манифестов. Проверить это можно без кластера, и задания раздела именно это и делают.

Ключевое число раздела получено в уроке 18.3: около половины работы по переносу приходится на код приложения, а не на написание YAML. Инструмент из этого урока намеренно не генерирует манифесты — генерация и есть то, что автоматические конвертеры делают плохо; вместо неё он выявляет решения, которые нужно принять, и оценивает объём. Признак состояния определяется по устройству тома (именованный против bind mount), а не по имени образа: иначе правило работало бы на учебном примере и не работало бы на чужом стеке.

Новое в разделе: depends_on не имеет аналога в принципе, потому что pod'ы запускаются параллельно по устройству системы, и правильная замена — повторы подключения в приложении, а не initContainers; сеть в Kubernetes плоская, а NetworkPolicy не действует без поддержки плагином сети — правило может существовать и молча не работать; ReadWriteOnce не решает задачу общего состояния, а лишь меняет симптом с явного (Multi-Attach error) на тихий (два процесса пишут в один файл), и выбор между исходами делает планировщик.

Чек-лист из урока 18.4 устроен так, чтобы отрицательные признаки были по модулю не слабее сильнейших положительных. Это проверяется отдельно — сравнением весов, а не вывода на удобном примере, — и проверка на подделке подтверждает работу: проект с семью положительными баллами («нагрузка скачет», «нужны регионы», «частые выкатки» — каждый настоящий) получает вывод «Compose достаточно», потому что дежурить со знанием кластера некому. Модель стоимости показывает, что постоянная часть превышает разовую уже в первый год, а управляемый кластер снимает около 16 % ежемесячной нагрузки — заметно, но далеко не половину: отладка и дежурство не уменьшаются.

Оговорка, которую стоит держать при чтении раздела: веса чек-листа и часы в модели стоимости — порядки величин, а не измерения. Ценность в соотношении разовой и постоянной частей и в том, что разговор идёт о конкретных строках, а не о моде.

Что нашла проверка в разделе 14. Одна находка: в уроке 14.2 продублирован номер подсказки — «Подсказка 1, Подсказка 3, Подсказка 3». Валидатор этого не ловит, заметил при осмотре. Правило добавлять не стал: нумерация подсказок нигде не используется как якорь, и цена ошибки — косметическая.

Отдельно стоит отметить один блок, написанный намеренно с ошибкой. В уроке 14.1 разбор ссылки на образ сначала показан в наивной реализации, которая для ghcr.io/myorg/api собирает полное имя как docker.io/ghcr.io/myorg/api — registry подставляется дважды. Ошибка разобрана тут же и исправлена отдельной функцией. Причина такого построения: именно эта ошибка превращает опечатку в имени registry в обращение к Docker Hub, где может оказаться чужой образ, — и увидеть её на готовом коде труднее, чем на процессе.

Отдельно о структуре раздела 14. Пересечение только с уроком 11.6 (закрепление по digest) и 12.7 (доверие к артефактам). Здесь digest разбирается с другой стороны: не как средство воспроизводимости, а как ответ на вопрос «почему в эксплуатации работает не то, что собрали». Новое в разделе: дедупликация слоёв работает в пределах репозитория — тот же образ в другом репозитории передаётся заново; три разных digest'а одного образа (index, manifest, config) и то, какой из них годится для закрепления; insecure registry защищает содержимое слоёв, но не защищает от подмены манифеста по тегу — а по digest защищает даже без TLS.

Что нашла проверка в разделе 13. Две находки, обе мои. Первая — служебная разметка инструмента, попавшая в текст урока 13.5: файл при этом оборвался на середине упражнения. Детектор артефактов не успел сработать — я заметил обрыв сам при осмотре хвоста файла, — но правило, добавленное после инцидента в уроке 5.8, остаётся единственной защитой от такого, и его стоит расширить проверкой на обрыв файла посреди раздела. Вторая — опечатка плain в уроке 13.4, пойманная детектором смешанных алфавитов.

Отдельно: правило проверки JSON доработано. Блок с фрагментом файла лога Docker — это JSON Lines, несколько объектов по одному на строку, и валидатор считал его сломанным документом. Теперь при ошибке «Extra data» на многострочном блоке предпринимается вторая попытка — построчная. Самопроверено на четырёх случаях: JSON Lines проходит, одиночный документ проходит, настоящая ошибка в многострочном и в одиночном блоке по-прежнему срабатывает.

Отдельно о структуре раздела 13. В отличие от разделов 11 и 12, здесь пересечений почти нет: диагностика раньше упоминалась фрагментарно (урок 8.6 — лестница из семи шагов для сети, урок 10.3 — отладка при разработке, урок 11.4 — подбор лимитов). Раздел собирает это в методику и добавляет то, чего не было нигде: разрыв длинных строк лога на границе 16 КиБ, CPU throttling при низкой средней загрузке, отсутствие логов в отчёте docker system df, различение утечки и фрагментации по отношению tracemalloc к anon. Урок 13.6 — справочник на тридцать записей, к которому отсылают остальные уроки раздела.

Что нашла проверка в разделе 12. Одна ошибка в ссылке: урок 12.6 ссылался на 03-images-and-layers/, тогда как каталог называется 03-images/, и файл имел другое имя. Правило сверки с плановым составом, добавленное после раздела 07, поймало это сразу. Детектор смешанных алфавитов сработал на намеренном фрагменте — поддельном токене ghp_примертокена... в учебном compose.yaml из задания 9. Формально это не опечатка, но правило право: строка выглядит как настоящий токен и содержит кириллицу, чего в реальном токене быть не может. Заменил на латинский плейсхолдер — так пример честнее.

Отдельно о структуре раздела 12. Пересечение с уже написанным здесь ещё плотнее, чем в разделе 11: урок 12.4 с уроком 2.5 и 11.2, урок 12.6 с 5.9, 9.5 и 11.5, урок 12.7 с 11.6. Подход тот же — не повторять механику, а добавить недостающий слой. В 12.4 это точный состав --privileged (четыре независимых ослабления, а не одно) и разбор того, что даёт каждая опасная capability; в 12.6 — извлечение секретов из чужого образа, аудит и порядок действий после утечки, включая то, что удаление образа утечку не устраняет; в 12.7 — доверие к артефакту (официальные образы против похожих, typosquatting, проверка подписи) в отличие от гигиены версий, разобранной в 11.6.

Отдельно о структуре раздела 11. Плановый состав пересекается с уже написанным: урок 11.3 с уроками 6.10 и 9.4, урок 11.4 с 6.13, урок 11.5 с 6.4 и 9.5. Повторять механику не стал — раздел 11 сделан операционным слоем: он ссылается на разобранную механику и добавляет то, чего в ней не было. В 11.3 это арифметика обнаружения отказа, последовательность слива трафика и расчёт stop_grace_period; в 11.4 — процедура подбора лимитов по измерению и различение утечки, фрагментации и кэша; в 11.5 — секреты при сборке (RUN --mount=type=secret) и внешние хранилища. Пересечение отмечено в самих уроках явной ссылкой на источник механики.

Что нашла проверка в разделах 09–10. Компоновочные теги !reset и !override, введённые Compose для управления слиянием файлов, неизвестны стандартному загрузчику PyYAML — валидатор считал корректные примеры сломанными. Добавлен загрузчик, понимающий эти теги. Второе: heredoc с именем файла check.sh компилировался как Python, потому что внутри шелл-скрипта встречалась строка python3 -c "import ...". Эвристика «похоже на Python» теперь применяется только к heredoc'ам без имени файла — известное расширение к Python отношения не имеет. Оба правила самопроверены на четырёх случаях.

Что нашла проверка в разделе 08. Детектор Python внутри heredoc опирался на эвристику «тело похоже на Python» и сработал на YAML-файле, где Python лежит строкой внутри command:. Заодно выяснилось, что YAML, записанный через heredoc, не проверялся вовсе — а таких файлов в курсе больше десятка. Теперь тип содержимого определяется по имени целевого файла из cat > файл <<TAG: .py компилируется, .yaml разбирается парсером YAML, .json — парсером JSON. Имя важнее расширения: Dockerfile.json — это Dockerfile, а не JSON, и такой файл пропускается. Правило самопроверено на четырёх случаях. Проверка сразу дала результат: 12 YAML-файлов внутри heredoc, прежде не проверявшихся, оказались валидны.

Валидатор доработан по итогам этих находок: он различает иллюстрацию синтаксиса и полный пример с забытым FROM, понимает heredoc и определяет тип его содержимого по имени файла, ищет служебные артефакты, контролирует парность HTML-тегов и сверяет ссылки на ненаписанные файлы с плановым составом. Эвристика Dockerfile покрыта самопроверкой на трёх случаях.

Ссылки на ненаписанные файлы — ожидаемое состояние: навигационный скелет создан целиком заранее, чтобы имена файлов и пути оставались стабильными и не ломались при наполнении. Все они ведут на файлы, явно перечисленные ниже в плановом составе разделов.

Что НЕ проверено — и почему это важно знать

Ни одна команда docker в курсе не выполнялась. Среда подготовки материала не содержит установленного Docker (docker: command not found, сокет отсутствует, служба не запущена). Проверено то, что проверяемо без демона:

Проверено фактическиНЕ проверено фактически
Синтаксис shell-команд (bash -n)Что команда отрабатывает без ошибки
Компиляция Python, разбор JSON/YAML/TOMLЧто образ собирается
Первая инструкция и структура DockerfileЧто docker build завершается успешно
Прогон pytest для двух примеров (20 тестов)Что тесты проходят внутри образа
Существование команд и флагов по документацииТочный формат вывода конкретной версии Engine
Версии пакетов по PyPI и Docker HubСовместимость версий между собой при установке

Отсюда следует, как читать блоки вывода в уроках: это ожидаемый вывод, восстановленный из документации и из описанного механизма, а не запись реального сеанса. Числа, зависящие от машины, у читателя будут другими. Качественная часть (код выхода, статус, наличие строки) следует из механизма и проверяется командой, приведённой рядом.

Что это меняет для использования курса. Перед первым проведением занятий по разделу прогоните его команды на машине с Docker: расхождения, если они есть, обнаружатся именно там — в форматировании вывода конкретной версии Engine и в совместимости версий пакетов при реальной установке. Логика уроков от этого не зависит, формулировки могут потребовать правки.


Плановый состав разделов

Список фиксируется заранее, чтобы имена файлов и ссылки оставались стабильными.

00-course-overview (5)

00-learning-goals.md, 01-competency-map.md, 02-course-roadmap.md, 03-learning-schedules.md, 04-assessment-system.md

01-environment (8)

MAIN.md, 01-linux-requirements.md, 02-docker-installation.md, 03-docker-daemon.md, 04-first-container.md, 05-docker-group-and-socket.md, 06-rootless-docker.md, exercises.md

02-containerization-basics (9)

MAIN.md, 01-processes-and-containers.md, 02-containers-vs-virtual-machines.md, 03-linux-namespaces.md, 04-cgroups.md, 05-capabilities.md, 06-container-runtime.md, 07-docker-terminology.md, exercises.md

03-images (7)

MAIN.md, 01-image-architecture.md, 02-layers-and-copy-on-write.md, 03-tags-and-digests.md, 04-image-management.md, 05-save-load-export-import.md, exercises.md

04-container-lifecycle (8)

MAIN.md, 01-lifecycle-states.md, 02-run-modes.md, 03-exec-logs-inspect.md, 04-signals-and-shutdown.md, 05-pid1-and-init.md, 06-restart-policies-and-exit-codes.md, exercises.md

05-dockerfile (11)

MAIN.md, 01-build-context-and-dockerignore.md, 02-base-instructions.md, 03-copy-add-run.md, 04-cmd-and-entrypoint.md, 05-build-cache.md, 06-buildkit.md, 07-multi-stage-builds.md, 08-healthcheck.md, 09-reproducible-builds.md, exercises.md

06-python-containers (15)

MAIN.md, 01-base-images.md, 02-dependency-management.md, 03-python-dockerfile.md, 04-environment-variables.md, 05-signals-and-pid1.md, 06-logging.md, 07-non-root-and-permissions.md, 08-cli-application.md, 09-flask.md, 10-fastapi.md, 11-workers.md, 12-testing.md, 13-resource-limits.md, exercises.md

07-storage (8)

MAIN.md, 01-container-filesystem.md, 02-volumes.md, 03-bind-mounts.md, 04-tmpfs-and-readonly.md, 05-permissions-uid-gid.md, 06-backup-and-restore.md, exercises.md

08-networking (8)

MAIN.md, 01-networking-basics.md, 02-bridge-networks.md, 03-port-publishing.md, 04-docker-dns.md, 05-network-modes.md, 06-troubleshooting.md, exercises.md

09-docker-compose (9)

MAIN.md, 01-compose-basics.md, 02-services-reference.md, 03-networks-and-volumes.md, 04-healthchecks-and-dependencies.md, 05-environment-and-secrets.md, 06-multiple-files-and-profiles.md, 07-practical-stack.md, exercises.md

10-development-workflow (7)

MAIN.md, 01-dev-vs-prod-images.md, 02-hot-reload.md, 03-debugging-in-container.md, 04-database-migrations.md, 05-automation.md, exercises.md

11-production (8)

MAIN.md, 01-production-principles.md, 02-runtime-hardening.md, 03-healthchecks-and-readiness.md, 04-resource-limits-and-memory.md, 05-configuration-and-secrets.md, 06-supply-chain.md, exercises.md

12-security (9)

MAIN.md, 01-threat-model.md, 02-daemon-and-socket.md, 03-users-and-namespaces.md, 04-capabilities-and-privileged.md, 05-seccomp-apparmor-selinux.md, 06-secrets.md, 07-supply-chain-security.md, exercises.md

13-observability (8)

MAIN.md, 01-logs-and-drivers.md, 02-inspect-events-stats.md, 03-resource-exhaustion.md, 04-disk-usage-and-cleanup.md, 05-deep-debugging.md, 06-diagnostic-table.md, exercises.md

14-registry (6)

MAIN.md, 01-registry-basics.md, 02-authentication.md, 03-local-registry.md, 04-tagging-strategy.md, exercises.md

15-testing (7)

MAIN.md, 01-testing-strategy.md, 02-tests-in-container.md, 03-compose-integration-tests.md, 04-testcontainers.md, 05-build-validation.md, exercises.md

16-ci-cd (7)

MAIN.md, 01-pipeline-design.md, 02-github-actions.md, 03-build-cache-in-ci.md, 04-scanning-and-sbom.md, 05-gitlab-ci.md, exercises.md

17-docker-internals (7)

MAIN.md, 01-engine-api-and-socket.md, 02-containerd-shim-runc.md, 03-namespaces-deep-dive.md, 04-cgroups-v2.md, 05-overlayfs.md, exercises.md

18-docker-and-kubernetes (6)

MAIN.md, 01-problems-solved.md, 02-kubernetes-objects.md, 03-compose-to-kubernetes.md, 04-when-orchestration.md, exercises.md

19-docker-limitations (6)

MAIN.md, 01-where-containers-hurt.md, 02-isolation-limits.md, 03-when-not-to-use-docker.md, 04-antipatterns.md, exercises.md

projects (13)

MAIN.md + для каждого из четырёх проектов: TASK.md, SOLUTION.md, CHECKLIST.md

assessments (7)

MAIN.md, module-quizzes.md, checkpoints.md, debugging-challenges.md, final-theory-exam.md, final-practical-exam.md, grading-rubric.md

references (15)

MAIN.md, docker-cli-cheatsheet.md, dockerfile-cheatsheet.md, compose-cheatsheet.md, networking-cheatsheet.md, storage-cheatsheet.md, security-checklist.md, debugging-checklist.md, production-checklist.md, python-container-checklist.md, cleanup-checklist.md, decision-map.md, antipatterns.md, further-learning.md, glossary.md

resources (8)

source-links.md + diagrams/: 01-docker-architecture.md, 02-namespaces-and-cgroups.md, 03-image-layers.md, 04-build-cache-flow.md, 05-network-topology.md, 06-compose-stack.md, 07-ci-pipeline.md


Рабочие примеры (resources/examples/)

ПримерИспользуется в разделеСтатус
hello-python/05. DockerfileНе начат
python-cli/06. Python containers, Проект 1Не начат
multistage-uv/05, 06Не начат
flask-basic/06. Python containersНе начат
fastapi-basic/06. Python containers, Проект 2Не начат
worker-redis/06, 09Не начат
pytest-container/15. TestingНе начат
compose-stack/09. Docker Compose, Проект 3Не начат
production-fastapi/11. Production, Проект 4Не начат
ci-pipeline/16. CI/CD, Проект 4Не начат
local-registry/14. RegistryНе начат
debug-scenarios/13. Observability, assessmentsНе начат

Незавершённые элементы

Незаполненных элементов не осталось: плановый состав создан полностью.

Состав

КатегорияПланСоздано
Разделы 00–191919
projects/* (TASK, CHECKLIST, SOLUTION + MAIN)1313
assessments/*77
references/*1515
resources/diagrams/*77
Рабочие примеры resources/examples/*1212 + README

Рабочие примеры

Все двенадцать собираются на Docker Engine 29.7.1. Стадия test проверена отдельно — до прогона она не собиралась в пяти примерах из шести (см. дефект про .dockerignore выше).

ПримерРазмер образаСтадия test
hello-python41Mнет
python-cli45M9 пройдено
multistage-uvтребует однократного make lock (документировано)
flask-basic44Mнет
fastapi-basic66M6 пройдено
worker-redis43Mнет
pytest-container45Mпройдено, порог покрытия
compose-stack73M9 пройдено
production-fastapi66M63 пройдено
ci-pipeline73Mпройдено (нужен git в стадии)
local-registry41Mнет
debug-scenariosшесть сценариев воспроизводятся

Что это означает для читателя

Курс готов целиком и прогнан на настоящем Docker. Материал можно проходить от начала до конца; проекты, оценивание, справочники, диаграммы и примеры на месте. Что именно проверялось запуском, а что осталось непроверяемым на этом стенде, перечислено в verify/FACTS.md.


Журнал изменений

ДатаИзменение
2026-07-29Создано дерево директорий, MAIN.md, README.md, COURSE_STATUS.md. Зафиксирован плановый состав файлов.
2026-07-29Раздел 00 завершён: цели, карта компетенций, карта курса, расписания, система оценивания.
2026-07-29Созданы локальные MAIN.md для всех разделов 01–19, projects, assessments, references. Навигация работает целиком.
2026-07-29Раздел 01 завершён: 8 файлов. Проверены ссылки и shell-примеры.
2026-07-29Раздел 02 завершён: 9 файлов. Проверены ссылки и shell-примеры.
2026-07-29Создан resources/source-links.md с версионным контекстом и проверенными источниками.
2026-07-29Раздел 03 завершён: 7 файлов. Проверка выявила невалидный JSON и битый якорь — исправлено.
2026-07-30Валидатор расширен: Python внутри heredoc, YAML, полнота Dockerfile-блоков.
2026-07-30Раздел 04 завершён: 8 файлов. Все проверки чистые.
2026-07-30Уроки 5.1 и 5.2. Эвристика проверки Dockerfile уточнена и покрыта самопроверкой.
2026-07-30Раздел 05 завершён: 11 файлов. Валидатор доработан: heredoc, Python в heredoc, артефакты, парность тегов.
2026-07-30Сверка версий с PyPI: исправлены устаревшие pin'ы в 8 файлах.
2026-07-30Созданы 7 рабочих примеров для раздела 06 (51 файл кода). 20 тестов прогнаны фактически.
2026-07-31Разделы 06–11 завершены: Python внутри container, storage, networking, Compose, development workflow, production.
2026-07-31Разделы 12–14 завершены: security, observability, registry.
2026-07-31Разделы 15–16 завершены: testing, CI/CD. Валидатор: значащие кавычки heredoc, пустой heredoc, исключение служебных каталогов.
2026-07-31Урок 15.1 переписан целиком: идентификаторы Python были на кириллице вопреки соглашению всех предыдущих уроков.
2026-08-03Раздел 17 завершён: 7 файлов. Добавлено правило «файл без секции Навигация считается оборванным».
2026-08-03Раздел 18 завершён: 6 файлов. Кластер не использовался; все выводы получены на документах и моделях, что отмечено в тексте.
2026-08-04Раздел 19 завершён: 6 файлов. Все 19 разделов готовы. Часть проверок выполнена по-настоящему: systemd-analyze verify, число системных вызовов и capabilities ядра, время создания venv.
2026-08-06Оглавление со ссылками на каждый урок добавлено в CONTENTS.md, README.md и MAIN.md — видно при первом открытии репозитория. Синхронность трёх копий и целостность ссылок проверяет verify/74-toc.py (--fix перезаписывает строки разделов по файлам курса).
2026-08-06Проверка оглавлений подключена как git pre-commit hook: .githooks/pre-commit. Включается в клоне командой git config core.hooksPath .githooks; проверяет staged-содержимое, срабатывает только на commit с Markdown, обходится через git commit --no-verify.
Markdown на GitHub ↗