Раздел 1. Практические задания
Задания выполняются в реальной системе. Каждое содержит постановку, ожидаемый результат и команды проверки. Разбор открывайте только после самостоятельной попытки.
Обозначения: [обяз.] — обязательное, [доп.] — дополнительное, [★] — повышенной сложности, [диаг.] — диагностическое.
Перед началом создайте рабочий каталог:
mkdir -p ~/docker-course/01-environment
cd ~/docker-course/01-environment
Задание 1. Отчёт о пригодности системы [обяз.]
Постановка. Напишите скрипт check-system.sh, собирающий данные о системе: дистрибутив и версия, кодовое имя релиза, architecture в двух формах, версия kernel, версия cgroup, тип файловой системы и свободное место под /var/lib, доступная память, тип виртуализации.
Скрипт должен работать без sudo и ничего не изменять.
Ожидаемый результат. Вывод из не менее чем девяти строк с явными значениями и заключением о пригодности.
Проверка:
./check-system.sh | grep -c ':' # не менее 9
./check-system.sh | grep -i cgroup
Полное решение — в уроке 1.1.
Задание 2. Установка и проверка версий [обяз.]
Постановка. Установите Docker Engine из официального репозитория. Затем подтвердите версии всех компонентов одной последовательностью команд, не открывая документацию.
Требуется получить версии: Docker Engine (server), Docker CLI (client), API, containerd, runc, Buildx, Compose.
Ожидаемый результат. Семь значений версий, каждое получено отдельной командой или шаблоном --format.
Проверка:
docker version --format 'engine={{.Server.Version}} cli={{.Client.Version}} api={{.Server.APIVersion}}'
docker compose version --short
docker buildx version
Разбор
Версии containerd и runc лежат в массиве Server.Components и извлекаются шаблоном с перебором:
docker version --format \
'{{range .Server.Components}}{{.Name}}: {{.Version}}{{"\n"}}{{end}}'
Engine: 29.0.1
containerd: 1.7.28
runc: 1.3.1
docker-init: 0.19.0
Полный скрипт проверки — verify-install.sh в уроке 1.2.
Главный признак успешной установки — наличие блока Server. Он означает, что CLI смог связаться с daemon. Всё остальное вторично.
Задание 3. Управление службой [обяз.]
Постановка.
- Запустите долгоживущий container:
docker run -d --name longrun alpine sleep 3600. - Остановите Docker командой
sudo systemctl stop docker. - Выполните
docker ps. Объясните наблюдаемое. - Проверьте состояние
longrun. - Остановите Docker по-настоящему и убедитесь, что он не запускается сам.
- Верните всё в рабочее состояние.
Ожидаемый результат. Письменное объяснение того, почему на шаге 3 daemon запустился заново, и что произошло с container.
Проверка:
systemctl is-active docker docker.socket
docker ps -a --filter name=longrun
Разбор
Шаг 3. docker ps сработал, потому что docker.socket продолжает слушать после остановки docker.service. Первое же обращение к сокету заставляет systemd запустить daemon — это socket activation.
Шаг 4. Состояние longrun зависит от настройки live-restore:
live-restore: false(по умолчанию) — container был остановлен вместе с daemon, статусExited;live-restore: true— container продолжал работать, статусUp.
Проверить настройку:
docker info --format '{{.LiveRestoreEnabled}}'
Шаг 5. Полная остановка требует обоих unit:
sudo systemctl stop docker.socket docker.service
docker ps
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
Теперь daemon действительно не работает.
Шаг 6.
sudo systemctl start docker.socket docker.service
docker rm -f longrun
Механизм подробно разбирается в уроке 1.3.
Задание 4. Чтение docker info [обяз.]
Постановка. Найдите в выводе docker info и выпишите шесть значений:
- Storage driver.
- Logging driver.
- Cgroup driver.
- Cgroup version.
- Список security options.
- Каталог данных.
Затем получите те же значения одной командой через --format.
Ожидаемый результат. Одна команда, выводящая все шесть значений в читаемом виде.
Проверка:
docker info --format 'storage={{.Driver}} logging={{.LoggingDriver}} cgroup-driver={{.CgroupDriver}} cgroup-version={{.CgroupVersion}} root={{.DockerRootDir}}'
docker info --format '{{.SecurityOptions}}'
Разбор
Одна команда со всеми значениями:
docker info --format '{{printf "%-16s %s\n" "Storage:" .Driver}}{{printf "%-16s %s\n" "Logging:" .LoggingDriver}}{{printf "%-16s %s\n" "Cgroup driver:" .CgroupDriver}}{{printf "%-16s v%s\n" "Cgroup version:" .CgroupVersion}}{{printf "%-16s %s\n" "Data root:" .DockerRootDir}}{{printf "%-16s %v\n" "Security:" .SecurityOptions}}'
Storage: overlayfs
Logging: local
Cgroup driver: systemd
Cgroup version: v2
Data root: /var/lib/docker
Security: [name=seccomp,profile=builtin name=cgroupns]
Что означают эти значения:
| Значение | Смысл |
|---|---|
overlayfs | Используется containerd image store (Docker 29+). При классическом хранилище было бы overlay2 |
local | Драйвер логирования с ротацией. По умолчанию был бы json-file без ротации |
systemd | Cgroup управляется через systemd — правильно для systemd-систем |
v2 | Единая иерархия cgroup; пути в /sys/fs/cgroup соответствуют примерам курса |
seccomp,profile=builtin | Активен встроенный профиль seccomp, ограничивающий системные вызовы |
cgroupns | Container получает собственный cgroup namespace |
Отсутствие apparmor в списке на Ubuntu — повод проверить, установлен ли AppArmor: он влияет на материал раздела 12.
Задание 5. Docker socket и права на host [доп.]
Выполняется только на собственной учебной машине. Задание демонстрирует механизм, а не предлагает его использовать.
Постановка. Покажите, что доступ к Docker socket даёт чтение произвольного файла host. Прочитайте /etc/shadow из container, не используя sudo в самой команде.
Затем объясните письменно, почему это не является уязвимостью Docker.
Ожидаемый результат. Содержимое /etc/shadow в терминале и объяснение из трёх-четырёх предложений.
Проверка:
docker run --rm -v /:/host:ro alpine head -3 /host/etc/shadow
Разбор
docker run --rm -v /:/host:ro alpine head -3 /host/etc/shadow
root:!:20038:0:99999:7:::
daemon:*:20038:0:99999:7:::
bin:*:20038:0:99999:7:::
Флаг :ro делает монтирование доступным только для чтения — для демонстрации этого достаточно, и риск случайного повреждения системы исключён.
Почему это не уязвимость. Возможность смонтировать любой каталог host — заявленная функция Docker, необходимая для законных задач: доступ к данным, конфигурации, сокетам. Daemon работает от root и выполняет запросы тех, кому разрешён доступ к сокету. Уязвимости здесь нет — есть модель доступа, которую нужно понимать: право отправлять команды daemon равно правам root на машине.
Практический вывод: список getent group docker — это список пользователей с root-эквивалентными правами. На production-сервере он должен быть коротким и проверяться регулярно.
Подробно — в уроке 1.5.
Задание 6. Rootless параллельно с системным [доп.]
Постановка. Настройте rootless Docker так, чтобы он работал параллельно с системным. Создайте Docker context для переключения. Докажите тремя независимыми проверками, что перед вами именно rootless daemon.
Ожидаемый результат. Скрипт, выводящий сравнение двух daemon по трём параметрам.
Проверка:
docker context ls
docker --context rootless info --format '{{.SecurityOptions}}' | grep -o rootless
docker --context rootless info --format '{{.DockerRootDir}}'
Ожидается, что DockerRootDir для rootless указывает в домашний каталог, а не в /var/lib/docker.
Полное решение — в уроке 1.6.
Задание 7. Логи daemon и запуск container [★]
Постановка. Настройте daemon так, чтобы он писал подробные логи. Затем запустите container с конкретным именем и найдите в журнале systemd записи, относящиеся именно к этому container.
Требования:
- Изменение конфигурации выполняется через
daemon.jsonс предварительной проверкой синтаксиса. - Найденные записи должны содержать идентификатор container.
- После выполнения верните конфигурацию в исходное состояние.
Ожидаемый результат. Команда journalctl, показывающая записи о жизненном цикле конкретного container.
Подсказка 1
Подробное логирование включается параметром "debug": true в daemon.json.
Подсказка 2
Полный ID container получается так:
docker inspect <name> --format '{{.Id}}'
Именно полный ID (64 символа), а не сокращённый, встречается в логах daemon.
Разбор
# 1. Резервная копия и включение debug
sudo cp /etc/docker/daemon.json /etc/docker/daemon.json.bak 2>/dev/null || true
sudo tee /etc/docker/daemon.json > /dev/null <<'EOF'
{
"debug": true,
"log-driver": "local",
"log-opts": { "max-size": "10m", "max-file": "3" }
}
EOF
# 2. Проверка до перезапуска — обязательный шаг
python3 -m json.tool /etc/docker/daemon.json > /dev/null && echo "JSON OK"
sudo dockerd --validate
# 3. Применение
sudo systemctl restart docker
# 4. Запуск container и получение полного ID
docker run -d --name logdemo alpine sleep 30
CID="$(docker inspect logdemo --format '{{.Id}}')"
echo "Container ID: $CID"
# 5. Поиск в журнале
sudo journalctl -u docker.service --since "2 min ago" --no-pager | grep -F "$CID" | head -20
Ожидаемый результат — строки вида:
dockerd[1842]: time="..." level=debug msg="container mounted via layerStore: ..." container=a3f2...
dockerd[1842]: time="..." level=debug msg="Calling POST /v1.53/containers/a3f2.../start"
dockerd[1842]: time="..." level=debug msg="Assigning addresses for endpoint logdemo's interface on network bridge"
Видно, как daemon монтирует файловую систему, обрабатывает вызов API и настраивает сеть.
Возврат конфигурации:
docker rm -f logdemo
sudo cp /etc/docker/daemon.json.bak /etc/docker/daemon.json 2>/dev/null || sudo rm /etc/docker/daemon.json
sudo systemctl restart docker
docker info --format 'Debug: {{.Debug}}'
Debug: false
Не оставляйте
debug: trueнадолго: журнал растёт быстро, и на нагруженной машине это заметно расходует диск.
Задание 8. Диагностика: daemon не стартует [диаг.]
Постановка. Воспроизведите и почините ситуацию: после изменения конфигурации Docker перестал запускаться.
- Внесите в
daemon.jsonзаведомо некорректный JSON (например, лишнюю запятую перед закрывающей скобкой). - Перезапустите daemon.
- Не заглядывая в файл, определите причину по логам.
- Исправьте.
Ожидаемый результат. Описание последовательности диагностики: какие команды выполнялись и что показала каждая.
Разбор
Воспроизведение:
sudo tee /etc/docker/daemon.json > /dev/null <<'EOF'
{
"log-driver": "local",
}
EOF
sudo systemctl restart docker
Job for docker.service failed because the control process exited with error code.
See "systemctl status docker.service" and "journalctl -xeu docker.service" for details.
Шаг 1. Статус службы.
systemctl status docker.service --no-pager
● docker.service - Docker Application Container Engine
Active: failed (Result: exit-code) since ...
Process: 190234 ExecStart=/usr/bin/dockerd ... (code=exited, status=1/FAILURE)
failed, exit code 1. Знаем, что daemon стартовал и сразу упал.
Шаг 2. Логи — последние строки перед падением.
sudo journalctl -u docker.service -n 20 --no-pager
dockerd[190234]: unable to configure the Docker daemon with file /etc/docker/daemon.json:
invalid character '}' looking for beginning of object key string
systemd[1]: docker.service: Main process exited, code=exited, status=1/FAILURE
Причина названа прямо: некорректный JSON в конфигурации, и даже указан символ.
Шаг 3. Подтверждение.
python3 -m json.tool /etc/docker/daemon.json
Expecting property name enclosed in double quotes: line 3 column 1 (char 30)
Номер строки и колонки.
Шаг 4. Исправление.
sudo tee /etc/docker/daemon.json > /dev/null <<'EOF'
{
"log-driver": "local"
}
EOF
sudo dockerd --validate
sudo systemctl restart docker
systemctl is-active docker
configuration OK
active
Методика, которую стоит запомнить. Она одинакова для любого отказа службы:
systemctl status <unit>— факт и код отказа.journalctl -u <unit> -n 30— последние строки перед падением; причина почти всегда там.- Проверить конкретную гипотезу отдельным инструментом (здесь — валидатор JSON).
- Исправить и подтвердить проверкой, а не предположением.
Правило на будущее: проверяйте daemon.json до перезапуска, а не после. Команды python3 -m json.tool и sudo dockerd --validate занимают секунду и избавляют от неработающего Docker.
Очистка после раздела
# ВНИМАНИЕ: НЕ `docker ps -aq | xargs -r docker rm -f`.
# Такая строка удаляет ВСЕ container'ы на машине, включая чужие:
# базу коллеги, кластер kind, работающий стенд. Проверено дорого —
# при подготовке курса она снесла кластер, поднятый для раздела 18.
# Удаляем только то, что создали в этом разделе.
docker ps -aq --filter 'ancestor=hello-world' --filter 'ancestor=alpine' \
| xargs -r docker rm -f
docker rmi hello-world alpine 2>/dev/null || true
docker system df
Если вы называли container'ы по образцу из урока, надёжнее фильтровать по имени: docker ps -aq --filter 'name=ПРЕФИКС' | xargs -r docker rm -f.
Проверьте, что daemon.json находится в том состоянии, в котором вы хотите его оставить:
cat /etc/docker/daemon.json 2>/dev/null || echo "конфигурация по умолчанию"
docker info --format 'Debug: {{.Debug}} Logging: {{.LoggingDriver}}'
Критерии завершения
Раздел закрыт, когда выполнены все обязательные задания и вы можете без подсказок ответить на вопросы из MAIN.md раздела.
Дальше: Quiz 01.
Навигация
← Предыдущий материал
Вернуться к разделу
Следующий раздел → Основы Containerization
Главное оглавление