Лекция 2. Сети, данные, Dockerfile и жизненный цикл процесса
ЛЕКЦИЯ № 2
Сети, данные, Dockerfile и жизненный цикл процесса
Дисциплина: системное администрирование.
Лекционный материал: сети и хранилища Docker, сборка образов и управление процессами контейнера.
Редакция от 8 октября 2026 года.
Содержание и цели лекции
Лекция продолжает изучение Docker и предполагает знакомство с понятиями image, container, registry, Docker Engine, namespaces и cgroups. Рассматриваются сети, постоянные данные, сборка собственных образов и корректное завершение главного процесса контейнера.
Учебная среда — Windows с Docker Desktop в режиме Linux containers. Рядом с Bash-блоками приведены варианты PowerShell или пояснения. Sudo применяется только в соответствующей отдельной Linux-установке и не добавляется в PowerShell. Имена создаваемых объектов и порты должны быть свободны. Контейнеры, сети и тома проверяются через docker ps -a, docker network ls и docker volume ls. Порт может занимать и обычная программа Windows: перечисленные команды Docker её не показывают. Команды очистки относятся только к созданным учебным объектам.
Перед практикой порт 8080 освобождается после первой лекции: если её lesson1-web ещё работает, сначала выполняется docker stop lesson1-web. Удалять чужой контейнер или завершать неизвестную программу Windows ради освобождения порта не требуется: выбирается другой свободный порт и последовательно меняется адрес проверки.
Цели изучения
- Понимать, как Docker подключает контейнер к сети и чем пользовательская bridge-сеть отличается от сети bridge по умолчанию.
- Различать внутренний порт контейнера, опубликованный порт хоста и межконтейнерное соединение.
- Осознанно выбирать между writable layer, volume, bind mount и tmpfs.
- Понимать build context, слои образа, кеш сборки и назначение .dockerignore.
- Различать RUN, CMD и ENTRYPOINT, а также shell- и exec-формы команд.
- Понимать назначение multi-stage builds, USER, HEALTHCHECK и безопасной передачи секретов при сборке.
- Понимать особую роль PID 1, механизм docker stop, сигналы, restart policy и отличие «процесс работает» от «сервис здоров».
- Уметь выполнить базовую диагностику проблем сети, хранения данных, сборки и завершения контейнера.
Windows-проект: в Проводнике создать docker-lesson2 и папки backup, bind-site, cache, curl, multi и project; внутри project создать site. Каждый самостоятельный эксперимент открывается в PowerShell из нужной папки. Для bind mount и backup терминал открывается в корне docker-lesson2, для сборки — в папке соответствующего Dockerfile.
Маршрут изучения и учебное окружение
Материал рассчитан на несколько занятий. Все темы сохраняются: сети и диагностика; данные и восстановление; сборка и кеш; управление процессами и работоспособностью; многоэтапные сборки и секреты; итоговый проект. Границы занятий выбираются по завершённым экспериментам, а не по объёму текста.
Общий Windows-проект — выбранная в Проводнике папка docker-lesson2. Для отдельно выбранного варианта Bash WSL используется $HOME/docker-lesson2; файлы этих проектов не смешиваются. Объекты Docker имеют префикс lesson2-. Примеры выполняются последовательно; для самостоятельного запуска отдельного эксперимента сначала выполняется его подготовка. При продолжении работы на другом занятии сохраняются Docker-объекты и файлы предыдущих шагов. Подготовка не перезаписывает чужие проекты: если каталог уже занят, выбирается другой учебный путь.
На хосте: PowerShell или Bash.
docker ps -a --filter name=lesson2-
docker network ls --filter name=lesson2-
docker volume ls --filter name=lesson2-
Перед первым выполнением имена должны быть свободны. Найденные объекты сначала идентифицируются; удаляются только объекты прежнего выполнения этой лекции. Bash-блоки ниже выполняются в интегрированном WSL либо на отдельном Linux-сервере. Работа в оболочке контейнера помечена отдельно, а содержимое Dockerfile и программ сохраняется в указанном файле.
Порядок работы с практическими разделами
Каждый эксперимент проходит семь шагов: определить понятия → объяснить задачу на примере → подготовить папки и файлы → выполнить команды → проверить ожидаемый результат → объяснить причину результата → ответить на вопрос для самопроверки. Подготовка выполняется Проводником и редактором; команды Docker вводятся в указанной оболочке. Практика продолжается после проверки предыдущего шага, а не после простого копирования блока.
Если результат отличается, фиксируются команда, текст ошибки, состояние контейнера и выбранная среда. Затем используется соответствующий диагностический раздел. Для отчёта достаточно сохранить команды, существенную часть вывода и объяснение: какой объект изменился и почему.
Windows и Docker Desktop: выбор среды работы
Учебные примеры запускают Linux-контейнеры через Docker Desktop на Windows. Windows-контейнеры — другой режим; образы nginx:alpine и alpine относятся к Linux. В терминале проверяются доступ к Engine и выбранный режим:
На хосте: PowerShell или Bash.
docker version
docker context show
docker info --format '{{.OSType}}'
Ожидается ответ linux. При ошибке соединения сначала проверяется, что Docker Desktop запущен. Windows Terminal — приложение терминала: открытая вкладка может содержать PowerShell или Bash Ubuntu, и это определяет синтаксис команд.
| Место выполнения | Что запускается | Пример пути |
|---|---|---|
| PowerShell в Windows | Docker CLI и команды PowerShell | C:\Users\student\docker-lesson2 |
| Ubuntu в WSL 2 с интеграцией Docker Desktop | Docker CLI и Bash-команды | /home/student/docker-lesson2 |
| Оболочка внутри контейнера | Linux-команды контейнерного окружения | /data, /usr/share/nginx/html |
Для Bash-примеров используется терминал пользовательского дистрибутива WSL 2, а не системный дистрибутив docker-desktop. В Docker Desktop включается интеграция выбранного дистрибутива: Settings → Resources → WSL Integration. Наличие backend WSL 2 само по себе не означает наличие Ubuntu с рабочей интеграцией. В PowerShell Docker можно использовать без Ubuntu. Подробнее: Docker Desktop и WSL 2.
PowerShell- и Bash-варианты выполняются на выбор. При обращении к одному Docker Engine они используют общие контейнеры, сети и тома: повторное выполнение второго варианта может вызвать конфликт имён. В этих примерах Windows-папка и отдельная папка /home в WSL содержат разные наборы файлов. Через /mnt/c или /mnt/f WSL может обращаться к той же Windows-папке; это не новая копия проекта. Основной маршрут практики ниже — PowerShell и папки Windows. Bash-блоки показывают альтернативный синтаксис. При выборе Bash необходимые папки и файлы также готовятся заранее: Windows-проект можно открыть через /mnt/c или /mnt/f, а отдельный Linux-проект — через Проводник по пути \\wsl.localhost к своему дистрибутиву и редактор. Для сборки терминал должен быть открыт в папке соответствующего Dockerfile; путь Windows C:\… нельзя вставлять в Bash как Linux-путь. Docker context show показывает выбранный контекст, а docker info — сведения о сервере. Имена контекстов в Windows и WSL могут отличаться даже при обращении к одному Engine; совпадение имени контекста само по себе также не доказывает совпадение серверов.
Как читать блоки команд
| Действие | Bash / WSL | PowerShell |
|---|---|---|
| Перенос команды | Обратная косая черта \ в конце строки | Одна строка либо обратный апостроф `; после него не должно быть пробелов |
| HTTP-запрос | curl URL | curl.exe URL — явный запуск утилиты, независимо от алиаса curl |
| Создание каталога | В Linux-примере каталог готовится заранее | Создать папку в Проводнике |
| Просмотр файла | cat FILE | Get-Content FILE |
| Пауза | sleep 10 | Start-Sleep -Seconds 10 |
| Текущий каталог | $PWD | (Get-Location).Path |
| Подготовка файлов | Содержимое сохраняется в указанном файле | Создать файл в редакторе и сохранить в папке проекта |
Команды Dockerfile и исходный код программы не являются командами PowerShell. В блоках с пометкой «внутри контейнера» сохраняется Linux-синтаксис: например, после docker exec -it … sh команды cat, ps и exit выполняются контейнерной оболочкой. Строка после docker … sh -c также обрабатывается Linux-оболочкой внутри контейнера, даже если Docker CLI запущен из PowerShell.
В PowerShell 5.1 символы && и || вне строки аргумента не поддерживаются как Bash-операторы. Встроенные двойные кавычки в аргументах внешних программ тоже могут передаваться иначе, чем в PowerShell 7. Ниже PowerShell-примеры используют простые аргументы; сложные Bash-блоки выполняются в WSL, а не копируются без изменений в PowerShell.
Подготовка папок и файлов средствами Windows
Папки создаются в Проводнике. В нужной папке выбирается «Открыть в терминале» и вкладка PowerShell; если терминал открылся в другом месте, расположение проверяется через Get-Location. Dockerfile, .dockerignore, HTML и исходники создаются в обычном редакторе и сохраняются в указанную папку. Команды создания папок и записи файлов PowerShell в этой лекции не требуются.
В Проводнике включается показ расширений: файл должен называться Dockerfile, а не Dockerfile.txt, и .dockerignore, а не .dockerignore.txt. В редакторе выбирается UTF-8; для скриптов и конфигурации предпочтительно без BOM. Если Блокнот добавляет .txt, при сохранении выбирается тип «Все файлы» и проверяется итоговое имя в Проводнике. Для Linux shell-скриптов также выбирается LF; иначе возможны ошибки /bin/sh^M или not found. Перед сохранением учебного примера проверяется, что файл не содержит чужой работы.
1. От запуска контейнера к эксплуатации сервиса
Создание контейнера и просмотр его состояния, логов и конфигурации — основа работы с Docker. Для эксплуатации сервиса требуются дополнительные решения: веб-приложению нужен сетевой доступ, базе данных — постоянное хранилище, а сборка приложения должна быть воспроизводимой. При остановке процесс должен корректно завершать работу и оставлять данные в согласованном состоянии.
Контейнер является частью серверной системы. При его эксплуатации необходимо определить, кто может подключаться к сервису, где находятся данные, как собирается образ и что происходит при сбое или остановке процесса.
Эксплуатация контейнера требует управления как минимум четырьмя независимыми аспектами: сетью, постоянными данными, воспроизводимой сборкой образа и жизненным циклом главного процесса.
Переход от тестового nginx-контейнера к работающему сервису требует настройки портов, DNS, конфигурации, логов, хранения данных, перезапуска и обновления.
2. Сети Docker подробно
Процесс в контейнере использует обычные механизмы Linux-сети: интерфейсы, IP-адреса, маршруты, DNS и сокеты. Сетевой namespace изолирует его сетевое окружение, а Docker подготавливает интерфейсы и соединяет контейнерную сеть с сетью хоста.
Docker Desktop на Windows. Опубликованный порт открывается из Windows-браузера по http://127.0.0.1:8080. Внутри контейнера localhost означает сам контейнер. Для обращения из контейнера к сервису Windows-хоста используется host.docker.internal и порт этого сервиса. Для связи контейнеров используются их сетевые имена. Bridge и veth принадлежат Linux-среде Docker Desktop, а не обычным интерфейсам Windows. IP контейнера не является основным адресом доступа из Windows. Документация сети.
Основные сетевые понятия и путь запроса
| Понятие | Объяснение | Связь с практикой |
|---|---|---|
| Сетевой интерфейс | Точка подключения сетевого окружения к сети | ip addr показывает интерфейсы контейнера и их адреса |
| Подсеть | Группа адресов, определяемая сетевым префиксом | Docker выдаёт контейнерам адреса из настроенного диапазона |
| Шлюз | Узел для передачи трафика в другие сети | Маршрут по умолчанию виден в ip route |
| DNS | Система получения сетевых адресов по именам | Имя lesson2-web разрешается в общей пользовательской сети |
| Сетевой alias | Дополнительное имя контейнера в определённой сети | Удобен для устойчивого имени зависимости; не является названием образа |
| Сокет | Точка сетевого обмена процесса | Слушатель nginx принимает TCP-соединения на порту 80 |
При обращении к http://lesson2-web клиент сначала получает IP по имени, затем устанавливает TCP-соединение к порту 80 и отправляет HTTP-запрос. DNS не передаёт страницу и не запускает приложение. Если имя найдено, а nginx ещё не слушает, разрешение имени пройдёт, но HTTP-запрос не получит ожидаемый ответ.
Имя lesson2-web
↓ DNS в общей Docker-сети
IP контейнера
↓ TCP-соединение к порту 80
HTTP-запрос к nginx
↓ чтение страницы
HTTP-ответ клиенту
Имена и aliases относятся к конкретной сети. Контейнеры одной пользовательской bridge-сети получают возможность находить друг друга по сетевым именам. Адреса, префиксы и маршруты из вывода команд не нужно переносить в конфигурацию как постоянные значения. В примерах неизвестное имя, недоступный порт и ошибочный HTTP-ответ рассматриваются как разные этапы диагностики. Сеть Docker.
Для самопроверки: Какие этапы уже проверены успешным nslookup, а какие ещё нужно проверить HTTP-запросом? Почему открытый порт хоста не требуется клиенту в общей сети?
2.1. Что видит процесс внутри контейнера
Сетевое окружение можно изучить во временном контейнере Alpine. Команды ip addr и ip route выполняются внутри его интерактивной оболочки; exit завершает оболочку и удаляет контейнер благодаря --rm. Состав утилит зависит от выбранного образа.
На хосте: PowerShell или Bash.
docker run --rm -it alpine:latest sh
После появления приглашения sh — внутри контейнера:
ip addr
ip route
cat /etc/resolv.conf
exit
В типичной bridge-сети контейнер получает виртуальный Ethernet-интерфейс, адрес из подсети Docker и маршрут через виртуальный шлюз. На стороне хоста этому интерфейсу соответствует другая сторона пары veth. Таким образом, сетевой namespace контейнера не «рисует» сеть программно в приложении - он использует реальные сетевые механизмы ядра Linux.
В обычном режиме bridge контейнер имеет собственный сетевой namespace. Внутри него процесс видит обычный сетевой интерфейс, IP-адрес, таблицу маршрутизации и DNS, но эти объекты изолированы от сетевого пространства хоста.
Ожидаемый результат: Внутри Alpine видны интерфейсы, маршрут по умолчанию и resolv.conf. После exit временный контейнер удаляется. IP и названия интерфейсов зависят от конфигурации.
2.2. Bridge-сеть: виртуальный коммутатор внутри Docker-хоста
На одном Docker-хосте самый распространённый вариант - bridge driver. Упрощённо его удобно представлять как программный коммутатор. Контейнеры подключаются к мосту виртуальными интерфейсами. Docker создаёт адресное пространство и правила, которые позволяют контейнерам обмениваться трафиком внутри сети, а исходящему трафику - выходить наружу через хост.
Docker host
eth0 -------------------------- LAN / Internet
|
[routing + firewall + NAT]
|
[Docker bridge: br-xxxx]
| | |
veth veth veth
| | |
lesson2-web app db
172.x.x.2 172.x.x.3 172.x.x.4
Важно не воспринимать адреса 172.x.x.x как фиксированное правило. Docker выбирает подсети в зависимости от конфигурации, а пользователь может задать собственный subnet. Привязывать конфигурацию приложения к автоматически выданному IP контейнера - плохая идея. Контейнер можно пересоздать, и адрес изменится.
Запуск и готовность — разные шаги. Команды выполняются последовательно, с проверкой результата каждого шага. Ответ docker run -d или docker start ещё не означает, что приложение готово принимать HTTP. При первом Connection refused нужно подождать несколько секунд и повторить запрос. Если ответ не появляется, проверяются docker ps -a и docker logs для этого контейнера. Для образа с HEALTHCHECK ожидается healthy, а не только running; проверка состояния повторяется через inspect. Сетевой запрос проверяет именно указанный адрес и порт.
2.3. Почему пользовательская bridge-сеть лучше default bridge
Docker автоматически создаёт сеть с именем bridge. Она нужна для совместимости и простейших сценариев. Но для проекта лучше создавать отдельную пользовательскую сеть. Главное практическое преимущество - нормальное обнаружение контейнеров по именам и явная граница изоляции. Кроме того, каждое приложение можно поместить в собственную сеть и не смешивать все контейнеры хоста в одном общем сегменте.
На хосте: PowerShell или Bash.
docker network create lesson2-net
docker network ls
docker network inspect lesson2-net
В пользовательской сети запускается nginx, а временный контейнер используется как HTTP-клиент. Порт nginx на хост пока не публикуется.
Bash / WSL:
docker run -d --name lesson2-web --network lesson2-net nginx:alpine
docker run --rm --network lesson2-net busybox \
wget -qO- http://lesson2-web | head
PowerShell:
docker run -d --name lesson2-web --network lesson2-net nginx:alpine
docker run --rm --network lesson2-net busybox wget -qO- http://lesson2-web | Select-Object -First 10
Клиент обращается к имени lesson2-web. Docker встроенно разрешает имена контейнеров в пользовательской сети. Это принципиально важнее, чем знание текущего IP. При замене контейнера сервис можно оставить под тем же сетевым именем, а конфигурация других компонентов не должна зависеть от старого адреса.
В пользовательской bridge-сети контейнеры могут обращаться друг к другу по DNS-именам. В конфигурации сервисов следует использовать имена и сетевые алиасы, а не автоматически выданные IP-адреса контейнеров.
Публикация -p не требуется для связи контейнеров в одной пользовательской bridge-сети. Она предоставляет доступ через адрес и порт Docker-хоста с учётом сетевых правил и настроек фильтрации.
Ожидаемый результат: В списке появилась lesson2-net. Клиент из этой сети получает HTML nginx по имени lesson2-web, хотя порты сервера на хост не опубликованы.
Для самопроверки: Почему клиент получил HTML по имени lesson2-web без -p? Что изменится, если запустить его в другой Docker-сети?
2.4. Внутренний порт и published port - это разные уровни
Если nginx слушает TCP 80 внутри контейнера, это свойство процесса и сетевого namespace контейнера. Команда -p создаёт отображение между адресом и портом хоста и портом контейнера. В стандартном режиме bridge Docker использует правила фильтрации и трансляции адресов. Поэтому запись -p 8080:80 нужно читать как «принимать подключения на порту 8080 Docker-хоста и направлять их на порт 80 контейнера».
Bash / WSL:
docker run -d --name lesson2-public \
--network lesson2-net \
-p 127.0.0.1:8080:80 nginx:alpine
curl http://127.0.0.1:8080
PowerShell:
docker run -d --name lesson2-public --network lesson2-net -p 127.0.0.1:8080:80 nginx:alpine
curl.exe http://127.0.0.1:8080
В учебном запуске выбран loopback. Запись -p 8080:80 без адреса показана в объяснении как отдельный вариант предоставления внешнего доступа.
Здесь есть важная деталь безопасности. Если адрес хоста слева не указан, Docker по умолчанию публикует порт на всех адресах хоста. На сервере с внешним интерфейсом это может неожиданно открыть сервис в локальную сеть или Интернет. Для административной панели, тестовой БД или внутреннего API часто безопаснее сначала привязать порт к loopback.
Bash / WSL:
docker run -d --name lesson2-local \
-p 127.0.0.1:8081:80 nginx:alpine
PowerShell:
docker run -d --name lesson2-local -p 127.0.0.1:8081:80 nginx:alpine
Форма -p HOST_IP:HOST_PORT:CONTAINER_PORT задаёт адрес и порт на Docker-хосте, через которые публикуется порт контейнера. Если HOST_IP не указан, публикация обычно действует на всех адресах хоста.
Не путайте публикацию порта с инструкцией EXPOSE в Dockerfile. EXPOSE является метаданными и документирует предполагаемый порт приложения. Она не создаёт внешний listener на хосте и не заменяет -p.
Ожидаемый результат: curl на хосте получает страницу через порт 8080. В docker ps видно, какой адрес хоста связан с портом 80 контейнера.
2.5. Межконтейнерный трафик не нужно выводить наружу
Представим приложение из трёх компонентов: reverse proxy, backend и база данных. Пользователь должен подключаться только к reverse proxy. Backend должен быть доступен reverse proxy, а база - backend. Публиковать PostgreSQL на 0.0.0.0 только потому, что backend должен к нему подключиться, не требуется. Контейнеры общаются по внутренней Docker-сети.
Internet / LAN
|
host:443 <- опубликован только входной сервис
|
[frontend network]
|
reverse-proxy ----- backend
|
[backend network]
|
database
(порт БД наружу не публикуется)
Такой подход уменьшает поверхность атаки и упрощает firewall-модель. В дальнейшем Docker Compose позволит описывать эти сети декларативно, но сетевой принцип останется тем же.
2.6. Один контейнер может состоять в нескольких сетях
Сетевую сегментацию можно строить так, чтобы промежуточный сервис был подключён сразу к двум сетям. Например, frontend видит reverse proxy и приложение, а backend-сеть соединяет приложение с БД. Контейнер базы не обязан находиться во frontend-сети.
На хосте: PowerShell или Bash.
docker network create lesson2-front
docker network create lesson2-back
docker run -d --name lesson2-db --network lesson2-back redis:alpine
docker run -d --name lesson2-app --network lesson2-back nginx:alpine
docker network connect lesson2-front lesson2-app
docker network inspect lesson2-front
docker network inspect lesson2-back
Системному администратору важно мыслить не только портами, но и границами доверия. Сеть Docker - это один из способов ограничить, какие группы контейнеров вообще имеют прямую связность друг с другом.
Ожидаемый результат: lesson2-app присутствует в двух сетях, lesson2-db — только в lesson2-back. Пример показывает состав сетей; nginx здесь не настроен обращаться к Redis как приложение.
2.7. Другие сетевые драйверы: знать назначение, но не использовать без причины
| Драйвер | Идея | Типичный сценарий |
|---|---|---|
| bridge | Изолированная сеть контейнеров на одном Docker- хосте | Большинство одиночных серверов и Compose-проектов |
| host | Контейнер использует сетевой namespace хоста | Редкие случаи, где нужна минимальная сет. абстракция/особая производительность |
| overlay | Сеть между несколькими Docker-хостами | Swarm и многонодовые сценарии |
| macvlan | Контейнер выглядит в L2-сети как отдельное устройство с MAC | Интеграция с legacy-сетями и специфическими требованиями |
| Драйвер | Идея | Типичный сценарий |
|---|---|---|
| ipvlan | Похожая интеграция с физической сетью с иной L2- моделью | Сети с ограничениями по MAC/контролем адресации |
| none | Сетевой интерфейс Docker не подключается | Полная изоляция сети для специальной задачи |
Bridge — один из сетевых драйверов Docker. Выбор драйвера определяется требованиями к связности, маршрутизации и изоляции, а не сложностью технологии. На обычном Linux-хосте режим host использует сетевой namespace хоста. Для Docker Desktop это отдельная возможность со своими ограничениями, поэтому схема обычного Linux-сервера не переносится на Windows буквально.
Границы применения драйверов на Windows. Практика этой лекции использует bridge. Host networking в Docker Desktop доступен начиная с версии 4.34 и включается отдельно в настройках; реализация поддерживает TCP/UDP и не даёт контейнеру прямого доступа ко всем интерфейсам Windows. Macvlan на Docker Desktop для Windows не поддерживается. Overlay требует отдельного многонодового окружения, например Swarm. Таблица объясняет назначение драйверов, а не предлагает выполнять все варианты на учебном ПК. Host networking, Macvlan.
2.8. Базовая диагностика сети
Когда контейнер «не видит другой контейнер», не нужно сразу перезапускать Docker. Диагностика строится послойно: существует ли контейнер, к какой сети он подключён, какое имя используется, слушает ли приложение нужный порт, есть ли публикация, если доступ идёт извне, и на каком адресе хоста она сделана.
На хосте: PowerShell или Bash.
docker ps
docker network ls
docker network inspect lesson2-net
docker inspect lesson2-web
docker port lesson2-public
# проверить DNS/HTTP из той же сети
docker run --rm --network lesson2-net busybox nslookup lesson2-web
docker run --rm --network lesson2-net busybox wget -qO- http://lesson2-web
docker exec -it lesson2-web sh
После появления приглашения sh — внутри контейнера: выполняется Linux-команда проверки сокетов, затем exit возвращает в PowerShell или Bash хоста.
ss -lnt 2>/dev/null || netstat -lnt 2>/dev/null || true
exit
Алгоритм диагностики сети: 1) проверить состояние контейнера; 2) проверить членство в Docker-сети; 3) проверить DNS-имя; 4) проверить, что приложение слушает нужный адрес и порт; 5) при внешнем доступе проверить публикацию порта и firewall хоста.
Эксперимент: DNS работает в общей сети
Используется lesson2-web из раздела 2.3. Сравниваются запросы клиента в общей сети и временного клиента в другой пользовательской сети.
На хосте: PowerShell или Bash.
docker run --rm --network lesson2-net busybox nslookup lesson2-web
docker run --rm --network lesson2-net busybox wget -qO- http://lesson2-web
docker network create lesson2-isolated
docker run --rm --network lesson2-isolated busybox nslookup lesson2-web
docker network rm lesson2-isolated
Ожидаемый результат: В общей сети имя разрешается и HTTP-запрос проходит. В другой сети это имя обычно не разрешается; ошибка nslookup здесь ожидаема. Проверка относится к встроенному DNS Docker, а не ко всем возможным маршрутам или внешним DNS-записям.
В диагностическом блоке выход из docker exec выполняется через exit. При отсутствии ss и netstat команда может не дать вывода; это отсутствие утилиты, а не доказательство отсутствия слушателя. Сведения о публикации проверяются с хоста через docker port.
3. Хранение данных: writable layer, volumes, bind mounts и tmpfs
Данные, которые должны переживать удаление контейнера, хранят вне его writable layer. Образ состоит из неизменяемых слоёв, а при создании контейнера поверх них появляется собственный записываемый слой. Изменение файла влияет на состояние контейнера, сохраняя исходный image.
Для временных файлов это нормально. Но writable layer неудобен как долгосрочное хранилище: его жизненный цикл связан с контейнером, он усложняет перенос данных и использует storage driver поверх слоистой файловой системы. Удалили контейнер - удалили его writable layer. Поэтому постоянное состояние выносят в отдельное хранилище.
Writable layer предназначен для изменений, относящихся к конкретному экземпляру контейнера. Постоянное прикладное состояние следует хранить через mounts, жизненный цикл которых не зависит от пересоздания контейнера.
Пути Windows и Linux. Источник bind mount в PowerShell — существующий Windows-путь, например C:\Users\student\docker-lesson2\bind-site. Назначение в контейнере остаётся Linux-путём /data или /usr/share/nginx/html. В WSL путь /mnt/c обозначает диск C:, а /home — Linux-файловую систему дистрибутива. Docker Desktop предоставляет контейнерам доступ к файлам Windows. Named volume управляется Docker, его не следует искать как обычную папку проекта в Проводнике. Bind mounts.
Что означает подключить хранилище
Монтирование — mount делает выбранное хранилище доступным по пути внутри контейнера. Источник — source указывает, откуда берутся данные; точка подключения — destination/target определяет, где процесс их увидит. Приложение обращается к обычному пути, например /data, а Docker задаёт, какое хранилище стоит за ним.
Источник Контейнер
Windows-папка bind-site ── bind mount ─→ /usr/share/nginx/html
Том lesson2-data ── volume ─────→ /data
Временная память ── tmpfs ──────→ /run
| Поле --mount | Назначение | Пример |
|---|---|---|
| type | Тип подключения | volume, bind или tmpfs |
| src / source | Источник; у volume — имя, у bind — путь | lesson2-data или полный путь Windows |
| dst / destination / target | Путь назначения внутри контейнера | /data |
| ro / readonly | Запрет записи через это подключение из контейнера | Можно читать файл, но нельзя изменить его через mount |
У tmpfs нет обычного файлового источника src. Именованный volume не является именем Windows-папки: расположением его файлов управляет Docker. Для bind mount выбирается заранее подготовленная папка Windows. Два контейнера могут видеть один volume по разным внутренним путям; одинаковое имя источника связывает их с одними данными.
Read-only определяет возможность записи через подключение, а не срок хранения. Volume, подключённый ro, остаётся volume; writable layer не становится постоянным из-за возможности записи. Изменение владельцем файла в папке Windows может стать видно контейнеру с read-only bind mount: запрет действует на запись со стороны контейнера. Правила прав доступа рассматриваются отдельно ниже.
Для самопроверки: Какая часть type=volume,src=lesson2-data,dst=/data,ro задаёт постоянное хранилище, какая — путь приложения, а какая — ограничение записи?
3.1. Named volume: хранилище, которым управляет Docker
Named volume создаётся и учитывается Docker daemon. Администратор работает с логическим именем, а не привязывает приложение к конкретному каталогу проекта. Это удобно для данных БД и других сервисов, которые должны переживать пересоздание контейнера.
На хосте: PowerShell или Bash.
docker volume create lesson2-data
docker volume ls
docker volume inspect lesson2-data
Следующий пример записывает файл в volume и читает его из нового контейнера после удаления первого:
Bash / WSL:
docker run --rm \
--mount type=volume,src=lesson2-data,dst=/data \
alpine sh -c 'date > /data/created.txt && cat /data/created.txt'
docker run --rm \
--mount type=volume,src=lesson2-data,dst=/data,ro \
alpine cat /data/created.txt
PowerShell:
docker run --rm --mount type=volume,src=lesson2-data,dst=/data alpine sh -c 'date > /data/created.txt && cat /data/created.txt'
docker run --rm --mount type=volume,src=lesson2-data,dst=/data,ro alpine cat /data/created.txt
Контейнеры в этих командах разные, а volume один. Именно это отделение жизненного цикла является ключевой идеей. Удаление контейнера не удаляет named volume автоматически. Поэтому на сервере нужно отдельно следить за накоплением неиспользуемых volumes.
На хосте: PowerShell или Bash.
docker volume ls
# справка по очистке томов
docker volume prune --help
# в современных версиях без --all удаляются неиспользуемые анонимные тома
# docker volume prune
# --all включает также неиспользуемые именованные тома
# docker volume prune --all
Named volume имеет собственный жизненный цикл. Он остаётся после удаления контейнера, пока не будет удалён явно или средствами очистки Docker.
Если проект ведётся в Bash WSL, для активной работы Linux-инструментами можно выбрать папку /home пользователя WSL. Её не следует смешивать с Windows-проектом: /mnt/c означает другой способ доступа к Windows-диску. Рекомендации для WSL.
3.2. Синтаксис --mount и -v
Docker поддерживает короткую запись -v и явный синтаксис --mount. В --mount тип, источник и точка подключения заданы отдельными полями: type=volume, src=..., dst=.... Оба синтаксиса позволяют подключать volumes и bind mounts; выбор типа определяется параметрами подключения.
Bash / WSL:
# Оба варианта подключают один volume
docker run --rm -v lesson2-data:/data alpine ls -la /data
docker run --rm \
--mount type=volume,src=lesson2-data,dst=/data \
alpine ls -la /data
PowerShell:
# Оба варианта подключают один volume
docker run --rm -v lesson2-data:/data alpine ls -la /data
docker run --rm --mount type=volume,src=lesson2-data,dst=/data alpine ls -la /data
3.3. Bind mount: контейнер получает конкретный путь хоста
Bind mount нужен тогда, когда важен именно путь на хосте. Типичные примеры - конфигурационный файл, каталог сайта, сертификаты, каталог с исходниками в разработке или директория, которую должен одновременно видеть контейнер и обычные процессы сервера.
В Проводнике открыть bind-site, редактором создать index.html со следующей страницей. PowerShell для команд mount открыть в корне docker-lesson2.
Файл bind-site/index.html: создать или открыть в редакторе, сохранить следующее содержимое.
<h1>Docker: bind mount</h1>
$BindPath = (Resolve-Path '.\bind-site').Path
docker run -d --name lesson2-bind -p 127.0.0.1:8082:80 --mount "type=bind,src=$BindPath,dst=/usr/share/nginx/html,ro" nginx:alpine
Параметр --mount с путём заключается в кавычки целиком, чтобы путь с пробелами передавался одним аргументом. Назначение /usr/share/nginx/html относится к Linux-контейнеру.
Здесь контейнер видит файлы прямо из каталога хоста. Если изменить index.html на хосте, изменение сразу станет видно nginx. Это удобно, но означает тесную зависимость от структуры каталогов конкретной машины.
Главный риск bind mount - запись в файловую систему хоста. По умолчанию bind mount доступен контейнеру на запись. Если сервису нужно только читать конфигурацию или контент, добавляйте readonly. Контейнер с широким bind mount может случайно или намеренно изменить критические файлы хоста в пределах выданных ему прав.
Bind mount связывает конкретный путь Docker-хоста с путём внутри контейнера. Он удобен для совместного доступа к файлам, но сильнее привязывает контейнер к хосту и требует особенно внимательно выдавать право записи.
Эксперимент: изменение bind mount без пересборки
Сначала проверить текущую страницу:
curl.exe http://127.0.0.1:8082
Затем изменить файл bind-site/index.html редактором Windows и сохранить:
Файл bind-site/index.html: создать или открыть в редакторе, сохранить следующее содержимое.
<h1>Changed on the host</h1>
Повторить HTTP-запрос без пересборки:
curl.exe http://127.0.0.1:8082
Ожидаемый результат: Второй запрос возвращает новую страницу, хотя контейнер и образ не заменялись. Readonly ограничивает запись из контейнера, но не мешает владельцу файлов изменять их на хосте.
Для самопроверки: Почему новая страница стала видна без build? Почему ro не помешал изменить index.html редактором Windows?
3.4. Монтирование поверх непустого каталога скрывает исходное содержимое
Bind mount или подключение непустого volume поверх каталога скрывает исходные файлы этого каталога в образе. Они остаются в image, но контейнер видит содержимое подключённого хранилища. У пустого volume есть особенность: по умолчанию Docker сначала копирует в него существующее содержимое каталога контейнера. Параметр volume-nocopy отключает это первоначальное копирование.
Например, если подключить bind mount пустого каталога к /usr/share/nginx/html, стандартная страница nginx исчезнет из представления контейнера. После пересоздания контейнера без этого mount она снова будет видна, потому что файл оставался в образе.
Mount скрывает нижележащий каталог. Пустой Docker volume по умолчанию предварительно заполняется файлами из каталога контейнера; bind mount такого копирования не выполняет.
3.5. tmpfs: данные нужны процессу, но не должны сохраняться
Для временных файлов, кеша и runtime-данных на Linux можно использовать tmpfs mount. Его содержимое хранится в памяти и исчезает при остановке контейнера. Однако tmpfs может использовать swap: этот механизм сам по себе не гарантирует, что чувствительные данные никогда не попадут на диск.
Bash / WSL:
docker run --rm -it \
--mount type=tmpfs,dst=/run,tmpfs-size=64m \
alpine sh
# для выхода из оболочки контейнера: exit
PowerShell:
docker run --rm -it --mount type=tmpfs,dst=/run,tmpfs-size=64m alpine sh
# для выхода из оболочки контейнера: exit
tmpfs не заменяет volume. Он решает противоположную задачу: подчеркнуть временный характер данных и не сохранять их после жизненного цикла контейнера.
3.6. Как выбрать тип хранения
| Задача | Рекомендуемый вариант | Почему |
|---|---|---|
| Данные PostgreSQL/MySQL | Named volume или специализированное внешнее хранилище | Данные переживают замену контейнера и не зависят от каталога проекта |
| Конфигурация nginx с хоста | Bind mount, обычно ro | Файл должен редактироваться администратором на хосте |
| Исходники во время разработки | Bind mount | Изменения на хосте сразу видны приложению |
| Временный runtime-каталог | tmpfs | Состояние не должно сохраняться |
| Кэш, который не жалко потерять | tmpfs или отдельный volume в зависимости от задачи | Выбор зависит от размера и необходимости переживать рестарт |
| Файлы, встроенные в релиз приложения | Image layer через COPY | Это часть версии приложения, а не внешнее состояние |
3.7. Права доступа: Linux UID/GID и каталоги Windows
Контейнер не отменяет модель Unix permissions. Процесс внутри контейнера работает от некоторого UID и GID. На обычном Linux-хосте в rootful-конфигурации без userns-remap числовые UID/GID процесса сравниваются с владельцами файлов Linux-файловой системы. Для bind mount папки Windows эта модель не описывает NTFS-права напрямую. При user namespaces и rootless применяется сопоставление идентификаторов, поэтому совпадение чисел внутри контейнера и на хосте нельзя считать универсальным правилом. Поэтому типичная проблема «permission denied в контейнере» часто оказывается обычным несовпадением владельца и прав доступа.
На хосте: PowerShell или Bash.
docker exec lesson2-web id
docker exec lesson2-web ps
docker top lesson2-web
# права папки Windows проверяются в Проводнике через Свойства
# посмотреть, каким пользователем образ запускается по умолчанию
docker inspect -f '{{.Config.User}}' nginx:alpine
Не следует лечить такую проблему chmod 777 на всём каталоге. Сначала нужно понять UID процесса, владельца файлов и требуемое направление доступа. Для production-образов желательно запускать приложение от непривилегированного пользователя, а права mount настраивать под него.
docker exec … id показывает пользователя дополнительной команды. Config.User описывает настройку запуска образа и может быть пустым, что означает значение root по умолчанию. Это не доказывает, что все процессы работают от одного пользователя: nginx может запускать master-процесс и worker-процессы с разными правами. Для проверки используется список процессов. В выводе docker top видны идентификаторы среды Docker-хоста.
Для каталога Windows имеют значение NTFS-права и доступ Docker Desktop к файлам. UID/GID внутри контейнера нельзя прямо приравнять к учётной записи Windows. Chmod/chown-примеры обычного Linux-хоста не переносятся на Windows-каталог без учёта способа mount. Для изучения Unix permissions используется каталог Linux-файловой системы WSL либо named volume. Свойства папки Windows проверяются средствами Windows.
3.8. Резервное копирование volume: важна согласованность данных
Named volume можно архивировать через временный контейнер. Для работающей базы данных файловая копия должна быть согласованной: архивирование без остановки или штатного backup-инструмента может создать непригодную для восстановления копию.
# PowerShell открыт в корне docker-lesson2
$BackupPath = (Resolve-Path '.\backup').Path
docker run --rm --mount type=volume,src=lesson2-data,dst=/source,ro --mount "type=bind,src=$BackupPath,dst=/backup" alpine tar czf /backup/lesson2-data.tar.gz -C /source .
Для PostgreSQL, MySQL и других СУБД обычно предпочтительны их штатные механизмы логического или физического резервного копирования, либо снапшот хранилища при соблюдении требований конкретной СУБД. Docker volume отвечает за место хранения, но не знает, когда приложение находится в консистентном состоянии.
Docker volume обеспечивает постоянство файлов, но не гарантирует прикладную согласованность резервной копии. Для баз данных способ backup должен учитывать требования самой СУБД.
3.9. Восстановление архива в новый том
Подготовка: в lesson2-data должен существовать created.txt из раздела 3.1, а в каталоге backup — lesson2-data.tar.gz из раздела 3.8. Имя lesson2-restored должно быть свободно. Восстановление выполняется в отдельный пустой том, сохраняя исходный.
# исходный том и архив уже подготовлены; имя lesson2-restored свободно
$BackupPath = (Resolve-Path '.\backup').Path
docker volume create lesson2-restored
docker run --rm --mount type=volume,src=lesson2-restored,dst=/target --mount "type=bind,src=$BackupPath,dst=/backup,ro" alpine tar xzf /backup/lesson2-data.tar.gz -C /target
docker run --rm --mount type=volume,src=lesson2-restored,dst=/data,ro alpine cat /data/created.txt
docker run --rm --mount type=volume,src=lesson2-data,dst=/original,ro --mount type=volume,src=lesson2-restored,dst=/restored,ro alpine diff /original/created.txt /restored/created.txt
Ожидаемый результат: В новом томе читается прежняя дата; diff не выводит различий и завершает работу с кодом 0. Для этого учебного набора проверка подтверждает совпадение файла. Для базы данных нужен также запуск восстановленной СУБД и проверка прикладных данных.
Восстановление должно проверяться регулярно. Успешный запуск tar не подтверждает пригодность резервной копии приложения; должны быть известны состав копии, способ восстановления, владельцы файлов и согласованность данных.
Для самопроверки: Почему архив восстанавливался в новый том? Какие результаты подтвердили совпадение учебных данных, а чего эта проверка не доказывает для базы данных?
4. Dockerfile глубже: build context, слои, кеш и правильный runtime
В первой лекции Dockerfile был показан как последовательность FROM и COPY. Теперь важно понять, что сборка образа - это отдельный инженерный процесс. От структуры Dockerfile зависят скорость CI, размер образа, безопасность, воспроизводимость и поведение приложения при запуске.
Два значения слова «контекст»
| Понятие | Что выбирается | Где используется |
|---|---|---|
| Docker context | Подключение клиента к определённому Docker Engine | docker context show; управление контейнерами и образами на выбранном сервере |
| Build context | Файлы и другие входные материалы, доступные сборщику | Последний аргумент docker build; COPY и ADD |
Выбор Docker context не означает выбор папки проекта. И наоборот, открытие другой папки в PowerShell не переключает сервер Docker. В команде docker build -t lesson2-service:2.0 . точка задаёт локальную папку контекста сборки, а выбранное подключение определяет среду, к которой обращается клиент.
Следующая схема объясняет контекст итогового проекта; его файлы создаются в разделе 7. Если терминал открыт в project, источником COPY site/ становится именно project/site. Назначение /usr/share/nginx/html/ относится уже к создаваемому Linux-образу.
docker-lesson2/
backup/ ← находится вне контекста project
project/ ← текущая папка терминала; контекст .
Dockerfile
.dockerignore
site/
index.html
COPY site/ /usr/share/nginx/html/
↑ источник в контексте ↑ назначение в образе
Опция -f выбирает файл Dockerfile, но сама по себе не меняет build context. Поэтому запуск из неправильной папки может дать ошибку COPY даже при найденном Dockerfile. Подготовка к сборке включает проверку текущей папки, дерева файлов и .dockerignore. Контекст сборки.
Для самопроверки: Если Dockerfile указан через -f, от какой папки будут отсчитываться пути COPY? Почему переключение Docker context не исправит отсутствующий site/index.html?
4.1. Build context: что Docker вообще имеет право увидеть при сборке
Последний аргумент команды docker build - это build context. В команде docker build -t app:1.0 . точка означает текущий каталог. Файлы внутри контекста могут использоваться инструкциями COPY и ADD. Файл за пределами контекста нельзя просто скопировать относительным путём «на уровень выше», потому что builder не должен произвольно читать всю файловую систему клиента.
project/
├── Dockerfile
├── .dockerignore
├── requirements.txt
├── app/
│ └── main.py
├── .git/
├── logs/
└── .env
Если передать весь каталог как контекст, в него могут попасть история Git, логи, временные файлы, зависимости, ключи и .env. Даже если часть этих файлов не будет скопирована в итоговый слой, само попадание лишних данных в контекст ухудшает скорость и создаёт риск утечки. Для этого используется .dockerignore.
# .dockerignore
.git
.gitignore
*.log
logs/
.env
.env.*
__pycache__/
node_modules/
*.tar
*.tar.gz
*.zip
token.txt
secrets/
.dockerignore исключает лишние файлы из build context до передачи контекста builder. Он уменьшает объём сборки и помогает не передавать случайные секреты и мусор.
4.2. Слои и кеш: порядок инструкций влияет на время сборки
BuildKit старается повторно использовать результат инструкции, если она и её входные данные не изменились. Но как только слой нужно перестроить, все зависящие от него последующие шаги могут также потребовать выполнения. Поэтому часто меняющиеся файлы выгодно копировать позже, а дорогие и редко меняющиеся зависимости - раньше.
В следующем варианте Python-проекта сначала копируется весь проект, а затем устанавливаются зависимости. Изменение любого включённого в COPY файла меняет входные данные шага установки пакетов и препятствует использованию его прежнего кеша.
# Неудачный порядок
FROM python:3.13-slim
WORKDIR /app
COPY . .
RUN pip install --no-cache-dir -r requirements.txt
CMD ["python", "app/main.py"]
Более эффективная структура сначала копирует только файл зависимостей, устанавливает пакеты и лишь затем копирует часто меняющийся исходный код.
# Более удачный порядок
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app/ ./app/
CMD ["python", "app/main.py"]
Если меняется только main.py, слой установки библиотек можно взять из кеша. На одном ноутбуке выигрыш может казаться небольшим, но в CI, где образ собирается десятки раз в день, правильная структура экономит значительное время и сетевой трафик.
Кеш Dockerfile зависит от инструкций и их входных данных. Стабильные и дорогие шаги сборки обычно располагают раньше, а часто меняющиеся исходники - позже.
Эксперимент: какие изменения сбрасывают кеш
Используется отдельный каталог cache. requirements.txt моделирует стабильные зависимости, а RUN с паузой — затратный шаг их подготовки. В этом эксперименте pip не запускается: проверяется порядок шагов Dockerfile без зависимости от внешнего сервиса пакетов.
В Проводнике открыть cache. Создать редактором requirements.txt, main.py и Dockerfile. Requirements.txt в этом эксперименте — обычный текст-модель зависимостей.
Файл cache/requirements.txt: создать или открыть в редакторе, сохранить следующее содержимое.
dependency-version=1
Файл cache/main.py: создать или открыть в редакторе, сохранить следующее содержимое.
print("version 1")
Файл cache/Dockerfile: создать или открыть в редакторе, сохранить следующее содержимое.
FROM alpine:latest
WORKDIR /app
COPY requirements.txt .
RUN echo "preparing dependencies" && sleep 2 && cp requirements.txt installed.txt
COPY main.py .
CMD ["cat", "/app/main.py"]
Открыть терминал в папке cache и выполнить две сборки:
docker build --progress=plain -t lesson2-cache:1 .
docker build --progress=plain -t lesson2-cache:1 .
Изменить main.py редактором на print("version 2"), сохранить и выполнить:
docker build --progress=plain -t lesson2-cache:2 .
Изменить requirements.txt редактором на dependency-version=2, сохранить и выполнить:
docker build --progress=plain -t lesson2-cache:3 .
Ожидаемый результат: При неизменённых входных данных повторная сборка использует CACHED для COPY и RUN. Изменение main.py перестраивает поздний COPY, сохраняя RUN из кеша. Изменение requirements.txt перестраивает его COPY, RUN и зависимые шаги. Первая сборка тоже может частично использовать существующий кеш; время само по себе не является доказательством.
Для самопроверки: Какое изменение сохранило кеш подготовки зависимостей, а какое его сбросило? Какие входные файлы использует соответствующий шаг?
4.3. RUN, CMD и ENTRYPOINT выполняются в разное время
| Инструкция | Когда действует | Назначение |
|---|---|---|
| RUN | Во время docker build | Установить пакеты, скомпилировать программу, изменить файловую систему образа |
| CMD | При docker run | Задать команду или аргументы по умолчанию |
| ENTRYPOINT | При docker run | Задать основной executable контейнера |
RUN не является командой, которая «запустится при старте контейнера». Она выполняется в процессе сборки. Результат попадает в слой образа. CMD и ENTRYPOINT, наоборот, описывают runtime. Именно вокруг неправильного понимания этой разницы возникает много плохих Dockerfile.
Как определяется итоговая команда запуска
Exec-форма записывает программу и аргументы как JSON-массив: список строк в двойных кавычках, разделённых запятыми и заключённых в квадратные скобки. Для разбора удобно отдельно выписать программу и её аргументы. CMD без ENTRYPOINT задаёт команду по умолчанию. При ENTRYPOINT в exec-форме CMD обычно задаёт добавляемые аргументы. Текст после имени образа в run заменяет CMD. Опция --entrypoint меняет программу и сбрасывает прежний CMD; необходимые аргументы задаются явно после образа.
Ниже demo:1 — условное имя для разбора, а не образ, который требуется скачивать. В каждом примере предполагается наличие /bin/echo и /bin/sh; варианты конфигурации таблицы относятся к разным сборкам. Команды в таблице объясняют правила. Практический пример с реальным lesson2-curl:1 следует сразу после таблицы.
| Конфигурация образа | Пример запуска | Итоговая программа и аргументы |
|---|---|---|
| Только CMD ["/bin/echo", "default"] | docker run demo:1 | /bin/echo default |
| Тот же CMD, без ENTRYPOINT | docker run demo:1 /bin/echo changed | /bin/echo changed |
| ENTRYPOINT ["/bin/echo"] и CMD ["default"] | docker run demo:1 | /bin/echo default |
| Те же ENTRYPOINT и CMD | docker run demo:1 changed | /bin/echo changed |
| ENTRYPOINT ["/bin/echo"] и CMD ["default"] | docker run --entrypoint /bin/sh demo:1 -c pwd | /bin/sh -c pwd; прежний default не добавляется |
Параметр Docker --entrypoint располагается до имени образа. После demo:1 в последней строке -c и pwd — аргументы контейнерной оболочки. Эти правила таблицы не переносятся буквально на shell-форму ENTRYPOINT: формы и обработка сигналов объясняются в разделе 4.5.
Для самопроверки: Для ENTRYPOINT ["curl"] и CMD ["https://example.com"] какую часть изменяет URL после имени образа? Чем это отличается от --entrypoint?
4.4. CMD и ENTRYPOINT вместе
Подготовка Windows: открыть папку curl в Проводнике, сохранить в ней следующий Dockerfile редактором и открыть PowerShell в этой папке. Нужен доступ к репозиторию пакетов Alpine и сайтам example.com/example.org.
Удобная модель: ENTRYPOINT задаёт стабильную программу, а CMD - аргументы по умолчанию. Пользователь может заменить CMD аргументами docker run, не меняя сам executable.
FROM alpine:latest
RUN apk add --no-cache curl
ENTRYPOINT ["curl"]
CMD ["https://example.com"]
После сохранения Dockerfile образ собирается и запускается:
На хосте: PowerShell или Bash.
# терминал открыт в папке curl с сохранённым Dockerfile
docker build -t lesson2-curl:1 .
# по умолчанию
docker run --rm lesson2-curl:1
# переопределяем только аргументы CMD
docker run --rm lesson2-curl:1 https://example.org
В этом примере контейнер ведёт себя как самостоятельная команда. Но для серверного приложения модель та же: ENTRYPOINT может быть бинарником приложения, CMD - набором его обычных параметров.
4.5. Exec form и shell form: синтаксис влияет на сигналы
У RUN, CMD и ENTRYPOINT есть shell- и exec-формы. Для долгоживущего процесса особенно важна exec-форма, записанная JSON-массивом. Она запускает executable напрямую. Shell-форма обычно запускает /bin/sh -c, и уже shell создаёт дочерний процесс. Это меняет PID 1 и передачу сигналов.
# Предпочтительно для основного процесса
ENTRYPOINT ["/usr/local/bin/myapp"]
CMD ["--config", "/etc/myapp/config.yml"]
# Требует осторожности: появляется /bin/sh -c
ENTRYPOINT /usr/local/bin/myapp --config /etc/myapp/config.yml
Если нужен entrypoint-script, хорошая практика - в конце скрипта заменить оболочку основным процессом через exec "$@". Тогда приложение становится PID 1 и получает сигналы Docker напрямую.
#!/bin/sh
set -e
# подготовка конфигурации...
exec "$@"
Для основного долгоживущего процесса предпочтительна exec-форма CMD/ENTRYPOINT. Она уменьшает число промежуточных процессов и упрощает корректную доставку Unix-сигналов.
4.6. Конфигурация приложения при запуске
Конфигурация — значения, определяющие работу приложения в конкретной среде: адрес зависимости, режим работы, имя каталога. Переменная окружения — именованное строковое значение, которое получает процесс. Docker передаёт переменную, но программа должна уметь её читать: произвольное имя переменной не меняет настройки nginx автоматически.
| Категория | Пример | Как меняется |
|---|---|---|
| Файлы приложения в образе | Программа и index.html, добавленные COPY | Изменение исходников и новая сборка |
| Параметры запуска Docker | Порт, network, mount, restart policy | Задаются при создании контейнера; отдельные поддерживаемые настройки изменяются update |
| Конфигурация приложения | Аргумент программы, файл конфигурации, переменная окружения | Передаётся способом, поддерживаемым приложением |
| Постоянные данные | Записи БД или файлы в volume | Изменяются независимо от выпуска образа |
ENV в Dockerfile задаёт значение по умолчанию в образе. Опция -e или --env при run задаёт значение для нового контейнера, при необходимости переопределяя ENV. Это позволяет использовать один образ с разными настройками. ARG предназначен для сборки и не является автоматическим набором переменных работающего контейнера. Значение -e не редактирует образ.
Практика: один образ, две конфигурации. Подготовка: Docker Desktop работает в режиме Linux containers; используется Alpine из предыдущих примеров. Папки и файлы создавать не требуется. Два временных контейнера запускаются последовательно; printenv читает переменную из собственного окружения процесса.
PowerShell или Bash — команды одинаковы:
docker run --rm -e "LESSON_MESSAGE=Hello from Docker" alpine printenv LESSON_MESSAGE
docker run --rm -e "LESSON_MESSAGE=Another configuration" alpine printenv LESSON_MESSAGE
Ожидаемый результат: первая команда выводит Hello from Docker, вторая — Another configuration. Это два разных контейнера из одного образа; --rm удаляет каждый после завершения printenv. Образ не пересобирался, volume не создавался, а различие результата вызвано параметром запуска.
Объяснение результата. Переменная находится в окружении Linux-процесса контейнера. Это не присваивание переменной $env:LESSON_MESSAGE в Windows PowerShell. docker start запускает существующий контейнер с уже сохранённой конфигурацией, а не принимает новый -e. Изменение переменных терминала после создания контейнера не обновляет его автоматически. Для нового набора переменных обычно создаётся новый контейнер; постоянные данные при этом сохраняются отдельно.
Переменные окружения не являются защищённым хранилищем секретов: настройки контейнера могут быть видны пользователям с доступом к Engine. Способы работы с секретами при сборке рассматриваются в разделе 6.3. Передача окружения при run.
Для самопроверки: Почему две команды дали разный вывод без build? Что сохранит образ, что сохранит контейнер и нужно ли пересобирать образ ради другого LESSON_MESSAGE?
5. PID 1, сигналы, остановка и автоматический перезапуск
Инструкции Dockerfile определяют поведение работающего контейнера. В PID namespace первый процесс получает PID 1. Для ядра Linux это специальный init-процесс данного namespace. Если PID 1 завершается, существование контейнера как рабочего окружения заканчивается: Docker считает главный процесс завершённым.
5.1. Почему контейнер живёт столько, сколько живёт его главный процесс
Контейнер не является отдельной «машиной», которая обязана оставаться включённой сама по себе. Если команда контейнера завершилась, контейнер переходит в exited. Поэтому запуск приложения в background внутри shell, который затем сразу завершился, часто приводит к мгновенной остановке контейнера. Главный сервис должен работать в foreground.
На хосте: PowerShell или Bash.
docker run --rm alpine echo done
# процесс echo завершился -> контейнер сразу закончил работу
Жизненный цикл контейнера связан с его главным процессом. Завершение PID 1 приводит к завершению контейнера.
5.2. Что делает docker stop
docker stop предназначен для корректной остановки. Docker отправляет главному процессу сигнал завершения - по умолчанию SIGTERM, если image не задаёт другой STOPSIGNAL. Затем Docker ждёт grace period. Если процесс не завершился, используется принудительный SIGKILL. Для Linux-контейнеров типичное значение ожидания по умолчанию - 10 секунд, если оно не переопределено.
На хосте: PowerShell или Bash.
docker run --init -d --name lesson2-stop alpine sleep 600
docker stop --time 30 lesson2-stop
docker inspect -f '{{.State.Status}} {{.State.ExitCode}}' lesson2-stop
docker start lesson2-stop
# отдельная проверка принудительного завершения
docker kill lesson2-stop
docker rm lesson2-stop
SIGKILL нельзя обработать. Поэтому приложение не получает возможности корректно закрыть соединения, дописать буферы или завершить транзакции. Для stateful-сервисов правильная реакция на SIGTERM особенно важна.
docker stop сначала посылает главному процессу сигнал корректного завершения и ждёт заданный timeout. Если процесс не завершился, Docker принудительно останавливает его SIGKILL.
5.3. Shell-обёртка может «съесть» сигнал
Форма ENTRYPOINT влияет на доставку сигналов главному процессу. Если PID 1 - это /bin/sh -c, а приложение является его дочерним процессом, shell не обязан корректно переслать SIGTERM. В результате docker stop ждёт тайм-аут, после чего убивает процессы. Exec-форма запускает приложение напрямую и избегает этой лишней прослойки.
Если нужен стартовый shell-скрипт, последняя команда запускается через exec. Тогда shell будет заменён приложением, а не останется PID 1 над ним.
5.4. PID 1 должен убирать завершившиеся дочерние процессы
У PID 1 есть ещё одна обязанность - принимать осиротевшие дочерние процессы и выполнять wait, чтобы завершившиеся процессы не накапливались как zombies. Многие серверные приложения справляются с этим сами. Если приложение порождает дочерние процессы и плохо выполняет init-функции, Docker предлагает флаг --init. Он помещает маленький init-процесс между Docker и приложением и помогает с signal forwarding и reaping.
На хосте: PowerShell или Bash.
# готовый пример работы с минимальным init
docker run --init -d --name lesson2-init alpine sleep 600
docker exec lesson2-init ps
docker stop lesson2-init
docker rm lesson2-init
Не нужно ставить полный systemd внутрь каждого контейнера только ради этой задачи. Один контейнер обычно описывает один сервис; если ему нужен лёгкий init, используется специализированный маленький init.
Флаг --init добавляет минимальный init-процесс, который помогает корректно пересылать сигналы и убирать завершившиеся дочерние процессы.
5.5. Exit code - часть диагностики
Когда главный процесс завершился, Docker сохраняет код возврата. Ноль обычно означает штатное завершение, ненулевое значение - ошибку согласно правилам конкретной программы. Дополнительно inspect показывает признаки вроде OOMKilled. Поэтому «контейнер остановился» - это не диагноз. Нужно выяснить причину.
Для диагностики создаётся контейнер, который печатает сообщение и завершается с кодом 7:
На хосте: PowerShell или Bash.
docker run --name lesson2-failed alpine sh -c 'echo controlled error; exit 7'
На хосте: PowerShell или Bash.
docker ps -a
docker inspect -f 'Status={{.State.Status}} Exit={{.State.ExitCode}} OOM={{.State.OOMKilled}} Error={{.State.Error}}' lesson2-failed
docker logs --tail 100 lesson2-failed
docker rm lesson2-failed
Ожидаемый результат: ExitCode читается вместе с логами и OOMKilled. Код 137 часто соответствует SIGKILL, но сам по себе не доказывает нехватку памяти; причина устанавливается по совокупности данных.
Как читать параметры HEALTHCHECK
Проверка работоспособности — команда, которая оценивает конкретную функцию приложения. Её успешное завершение сообщает Docker об успехе, ошибочный результат — о неуспехе. Выбор команды определяет смысл healthy: HTTP-запрос к главной странице проверяет этот путь, а не автоматически все функции приложения, зависимости и корректность данных.
| Параметр | Значение для проверки |
|---|---|
| interval | Пауза между завершением предыдущей проверки и следующей обычной проверкой |
| timeout | Предельная длительность одной проверки |
| retries | Число последовательных неудачных проверок для перехода в unhealthy |
| start-period | Период начальной инициализации, в котором неудачи не учитываются до первого успеха |
Starting означает, что первоначальный успешный результат ещё не получен. Успех переводит состояние в healthy и сбрасывает счётчик последовательных неудач; нужное число неудач переводит его в unhealthy. В start-period проверки выполняются, а не отключаются. Если успех получен ещё в этом периоде, последующие неудачи уже учитываются. Современные версии также поддерживают start-interval — отдельную частоту проверок во время инициализации; в учебном Dockerfile она не задаётся.
Ниже упрощённая последовательность для interval=5s, retries=2, без start-period. Предполагается, что каждая команда заканчивается быстро; фактическое время может увеличиться из-за timeout, нагрузки и длительности самой проверки.
Запуск → starting
Первая проверка успешна → healthy
Следующая проверка неуспешна → healthy, одна неудача подряд
Ещё одна проверка неуспешна → unhealthy, две неудачи подряд
Следующая проверка успешна → healthy, счётчик неудач сброшен
Эта шкала объясняет эксперимент с маркером в разделе 7. Она не является таймером гарантированной готовности и не задаёт restart policy. При ошибке проверяется не только итоговый статус, но и журнал отдельных запусков Health.Log. Команда проверки должна быть доступна в образе; наличие curl.exe на Windows не означает наличие curl внутри контейнера. HEALTHCHECK.
Для самопроверки: Почему после первой неудачи при retries=2 контейнер ещё может быть healthy? Доказывает ли ответ главной страницы работоспособность всех функций приложения?
5.6. HEALTHCHECK: процесс может существовать, а сервис - уже не работать
В обычном режиме выполнения Docker показывает running, пока работает главный процесс контейнера. Но процесс может зависнуть, потерять соединение с зависимостью, перестать принимать HTTP-запросы или работать с критической ошибкой. HEALTHCHECK добавляет отдельный показатель состояния: starting, healthy или unhealthy.
FROM nginx:alpine
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD wget -qO- http://127.0.0.1/ >/dev/null || exit 1
После сборки и запуска контейнера из этого Dockerfile состояние проверяется в терминале. CONTAINER заменяется именем или идентификатором контейнера:
На хосте: PowerShell или Bash.
# шаблон: CONTAINER — контейнер с заданным healthcheck
docker inspect -f '{{.State.Status}} / {{if .State.Health}}{{.State.Health.Status}}{{else}}no-healthcheck{{end}}' CONTAINER
Важно: healthcheck сам по себе в обычном docker run не является полноценным оркестратором и не перезапускает контейнер только из-за статуса unhealthy. Он даёт платформе и администратору сигнал о состоянии. В оркестраторах health probes становятся частью автоматического управления.
Состояние running означает, что PID 1 ещё существует. Состояние healthy означает, что заданная проверка работоспособности проходит успешно. Это разные признаки.
5.7. Restart policy: что Docker должен делать после падения
На сервере часто нужно, чтобы сервис автоматически вернулся после сбоя процесса или перезапуска Docker daemon. Для standalone-контейнеров Docker поддерживает restart policy. Это лучше, чем запускать отдельный process manager вокруг каждого docker run.
| Policy | После завершения процесса | Ручная остановка и перезапуск демона |
|---|---|---|
| no | Автоматического запуска нет | Автоматического запуска нет |
| on-failure[:N] | Перезапуск при ненулевом коде, до N попыток при заданном ограничении | Перезапуск демона сам по себе не запускает контейнер; ручная остановка подавляет перезапуск |
| always | Перезапуск независимо от кода завершения | После ручного stop остаётся остановленным до start или перезапуска демона |
| unless-stopped | Перезапуск, если контейнер не оставлен остановленным | Ручная остановка сохраняется после перезапуска демона |
Bash / WSL:
docker run -d --name lesson2-redis \
--restart unless-stopped redis:alpine
# изменить policy существующего контейнера
docker update --restart unless-stopped lesson2-redis
PowerShell:
docker run -d --name lesson2-redis --restart unless-stopped redis:alpine
# изменить policy существующего контейнера
docker update --restart unless-stopped lesson2-redis
Restart policy не заменяет мониторинг и исправление причины падения. Контейнер, который бесконечно падает и поднимается, формально «автоматизирован», но сервис остаётся неисправным. Нужны logs, health, метрики и alerting.
Restart policy управляет автоматическим запуском контейнера после завершения процесса или перезапуска Docker. Она повышает доступность, но не устраняет причину аварии.
Таблица относится к standalone-контейнерам Docker Engine. Restart policy не является проверкой работоспособности. После ручного stop политика не должна немедленно запускать контейнер снова; start возобновляет его работу. Для эксперимента с аварией ниже процесс сначала работает более 10 секунд.
5.8. Running, healthy и restart - три разные вещи
Полезно разделить три механизма. Первый - состояние процесса: жив PID 1 или нет. Второй - healthcheck: выполняет ли сервис проверяемую функцию. Третий - restart policy: что делать после завершения процесса. Эти механизмы не следует смешивать.
| Механизм | Вопрос |
|---|---|
| State.Status | Жив ли главный процесс контейнера? |
| Health.Status | Проходит ли заданная проверка работоспособности? |
| RestartPolicy | Нужно ли Docker автоматически снова запустить контейнер после его завершения? |
5.9. «Один сервис на контейнер» - инженерный принцип, а не запрет на дочерние процессы
Фраза «один процесс на контейнер» слишком грубая. Apache, nginx, PostgreSQL и другие сервисы могут создавать worker-процессы. Это нормально. Смысл принципа в ответственности: контейнер не должен превращаться в мини-сервер, внутри которого одновременно управляются независимые SSH, cron, БД, backend и reverse proxy только потому, что так привыкли на виртуальной машине.
Разделение независимых компонентов облегчает обновление, масштабирование, диагностику, healthchecks и выдачу прав. Если один компонент падает, его можно заменить отдельно. Именно из этого естественно вырастает следующий инструмент курса - Docker Compose.
6. Расширение сборки: multi-stage, USER, секреты и анализ образа
Эти темы развивают базовую сборку и сохраняются в полном объёме. Примеры curl, Go и кеша используют отдельные каталоги, чтобы Dockerfile одного эксперимента не заменял файлы другого.
Пример Go готовится редактором: main.go и Dockerfile многоэтапной сборки сохраняются в папке multi. Пример curl уже подготовлен в разделе 4.4. Содержимое программ и Dockerfile не выполняется в PowerShell. Для каждого примера терминал открывается в соответствующей папке.
6.1. Multi-stage build: инструменты сборки не обязаны попадать в production image
Компилятор, Git, package manager, заголовочные файлы и тестовые зависимости нужны, чтобы создать артефакт, но часто не нужны, чтобы его запускать. Если всё делать в одном stage, production image содержит лишние инструменты, становится больше и имеет большую поверхность атаки. Multi-stage build разделяет build environment и runtime environment.
Исходный код сохраняется редактором в Windows-папке multi как main.go; для Bash WSL путь — $HOME/docker-lesson2/multi/main.go. Название файла не входит в текст программы.
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello Docker")
})
if err := http.ListenAndServe(":8080", nil); err != nil {
panic(err)
}
}
Файл Dockerfile:
FROM golang:1.26-alpine AS build
WORKDIR /src
COPY main.go .
RUN CGO_ENABLED=0 go build -o /out/app main.go
FROM alpine:latest
RUN addgroup -S app && adduser -S -G app app
COPY --from=build /out/app /usr/local/bin/app
USER app
EXPOSE 8080
ENTRYPOINT ["/usr/local/bin/app"]
На первом этапе используется набор инструментов Go, а в финальный образ копируется только готовый бинарник. Один Dockerfile описывает всю сборку. Для воспроизводимых выпусков фиксируют протестированные версии или digest базовых образов.
Multi-stage build использует несколько FROM. В ранних stages выполняется сборка, а в финальный stage копируются только нужные runtime-артефакты. Это уменьшает размер образа и количество лишних инструментов внутри него.
Dockerfile многоэтапной сборки сохраняется в каталоге multi рядом с main.go. Проверка готового приложения:
Bash / WSL:
docker build -t lesson2-go:1 .
docker run -d --name lesson2-go -p 127.0.0.1:8084:8080 lesson2-go:1
curl http://127.0.0.1:8084
docker exec lesson2-go id
docker exec lesson2-go sh -c 'command -v go || true'
docker stop lesson2-go
docker rm lesson2-go
PowerShell:
docker build -t lesson2-go:1 .
docker run -d --name lesson2-go -p 127.0.0.1:8084:8080 lesson2-go:1
curl.exe http://127.0.0.1:8084
docker exec lesson2-go id
docker exec lesson2-go sh -c 'command -v go || true'
docker stop lesson2-go
docker rm lesson2-go
Ожидаемый результат: HTTP возвращает Hello Docker; id показывает непривилегированного пользователя app; go отсутствует в финальном образе. Удаление учебного контейнера не удаляет собранный образ.
6.2. USER: root внутри контейнера всё равно остаётся привилегированным субъектом
Запуск процесса от root внутри контейнера не означает автоматически root на хосте во всех конфигурациях, но это всё равно более опасная исходная позиция. Если приложение не требует root, образ должен переключаться на непривилегированного пользователя. Это снижает последствия уязвимости и хорошо сочетается с минимальными правами на volumes и bind mounts.
FROM alpine:latest
RUN addgroup -S app && adduser -S -G app app
WORKDIR /app
COPY --chown=app:app app /app/app
USER app
ENTRYPOINT ["/app/app"]
Но USER нельзя добавлять механически. Приложению могут быть нужны права на конкретные каталоги или привилегированный порт. Задача администратора - выдать ровно необходимые права, а не сначала запускать root, а затем считать контейнер достаточной защитой.
Dockerfile с /app/app выше — шаблон: перед сборкой необходимо подготовить исполняемый файл app, совместимый с базовым образом, и права на его выполнение. Готовый пример непривилегированного приложения приведён в многоэтапной сборке.
6.3. Секреты нельзя сохранять в ARG и ENV Dockerfile
Пароль к приватному registry, токен Git или ключ доступа может понадобиться во время сборки. Плохая идея - писать его в Dockerfile, передавать через ARG и сохранять через ENV. Такие значения могут остаться в метаданных или слоях образа и попасть в registry. Современный BuildKit поддерживает secret mounts: секрет становится доступен только на нужном RUN и не должен сохраняться в образе.
# фрагмент Dockerfile; some-command заменяется нужной программой
RUN --mount=type=secret,id=repo_token \
some-command --token-file /run/secrets/repo_token
Команда сборки передаёт секрет из локального файла:
Bash / WSL: Linux-пути; вариант Windows приведён рядом.
# шаблон передачи секрета, хранящегося вне каталога сборки
docker build --secret id=repo_token,src="$HOME/.config/lesson2/repo-token" .
Секреты сборки не следует передавать через Dockerfile ARG/ENV. Для временного доступа во время build используют BuildKit secret mounts или другие специализированные механизмы секретов.
some-command — обозначение программы, которой нужен секрет, а не готовая команда. Файл секрета лучше хранить вне build context; его нельзя копировать в образ или выводить в логи. Secret mount не защищает от самой команды сборки, если та сохраняет или печатает содержимое. .dockerignore исключает случайные файлы секретов из обычного контекста.
PowerShell, шаблон передачи секрета: файл repo-token предварительно создаётся редактором в папке lesson2-secrets вне каталога сборки. В примере папка находится в профиле Windows-пользователя. Содержимое не выводится в терминал.
$SecretPath = Join-Path $env:USERPROFILE 'lesson2-secrets\repo-token'
docker build --secret "id=repo_token,src=$SecretPath" .
Перед командой терминал открывается в каталоге Dockerfile, использующего соответствующий secret mount. Программа some-command в объяснении остаётся шаблоном, а не готовым учебным приложением.
6.4. Как анализировать получившийся образ
После сборки полезно смотреть не только факт «build succeeded». Администратор должен знать размер образа, его history, пользователя по умолчанию, entrypoint, cmd и healthcheck. Это помогает находить случайно огромные слои и неверный runtime.
На хосте: PowerShell или Bash.
docker image ls
docker history --no-trunc lesson2-go:1
docker inspect lesson2-go:1
# удобные выборочные поля
docker inspect -f 'User={{.Config.User}}' lesson2-go:1
docker inspect -f 'Entrypoint={{json .Config.Entrypoint}} Cmd={{json .Config.Cmd}}' lesson2-go:1
6.5. Типичный Dockerfile: что проверять на ревью
- Базовый image выбран осознанно и имеет управляемую версию.
- Build context не содержит лишние каталоги; есть .dockerignore.
- Слои расположены так, чтобы дорогое и стабильное кешировалось.
- В финальном image нет компиляторов и build-зависимостей без необходимости.
- Главный процесс запускается корректной exec-формой.
- Приложение работает не от root, если root не требуется.
- Секреты не хранятся в Dockerfile и слоях.
- HEALTHCHECK проверяет полезную работоспособность, если это уместно.
- В image не помещают runtime-данные, которые должны изменяться независимо от версии приложения.
7. Интегрированная демонстрация: сеть + volume + собственный image + health
Следующий пример объединяет сборку nginx-образа, подключение постоянного volume, пользовательскую сеть, публикацию на localhost и проверку работоспособности.
Перед выполнением примера проверяется отсутствие конфликтов имён lesson2-service, lesson2-app-net и lesson2-runtime-data. Порт 8080 должен быть свободен. Если он занят контейнером lesson2-public из раздела 2, этот учебный контейнер сначала останавливается командой docker stop lesson2-public. При продолжении другого занятия перед сборкой открывается терминал в Windows-папке project. Существующие объекты с важными данными не удаляются ради повторения примера.
7.1. Подготовка проекта
В Проводнике открыть project/site. Создать index.html редактором. В папке project создать .dockerignore.
Файл project/site/index.html: создать или открыть в редакторе, сохранить следующее содержимое.
<h1>Docker lesson 2</h1>
Файл project/.dockerignore: создать или открыть в редакторе, сохранить следующее содержимое.
.git
*.log
.env
.env.*
backup/
*.tar.gz
7.2. Dockerfile с healthcheck
Файл project/Dockerfile: создать или открыть в редакторе, сохранить следующее содержимое.
FROM nginx:alpine
COPY site/ /usr/share/nginx/html/
HEALTHCHECK --interval=5s --timeout=3s --retries=2 \
CMD test ! -f /data/unhealthy && wget -qO- http://127.0.0.1/ >/dev/null || exit 1
Открыть PowerShell в папке project и собрать образ:
docker build -t lesson2-service:2.0 .
Файлы сайта входят в версию образа. Изменение index.html оформляется новой сборкой image, а не ручным редактированием через docker exec.
7.3. Создание сети и volume
На хосте: PowerShell или Bash.
docker network create lesson2-app-net
docker volume create lesson2-runtime-data
Постоянный volume подключается к каталогу /data для демонстрации сохранения отдельного файла. Логи nginx остаются доступными через docker logs. В официальном образе nginx стандартные файлы журналов связаны с stdout и stderr, поэтому подключение volume к каталогу логов само по себе не служит демонстрацией их накопления в файлах.
7.4. Запуск контейнера
Bash / WSL:
docker run -d --name lesson2-service \
--network lesson2-app-net \
-p 127.0.0.1:8080:80 \
--mount type=volume,src=lesson2-runtime-data,dst=/data \
--restart unless-stopped \
lesson2-service:2.0
PowerShell:
docker run -d --name lesson2-service --network lesson2-app-net -p 127.0.0.1:8080:80 --mount type=volume,src=lesson2-runtime-data,dst=/data --restart unless-stopped lesson2-service:2.0
7.5. Проверка состояния и конфигурации
Bash / WSL:
docker ps
curl http://127.0.0.1:8080
docker network inspect lesson2-app-net
docker volume inspect lesson2-runtime-data
docker inspect -f '{{.State.Status}} {{if .State.Health}}{{.State.Health.Status}}{{end}}' lesson2-service
docker logs lesson2-service
PowerShell:
docker ps
curl.exe http://127.0.0.1:8080
docker network inspect lesson2-app-net
docker volume inspect lesson2-runtime-data
docker inspect -f '{{.State.Status}} {{if .State.Health}}{{.State.Health.Status}}{{end}}' lesson2-service
docker logs lesson2-service
Каждая команда проверяет отдельный аспект: docker ps — состояние контейнера; curl — HTTP-ответ; network inspect — состав сети; volume inspect — параметры хранилища; inspect health — результат проверки работоспособности; logs — сообщения приложения. Сразу после запуска healthcheck может иметь статус starting; после успешной проверки он сменяется на healthy.
7.6. Проверка межконтейнерного DNS без публикации дополнительного порта
Bash / WSL:
docker run --rm --network lesson2-app-net busybox \
wget -qO- http://lesson2-service | head
PowerShell:
docker run --rm --network lesson2-app-net busybox wget -qO- http://lesson2-service | Select-Object -First 10
Этот запрос идёт по внутренней Docker-сети и использует имя контейнера. Порт 8080 хоста здесь вообще не участвует.
Полезно зафиксировать исходные показатели перед экспериментами. Счётчик перезапусков читается через docker inspect -f '{{.RestartCount}}' lesson2-service.
Эксперимент: running, unhealthy и restart policy
После первого успешного healthcheck создаётся специально предусмотренный маркер неисправности. Он имитирует дополнительное условие проверки; это не обязательная часть production-healthcheck. Nginx продолжает обслуживать HTTP.
Bash / WSL:
docker inspect -f '{{.State.Status}} {{.State.Health.Status}} restarts={{.RestartCount}}' lesson2-service
docker exec lesson2-service touch /data/unhealthy
# ожидание нескольких циклов проверки
sleep 15
docker inspect -f '{{.State.Status}} {{.State.Health.Status}} restarts={{.RestartCount}}' lesson2-service
docker inspect -f '{{json .State.Health.Log}}' lesson2-service
curl http://127.0.0.1:8080
docker exec lesson2-service rm /data/unhealthy
sleep 10
docker inspect -f '{{.State.Status}} {{.State.Health.Status}} restarts={{.RestartCount}}' lesson2-service
PowerShell:
docker inspect -f '{{.State.Status}} {{.State.Health.Status}} restarts={{.RestartCount}}' lesson2-service
docker exec lesson2-service touch /data/unhealthy
# ожидание нескольких циклов проверки
Start-Sleep -Seconds 15
docker inspect -f '{{.State.Status}} {{.State.Health.Status}} restarts={{.RestartCount}}' lesson2-service
docker inspect -f '{{json .State.Health.Log}}' lesson2-service
curl.exe http://127.0.0.1:8080
docker exec lesson2-service rm /data/unhealthy
Start-Sleep -Seconds 10
docker inspect -f '{{.State.Status}} {{.State.Health.Status}} restarts={{.RestartCount}}' lesson2-service
Ожидаемый результат: При наличии маркера контейнер остаётся running, проверка становится unhealthy, HTTP может отвечать, а RestartCount не увеличивается. После удаления маркера успешная проверка возвращает healthy. При высокой нагрузке переход может занять дольше: состояние повторно читается через inspect.
Эксперимент: завершение процесса и ручная остановка
Используется тот же контейнер с unless-stopped. Nginx завершается своей командой nginx -s stop изнутри контейнера. Это контролируемая имитация завершения приложения без ручного docker stop; её результат отделяется от статуса unhealthy. Команда exec может завершиться ошибкой из-за исчезновения контейнерного процесса.
Bash / WSL:
sleep 11
docker inspect -f 'restarts={{.RestartCount}}' lesson2-service
docker exec lesson2-service nginx -s stop || true
sleep 10
docker inspect -f '{{.State.Status}} restarts={{.RestartCount}}' lesson2-service
docker stop lesson2-service
sleep 3
docker inspect -f '{{.State.Status}}' lesson2-service
docker start lesson2-service
PowerShell:
Start-Sleep -Seconds 11
docker inspect -f 'restarts={{.RestartCount}}' lesson2-service
docker exec lesson2-service nginx -s stop # возможен ожидаемый ненулевой код возврата
Start-Sleep -Seconds 10
docker inspect -f '{{.State.Status}} restarts={{.RestartCount}}' lesson2-service
docker stop lesson2-service
Start-Sleep -Seconds 3
docker inspect -f '{{.State.Status}}' lesson2-service
docker start lesson2-service
Ожидаемый результат: После завершения nginx Docker повторно запускает контейнер, RestartCount увеличивается. После ручного stop состояние остаётся exited; start запускает контейнер по явной команде. Перезапуск всего Docker daemon для этого эксперимента не требуется.
Для самопроверки: В каком эксперименте менялся только Health.Status, в каком увеличивался RestartCount, а в каком контейнер оставался exited? Какие наблюдения различают эти случаи?
7.7. Проверка постоянства volume
После docker start из предыдущего эксперимента дождаться running и успешного healthcheck через docker inspect. Затем выполняется запись в том. Команды этого раздела удаляют lesson2-service; дальнейший доступ к этому контейнеру уже невозможен до его повторного создания.
Bash / WSL:
docker exec lesson2-service sh -c 'echo lesson2 >> /data/lab-note.txt'
docker stop lesson2-service
docker rm lesson2-service
docker run --rm \
--mount type=volume,src=lesson2-runtime-data,dst=/data,ro \
alpine cat /data/lab-note.txt
PowerShell:
docker exec lesson2-service sh -c 'echo lesson2 >> /data/lab-note.txt'
docker stop lesson2-service
docker rm lesson2-service
docker run --rm --mount type=volume,src=lesson2-runtime-data,dst=/data,ro alpine cat /data/lab-note.txt
После удаления контейнера файл остаётся в volume. Перед завершением занятия можно оставить сеть и том для продолжения. При окончательной очистке следующие команды удаляют только сеть и хранилище этого примера; удаление volume уничтожает записанный в нём учебный файл.
На хосте: PowerShell или Bash.
docker network rm lesson2-app-net
docker volume rm lesson2-runtime-data
# образ можно оставить для следующего занятия или удалить:
# docker image rm lesson2-service:2.0
Ожидаемый результат: После удаления контейнера временный Alpine выводит lesson2 из lab-note.txt. После явного удаления тома этот файл уничтожается. Исходники остаются в каталоге project.
8. Типичные ошибки после второй лекции
«Для связи backend с БД обязательно нужен -p»
Нет. Если контейнеры находятся в одной подходящей Docker-сети, они могут общаться напрямую. Публикация нужна для доступа через адрес хоста/из других сетей.
«Можно прописать IP контейнера в конфиг и забыть»
IP динамический. Используйте DNS-имя контейнера/сервиса или сетевой alias.
«Volume - это просто папка, поэтому буду вручную править /var/lib/docker»
Docker управляет storage location. Работайте через Docker и mounts; прямое вмешательство в внутренние каталоги daemon не является нормальным интерфейсом.
«Bind mount безопасен, ведь контейнер изолирован»
Bind mount намеренно пробивает файловую границу. Если он writable, процесс контейнера может менять выданные файлы хоста.
«COPY . . - нормально для любого проекта»
Так легко передать огромный контекст, сбить кеш и случайно включить секреты. Нужен .dockerignore и осмысленный порядок COPY.
«RUN запускает сервер при старте»
RUN работает при build. Runtime определяют CMD/ENTRYPOINT и аргументы docker run.
«Shell-форма короче, значит всегда лучше»
Для PID 1 она может создать shell-обёртку и ухудшить обработку сигналов.
«Если контейнер running, приложение исправно»
Running показывает существование процесса. Для функциональной проверки нужен healthcheck/мониторинг.
«Restart policy решает любые падения»
Она только повторяет запуск. Причина ошибки и бесконечный crash loop никуда не исчезают.
9. Вопросы для самопроверки
- Почему два контейнера в одной пользовательской bridge-сети могут общаться без публикации портов на хосте?
- Почему конфигурацию приложения нежелательно привязывать к IP контейнера?
- Чем -p 127.0.0.1:8080:80 безопаснее -p 8080:80 для локального административного интерфейса?
- Какая разница между named volume и bind mount с точки зрения зависимости от хоста?
- Что произойдёт с файлами writable layer после удаления контейнера?
- Почему обычный tar каталога работающей базы данных не всегда является корректным backup?
- Что такое build context и зачем нужен .dockerignore?
- Почему COPY requirements.txt перед COPY . может ускорить повторные сборки?
- В чём разница между RUN, CMD и ENTRYPOINT?
- Почему exec-форма ENTRYPOINT предпочтительна для долгоживущего процесса?
- Что даёт multi-stage build?
- Почему пароль нельзя безопасно «спрятать» в Dockerfile через ARG?
- Что означает PID 1 внутри контейнера?
- Чем docker stop отличается от docker kill?
- Почему status=running не гарантирует, что веб-приложение отвечает?
- Чем restart policy отличается от healthcheck?
Ответы для проверки
- Контейнеры обмениваются трафиком внутри общей сети; публикация на хост нужна для другого пути доступа.
- Адрес может измениться при пересоздании; имя или сетевой alias описывают зависимость устойчивее.
- Loopback ограничивает обычный путь доступа Docker-хостом; без адреса порт публикуется на всех адресах по умолчанию.
- Volume управляется Docker по имени, bind mount зависит от конкретного пути Docker-хоста.
- Записываемый слой удаляется вместе с контейнером.
- Файлы работающей БД могут отражать несогласованное состояние; нужен способ backup, поддерживаемый СУБД.
- Контекст — доступные builder файлы; .dockerignore исключает ненужные или чувствительные файлы.
- Изменение кода не меняет ранний COPY зависимостей, поэтому шаг установки может сохранить кеш.
- RUN действует при сборке; CMD задаёт команду или аргументы по умолчанию; ENTRYPOINT задаёт точку входа.
- При прямом запуске меньше промежуточных оболочек и проще доставка сигналов; приложение всё равно должно их обрабатывать.
- Инструменты и зависимости сборки остаются в раннем этапе, финальный образ получает нужные артефакты.
- ARG и ENV могут оставить значения в метаданных, истории или слоях; для временного доступа применяют secret mounts.
- PID 1 — главный процесс данного PID namespace с особенностями сигналов и обработки дочерних процессов.
- stop сначала даёт время на корректное завершение; kill по умолчанию отправляет SIGKILL без такой возможности.
- Живой процесс может не выполнять полезную функцию; требуется функциональная проверка.
- Healthcheck оценивает функцию сервиса; restart policy реагирует на завершение процесса и условия запуска.
10. Краткий конспект
Пользовательская bridge-сеть объединяет контейнеры на одном Docker-хосте и предоставляет DNS-разрешение имён. Для межконтейнерного обмена в одной сети публикация порта на хост обычно не нужна.
Параметр -p публикует порт контейнера через адрес и порт Docker-хоста. Если host IP не указан, порт обычно публикуется на всех интерфейсах хоста.
Постоянные данные следует отделять от writable layer контейнера. Named volume управляется Docker; bind mount связывает контейнер с конкретным путём хоста; tmpfs предназначен для временных данных.
Build context определяет набор файлов, доступных builder. .dockerignore исключает ненужные файлы из контекста.
Порядок Dockerfile влияет на build cache. Редко меняющиеся дорогие шаги выгодно располагать раньше, часто меняющиеся исходники - позже.
RUN выполняется при сборке. CMD и ENTRYPOINT определяют runtime. Для основного процесса предпочтительна exec-форма.
Multi-stage build отделяет инструменты сборки от финального runtime image.
Секреты нельзя хранить в Dockerfile через ARG/ENV; для build используют специальные secret mounts.
Контейнер живёт, пока работает его главный процесс PID 1. docker stop сначала пытается завершить его сигналом и только после timeout использует принудительное завершение.
Healthcheck, состояние running и restart policy решают разные задачи и должны рассматриваться отдельно.
11. Домашнее задание / самостоятельная практика
- Создать пользовательскую сеть lesson2-home-net и запустить в ней два контейнера. Проверить обращение одного контейнера к другому по имени.
- Запустить nginx так, чтобы его порт 80 был доступен только на 127.0.0.1:8085 хоста. Объяснить, почему это отличается от -p 8085:80.
- Создать named volume, записать в него файл из одного контейнера, удалить контейнер и прочитать файл из другого.
- Создать bind mount каталога с index.html в nginx и подключить его только для чтения.
- Написать Dockerfile для простого статического сайта. Добавить .dockerignore и HEALTHCHECK.
- Показать docker history и docker inspect собранного image. Найти Config.User, Cmd/Entrypoint и Healthcheck.
- Запустить контейнер с --restart unless-stopped и объяснить, в каких случаях он будет автоматически запущен снова.
- Создать файловый архив учебного тома и восстановить его в новый том. Сравнить исходный и восстановленный файл, как в разделах 3.8–3.9. Для этого эксперимента не используется каталог работающей базы данных.
- Повторить эксперимент с кешем из раздела 4.2: две одинаковые сборки, изменение исходника и изменение файла зависимостей. Сохранить вывод и объяснить, какие шаги использовали CACHED.
- Повторить эксперимент с маркером unhealthy из раздела 7. Зафиксировать healthy → unhealthy → healthy и неизменный RestartCount. Затем отдельно завершить nginx его командой и проверить увеличение RestartCount; ручной docker stop сравнить отдельно.
- Создать одноразовый учебный контейнер с контролируемым кодом завершения 7, как в разделе 5.5. Проверить ExitCode, OOMKilled и логи. Здесь OOMKilled ожидается false; намеренно исчерпывать память не требуется.
- Подготовить короткий письменный ответ: почему «контейнер работает» и «сервис исправен» - не одно и то же.
Критерии выполнения самостоятельной практики
| Результат | Подтверждение |
|---|---|
| Связность и DNS | Имя разрешается и запрос из общей сети получает страницу без дополнительного -p |
| Постоянные данные | Файл читается новым контейнером и совпадает после восстановления в новый том |
| Bind mount | Изменение на хосте видно без сборки; подключение отмечено как read-only в inspect |
| Кеш сборки | Сохранён вывод CACHED и объяснено, почему разные изменения затронули разные шаги |
| Работоспособность | Зафиксированы healthy → unhealthy → healthy и отдельно счётчик перезапусков после контролируемого завершения nginx |
| Диагностика | Приведены код завершения, OOMKilled и соответствующие логи с объяснением |
В отчёте важны команды, ключевые результаты и объяснение причин. Идентификаторы, IP-адреса и продолжительность выполнения могут отличаться. Дополнительное задание: описать действия для отката образа и восстановления данных — это разные операции.
12. Дальнейшие темы
Следующая тема — многоконтейнерные приложения и Docker Compose: compose.yaml, services, networks, volumes, environment, depends_on, зависимости по состоянию здоровья, build, restart, profiles и жизненный цикл проекта. Дальнейшее изучение включает реестры, версии образов, усиление безопасности, ограничения ресурсов и диагностику Docker-хоста.
Очистка остальных учебных объектов
Выполняется после окончания всех занятий по этому материалу. Предварительно проверяется, что перечисленные имена принадлежат примерам лекции. Отсутствующий объект можно пропустить. Общие prune-команды не нужны.
На хосте: PowerShell или Bash.
docker stop lesson2-web lesson2-public lesson2-local lesson2-bind lesson2-db lesson2-app lesson2-redis
docker rm lesson2-web lesson2-public lesson2-local lesson2-bind lesson2-db lesson2-app lesson2-redis
docker network rm lesson2-net lesson2-front lesson2-back
docker volume rm lesson2-data lesson2-restored
docker image rm lesson2-cache:1 lesson2-cache:2 lesson2-cache:3 lesson2-curl:1 lesson2-go:1 lesson2-service:2.0
Команды удаления томов уничтожают их учебные файлы. Архив backup/lesson2-data.tar.gz и исходники остаются на хосте. Контейнер, сеть и том итогового проекта очищаются в разделе 7; объекты самостоятельно выполненного домашнего задания удаляются по выбранным для них именам.
13. Источники и дополнительная литература
- Docker Docs. Networking overview: https://docs.docker.com/engine/network/
- Docker Docs. Bridge network driver: https://docs.docker.com/engine/network/drivers/bridge/
- Docker Docs. Port publishing and mapping: https://docs.docker.com/engine/network/port-publishing/
- Docker Docs. Storage: https://docs.docker.com/engine/storage/
- Docker Docs. Volumes: https://docs.docker.com/engine/storage/volumes/
- Docker Docs. Bind mounts: https://docs.docker.com/engine/storage/bind-mounts/
- Docker Docs. Dockerfile reference: https://docs.docker.com/reference/dockerfile/
- Docker Docs. Build context and .dockerignore: https://docs.docker.com/build/concepts/context/
- Docker Docs. Building best practices: https://docs.docker.com/build/building/best-practices/
- Docker Docs. Optimize cache usage in builds: https://docs.docker.com/build/cache/optimize/
- Docker Docs. Multi-stage builds: https://docs.docker.com/build/building/multi-stage/
- Docker Docs. Build secrets: https://docs.docker.com/build/building/secrets/
- Docker Docs. Run multiple processes in a container: https://docs.docker.com/engine/containers/multi-service_container/
- Docker Docs. docker container stop: https://docs.docker.com/reference/cli/docker/container/stop/
- Docker Docs. Start containers automatically / restart policies: https://docs.docker.com/engine/containers/start-containers-automatically/
- Linux man-pages. pid_namespaces(7): https://man7.org/linux/man-pages/man7/pid_namespaces.7.html
- Nigel Poulton. Docker Deep Dive, 4th Edition. Packt, 2025.
- Sean P. Kane, Karl Matthias. Docker: Up & Running, 3rd Edition. O’Reilly, 2023.
- Docker Docs. tmpfs mounts: https://docs.docker.com/engine/storage/tmpfs/
- Docker Docs. docker volume prune: https://docs.docker.com/reference/cli/docker/volume/prune/
- NGINX Docker image. Dockerfile: Dockerfile официального образа
Практические примеры рассчитаны на Linux-контейнеры в Docker Desktop для Windows; особенности PowerShell, WSL и путей поясняются рядом с командами. Windows-контейнеры рассматриваются отдельно. Перед применением команд используются сведения для установленной версии Docker из официальной документации.