Блок 17

Виртуализация и контейнеры

Linux: блок 17

> Что вас ждёт в этом блоке: мы познакомимся с одной из важнейших технологий современной IT-индустрии — контейнеризацией, и в первую очередь с Docker, инструментом, ставшим фактическим стандартом у…

Обновлено: · DeeNet

Введение в блок

> Что вас ждёт в этом блоке: мы познакомимся с одной из важнейших технологий современной IT-индустрии — контейнеризацией, и в первую очередь с Docker, инструментом, ставшим фактическим стандартом упаковки и развёртывания приложений. Вы поймёте разницу между виртуальными машинами и контейнерами, научитесь запускать, останавливать и управлять контейнерами, создавать собственные образы через Dockerfile, оркестрировать многоконтейнерные приложения через Docker Compose, и разберётесь с постоянным хранением данных и сетевым взаимодействием контейнеров.

>

> Что нужно знать перед началом: весь пройденный материал курса особенно важен здесь — процессы и изоляция (Блок 11), файловая система и права доступа (Блоки 5, 8), сеть и порты (Блок 12), пакетный менеджер (Блок 9), а также bash-скрипты (Блок 15), которые часто используются вместе с Docker для автоматизации.

Урок 17.1. Виртуальные машины vs контейнеры

Понять фундаментальную разницу между виртуализацией на уровне оборудования (виртуальные машины) и виртуализацией на уровне операционной системы (контейнеры), разобраться в технических механизмах, которые делают контейнеры возможными в Linux, и осознать, для каких задач лучше подходит каждый из этих подходов.

Теория

Проблема, которую решает и виртуализация, и контейнеризация — изоляция и переносимость. Представьте типичную ситуацию: вы разработали приложение на своём компьютере, оно прекрасно работает, но при попытке запустить его на сервере коллеги или в продакшене оно неожиданно ломается — потому что там установлена другая версия языка программирования, отсутствуют нужные библиотеки, или конфликтуют версии зависимостей с другими приложениями, уже работающими на этой машине. Эта проблема настолько распространена, что получила ироничное неформальное название «у меня на компьютере работает» (“works on my machine”). И виртуальные машины, и контейнеры — это способы решить данную проблему, хотя и совершенно разными техническими средствами.

Виртуальные машины (Virtual Machines, VM) — виртуализация на уровне оборудования.

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

Примеры гипервизоров: VirtualBox, VMware, KVM (Kernel-based Virtual Machine — встроенный в ядро Linux гипервизор), Hyper-V (от Microsoft).

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

Контейнеры — виртуализация на уровне операционной системы.

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

Ключевая характеристика: контейнер — это, по сути, специальным образом изолированный процесс (вспомните понятие процесса из Блока 11), которому кажется, что он работает в собственной, отдельной от всего остального операционной системе — со своей файловой системой, своим сетевым стеком, своим списком процессов — но на самом деле все эти иллюзии создаются механизмами изоляции внутри одного и того же ядра Linux, разделяемого со всеми остальными контейнерами и самим хостом.

Технические механизмы ядра Linux, делающие контейнеры возможными (на концептуальном уровне, без глубокого погружения в детали реализации ядра — это отдельная, весьма продвинутая тема):

Эти два механизма (namespaces для изоляции «видимости», cgroups для ограничения ресурсов) вместе создают у процессов внутри контейнера убедительную иллюзию работы в отдельной, независимой системе, при том что физически это по-прежнему обычные процессы того же самого ядра хост-системы, просто особым образом изолированные и ограниченные.

Сравнительная таблица — виртуальные машины vs контейнеры:

| Характеристика | Виртуальные машины | Контейнеры |

|---|---|---|

| Изоляция | Полная (отдельное ядро) | На уровне процессов (общее ядро) |

| Размер образа | Обычно гигабайты (полная ОС) | Обычно мегабайты (только приложение и зависимости) |

| Время запуска | Секунды-минуты (загрузка ядра и ОС) | Доли секунды (запуск процесса) |

| Накладные расходы ресурсов | Существенные (каждая ВМ — полная ОС) | Минимальные (совместное использование ядра) |

| Разные ОС на одном хосте | Да (можно запустить Windows-ВМ на Linux-хосте) | Нет (контейнер должен быть совместим с ядром хоста — на практике это означает, что на Linux-хосте можно запускать только Linux-контейнеры) |

| Уровень изоляции безопасности | Более сильный (отдельное ядро — сложнее «вырваться» из ВМ) | Более слабый по своей природе (общее ядро — потенциально больше точек для атаки, хотя современные механизмы делают контейнеры весьма безопасными при правильной настройке) |

| Типичное применение | Запуск полностью изолированных систем, разных ОС, задачи с высокими требованиями к изоляции безопасности | Упаковка и развёртывание приложений, микросервисная архитектура, CI/CD (непрерывная интеграция/доставка), локальная разработка |

Важное уточнение про «разные ОС на одном хосте». Когда говорят, что «в контейнере Linux на хосте Windows» или наоборот — технически на Windows или macOS для запуска Linux-контейнеров (что является наиболее распространённым случаем использования Docker, поскольку подавляющее большинство образов контейнеров построены на основе Linux) на самом деле незаметно для пользователя используется лёгкая виртуальная машина с Linux-ядром внутри, а уже контейнеры запускаются поверх неё — то есть фундаментальное требование «контейнеры используют ядро хоста» никуда не девается, просто на не-Linux системах это скрыто дополнительным слоем виртуализации.

Docker — самая популярная платформа контейнеризации. Хотя сама технология контейнеров в Linux существовала и до Docker (в различных менее удобных формах, таких как LXC — Linux Containers), именно Docker, представленный в 2013 году, сделал контейнеризацию по-настоящему массовой и удобной технологией благодаря простому интерфейсу командной строки, стандартизированному формату образов, и созданию Docker Hub — общедоступного репозитория готовых образов контейнеров. Docker — это то, чем мы будем заниматься в оставшейся части этого блока.

Когда использовать виртуальные машины, а когда контейнеры — практические соображения:

Практика

В этом вводном уроке мы исследуем механизмы, лежащие в основе контейнеризации, ещё до установки самого Docker (которую сделаем в уроке 17.3) — чтобы прочувствовать, что контейнеры не «магия», а использование конкретных, изучаемых механизмов ядра Linux, с некоторыми из которых мы уже косвенно сталкивались в предыдущих блоках курса.

Шаг 1. Изучите текущие пространства имён (namespaces) вашей системы — команда lsns показывает все активные namespaces в системе:

```bash

lsns

```

Что вы увидите: список активных пространств имён разных типов (net, mnt, pid, uts, ipc, user, cgroup) с указанием, какие процессы к ним относятся. Даже без единого запущенного контейнера, ваша система уже активно использует эти механизмы для базовой организации процессов.

[Скриншот терминала]

Шаг 2. Посмотрите пространства имён конкретно вашего текущего терминального процесса (используя $$ — переменную PID текущего процесса, которую мы изучали в уроке 15.3):

```bash

ls -la /proc/$$/ns/

```

Что вы увидите: список символических ссылок (симлинков, изученных в Блоке 5) на разные пространства имён, к которым принадлежит ваша текущая оболочка — это подтверждает, что каждый процесс в Linux всегда принадлежит к какому-то набору namespaces, даже если это просто «обычные», не связанные с контейнерами процессы.

[Скриншот терминала]

Шаг 3. Изучите механизм control groups (cgroups) — просмотрите файловую систему cgroups (мы кратко упоминали /proc и подобные виртуальные файловые системы в Блоке 9):

```bash

mount | grep cgroup

```

Что вы увидите: строки, показывающие, что cgroups смонтированы как специальная файловая система — этот же механизм используется системой systemd (изученной в Блоке 10) для управления ресурсами служб, и точно так же будет использоваться Docker для ограничения ресурсов контейнеров.

Шаг 4. Изучите, как выглядит ограничение ресурсов через cgroups для реально существующей systemd-службы — например, для SSH-службы (можно использовать и любую другую активную службу):

```bash

systemctl show ssh --property=CPUAccounting,MemoryAccounting 2>/dev/null || systemctl show ssh.service --property=CPUAccounting,MemoryAccounting

```

Что вы увидите: параметры учёта ресурсов для процесса SSH-службы — то же базовое устройство, что вскоре будет использовать и Docker, но в применении к обычной системной службе, а не к контейнеру.

Шаг 5. Сравните с виртуальными машинами — проверьте, установлен ли в вашей системе KVM (гипервизор ядра Linux, который мы упоминали в теории) — это чисто информационная проверка, устанавливать и настраивать полноценную виртуальную машину в рамках этого курса мы не будем, поскольку фокус блока — контейнеры:

```bash

lsmod | grep kvm

```

(lsmod — команда для просмотра загруженных модулей ядра, мы упоминали модули ядра вскользь в контексте драйверов ранее в курсе.)

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

Шаг 6. Изучите разницу в потреблении ресурсов концептуально — сравните размер типичного образа виртуальной машины и типичного образа контейнера через поиск информации (без необходимости скачивать что-либо):

```bash

echo "Для сравнения: типичный установочный ISO-образ Ubuntu Server занимает около 1.5-2 ГБ."

echo "Типичный минимальный Docker-образ (например, alpine) занимает менее 10 МБ."

```

Это наглядно иллюстрирует разницу в подходах — контейнер не несёт с собой полноценную операционную систему, только необходимый минимум для работы конкретного приложения.

Разбор команд

Команда lsns

Команда lsmod

Директория /proc/PID/ns/

Возможные ошибки

Почему возникает: утилита lsns входит в пакет util-linux, который обычно предустановлен в Ubuntu, но в редких минимальных установках может отсутствовать.

Как исправить: sudo apt install util-linux (хотя это, как правило, уже базовая часть системы и переустановка требуется крайне редко).

Практические задания

  1. Изучите вывод lsns подробнее — найдите в выводе строки с типом net (сетевые namespaces) и pid (namespaces процессов), и сопоставьте количество различных PID namespaces с вашим текущим пониманием того, сколько изолированных «групп процессов» существует в системе прямо сейчас (без единого запущенного контейнера, скорее всего, будет всего один или очень небольшое количество).
  2. Найдите в интернете (без необходимости выполнения) сравнение времени запуска типичной виртуальной машины (обычно секунды-десятки секунд, включая полную загрузку ядра и инициализацию системы) и типичного Docker-контейнера (обычно доли секунды) — запишите в конспект найденные примерные цифры и объясните своими словами, почему существует такая разница, опираясь на материал теории этого урока.
  3. Опишите в конспекте сценарий использования (реальный или гипотетический), где было бы разумно использовать именно виртуальную машину, а не контейнер, и объясните свой выбор, опираясь на сравнительную таблицу из теории.

Проверка знаний

Вопрос 1. В чём принципиальное отличие виртуальной машины от контейнера с точки зрения того, что каждая из этих технологий виртуализирует?

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

Вопрос 2. Почему невозможно запустить, например, полноценный Windows-контейнер на хосте с Linux-ядром, тогда как запуск Windows-виртуальной машины на Linux-хосте вполне возможен?

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

Итоги урока

Вы изучили: проблему переносимости приложений, которую решают и виртуальные машины, и контейнеры, принцип работы виртуальных машин через гипервизоры, принцип работы контейнеров через namespaces и cgroups ядра Linux, сравнительные характеристики обоих подходов, роль Docker в популяризации контейнеризации.

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

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

Урок 17.2. Введение в Docker

Понять архитектуру Docker (клиент-сервер), освоить ключевую терминологию (образ, контейнер, Dockerfile, реестр), разобраться в понятии слоёв образа, и осознать роль Docker Hub как публичного реестра образов.

Теория

Что такое Docker. Docker — это платформа для разработки, доставки и запуска приложений в контейнерах. Как мы выяснили в предыдущем уроке, сама технология контейнеризации основана на механизмах ядра Linux (namespaces, cgroups), но именно Docker предоставил удобный, стандартизированный инструментарий для работы с этими механизмами — простой интерфейс командной строки, понятный формат описания образов, и экосистему для обмена готовыми образами.

Архитектура Docker: клиент-сервер. Docker устроен по модели клиент-сервер, аналогично многим системам, которые мы изучали ранее в курсе (например, веб-серверы в Блоке 12, или сам SSH в Блоке 13):

Ключевая терминология Docker:

Docker Hub — крупнейший публичный реестр Docker-образов. По аналогии с тем, что GitHub является наиболее популярной платформой для хостинга Git-репозиториев (Блок 16), Docker Hub (hub.docker.com) — крупнейший публичный реестр готовых Docker-образов. Здесь можно найти официальные образы практически любого популярного программного обеспечения — веб-серверов (Nginx, которым мы займёмся в Блоке 19), баз данных (PostgreSQL, MySQL), языков программирования (Python, Node.js), и бесчисленное множество образов, созданных сообществом для самых разных целей.

Официальные образы (official images) vs образы сообщества. На Docker Hub существует важное различие: официальные образы — специально отмеченные, курируемые и поддерживаемые самим Docker в сотрудничестве с разработчиками соответствующего программного обеспечения (например, официальный образ ubuntu, nginx, postgres) — они проходят проверку безопасности и считаются надёжным, рекомендуемым выбором для начала работы. Образы сообщества (создаваемые отдельными пользователями или организациями) могут быть точно так же полезны и качественны, но не имеют такого же уровня формальной проверки — при их использовании стоит проявлять больше осторожности, особенно проверяя источник и репутацию автора образа.

Теги образов (image tags). Каждый образ обычно имеет несколько тегов — вариантов с разными версиями или конфигурациями. Полное имя образа в Docker обычно записывается в формате:

```

[реестр/]имя_образа[:тег]

```

Например:

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

Как Docker изолирует контейнеры друг от друга и от хоста — краткое резюме материала предыдущего урока в контексте Docker конкретно. Каждый Docker-контейнер использует собственные namespaces (изолированная файловая система, собственный список процессов, собственный сетевой стек с отдельным набором портов) и ограничивается через cgroups (можно задать лимиты на использование CPU и памяти, что мы разберём в практике следующих уроков) — при этом все контейнеры на одной машине совместно используют одно и то же ядро хост-системы.

Практика

В этом уроке мы пока не устанавливаем Docker (это будет в уроке 17.3), а изучаем концепции через изучение публичной информации на Docker Hub и знакомство с общей структурой того, что нам предстоит установить.

Шаг 1. Изучите официальную документацию и сайт Docker Hub через браузер (без необходимости регистрации на этом этапе) — откройте hub.docker.com и найдите официальный образ ubuntu. Обратите внимание на:

Шаг 2. Найдите на Docker Hub официальный образ nginx (веб-сервер, с которым мы подробно познакомимся в Блоке 19) и изучите список доступных тегов — обратите внимание на существование вариантов с суффиксом -alpine (более компактные образы на основе Alpine Linux) в сравнении с обычными вариантами.

Шаг 3. Найдите официальный образ postgres (база данных, тоже изучаемая в Блоке 19) и посмотрите на структуру описания образа на странице — обычно там присутствует раздел «How to use this image» с примерами команд запуска, что является типичным и очень полезным паттерном оформления страниц образов на Docker Hub.

Шаг 4. Изучите концепцию слоёв образа на конкретном примере — на странице любого официального образа на Docker Hub обычно можно найти ссылку на исходный Dockerfile этого образа (часто ведущую на GitHub-репозиторий, что является отличной практической иллюстрацией связи материала Блока 16 с материалом этого блока). Просмотрите Dockerfile образа nginx (если найдёте ссылку) и обратите внимание на количество отдельных инструкций — каждая из них потенциально создаёт отдельный слой.

Шаг 5. Составьте в конспекте таблицу-сравнение (на основе изученного материала теории), сопоставляющую термины Docker с уже знакомыми нам концепциями из предыдущих блоков курса:

| Термин Docker | Аналогия из уже изученного материала |

|---|---|

| Образ (image) | ISO-образ для установки ОС (статичный шаблон) |

| Контейнер (container) | Запущенный процесс на основе программы (динамический экземпляр) |

| Dockerfile | Bash-скрипт, описывающий последовательность настройки (Блок 15) |

| Реестр (registry) | GitHub, но для образов, а не для кода (Блок 16) |

| Слои образа | История коммитов Git — последовательность наслаивающихся изменений (Блок 16) |

Дополните эту таблицу своими собственными аналогиями, если они помогают лучше осмыслить материал.

Разбор команд

В этом уроке мы пока не выполняли команд Docker (это начнётся в уроке 17.4, после установки в уроке 17.3) — урок был полностью посвящён концептуальному знакомству с архитектурой и терминологией.

Возможные ошибки

Специфичных практических ошибок в этом уроке не возникает, поскольку мы не выполняли команд, работающих непосредственно с Docker. Однако полезно заранее отметить концептуальную ловушку, с которой часто сталкиваются начинающие:

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

Как избежать: держать в голове чёткую аналогию «чертёж vs построенный по чертежу объект» (или «класс vs объект», если вам знаком этот термин из программирования) — образ статичен и неизменяем, контейнер — это работающий (или созданный, но не обязательно запущенный) экземпляр образа, из одного образа можно создать множество независимых контейнеров.

Практические задания

  1. Найдите на Docker Hub официальный образ python и изучите доступные теги — обратите внимание на разные варианты: полные образы (обычно на основе Debian), варианты -slim (уменьшенные), и -alpine (максимально компактные) — опишите в конспекте, в чём, по вашему мнению, может быть практический компромисс между использованием полного и минимального варианта образа.
  2. Изучите страницу «Docker Official Images» на Docker Hub (обычно доступна через специальный раздел сайта) и составьте в конспекте список из пяти официальных образов, которые кажутся вам наиболее полезными или интересными, с кратким описанием, для чего каждый из них предназначен.
  3. Найдите информацию (через поиск в интернете) о том, что такое «Docker Desktop», и чем он отличается от Docker Engine (движка, который мы установим и будем использовать непосредственно в терминале Ubuntu в следующем уроке) — опишите разницу в конспекте.

Проверка знаний

Вопрос 1. В чём разница между Docker-образом и Docker-контейнером, используя аналогию с чертежом и построенным по нему объектом?

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

Вопрос 2. Почему в серьёзных проектах рекомендуется указывать конкретную версию (тег) образа вместо использования latest?

Ответ: Тег latest — это не гарантированно самая современная или стабильная версия, а лишь ещё один обычный тег, который автор образа обновляет по своему усмотрению; используя его, вы рискуете, что при пересборке или повторном скачивании образа его содержимое неожиданно изменится (автор выпустил новую версию с другим поведением), что может непредсказуемо сломать работу вашего приложения. Указание конкретной версии (например, nginx:1.25) гарантирует воспроизводимость — используемый образ остаётся одним и тем же независимо от того, когда и сколько раз вы его скачиваете или пересобираете.

Итоги урока

Вы изучили: архитектуру Docker (клиент-сервер, dockerd и docker CLI), ключевую терминологию (образ, контейнер, Dockerfile, реестр, слои), роль Docker Hub, различие официальных образов и образов сообщества, значение тегов и практику указания конкретных версий.

Вы умеете: находить и изучать образы на Docker Hub, объяснять разницу между образом и контейнером, ориентироваться в базовой терминологии Docker.

В следующем уроке потребуется: концептуальное понимание, полученное в этом уроке — теперь мы установим Docker в нашей системе Ubuntu и настроим его для практической работы.

Урок 17.3. Установка Docker на Ubuntu

Установить Docker Engine в системе Ubuntu правильным, официально рекомендуемым способом, настроить возможность работы с Docker без постоянного использования sudo, и проверить корректность установки.

Теория

Docker Engine vs Docker Desktop. Как мы затронули в практических заданиях предыдущего урока, важно различать эти два понятия: Docker Engine — это непосредственно движок контейнеризации (демон dockerd плюс интерфейс командной строки docker), который мы будем устанавливать и использовать напрямую в терминале Ubuntu. Docker Desktop — это дополнительное графическое приложение (в первую очередь предназначенное для пользователей Windows и macOS, где нет собственного, встроенного ядра Linux), включающее в себя Docker Engine внутри лёгкой виртуальной машины, вместе с удобным графическим интерфейсом. Поскольку мы работаем непосредственно в Ubuntu — полноценной Linux-системе — нам не нужен Docker Desktop; мы устанавливаем Docker Engine напрямую, что является наиболее эффективным и «нативным» способом использования Docker.

Официальный способ установки — через собственный репозиторий Docker. Хотя Ubuntu имеет в своих стандартных репозиториях пакет с похожим названием (docker.io), официально рекомендуемый способ — добавить собственный APT-репозиторий Docker (вспомните материал Блока 9 про добавление сторонних репозиториев) и устанавливать пакеты непосредственно оттуда — это гарантирует получение самой актуальной, официально поддерживаемой версии Docker, а не потенциально устаревшей версии из стандартных репозиториев Ubuntu.

Компоненты, устанавливаемые вместе с Docker Engine:

Группа docker и работа без постоянного sudo. По умолчанию, сразу после установки, для выполнения практически любой команды docker требуются права root (через sudo), поскольку демон Docker взаимодействует с ядром на низком уровне, требующем повышенных привилегий. Однако существует удобный и широко практикуемый способ разрешить обычному пользователю выполнять команды Docker без постоянного sudo: добавить этого пользователя в специальную группу docker (вспомните управление группами из урока 8.4) — членство в этой группе даёт права на взаимодействие с сокетом (специальным механизмом межпроцессного взаимодействия) демона Docker.

Важное соображение безопасности. Стоит понимать: добавление пользователя в группу docker фактически предоставляет ему возможности, эквивалентные правам root на всей системе — потому что через Docker можно, например, запустить контейнер с полным доступом к файловой системе хоста. Это важно учитывать при администрировании многопользовательских систем: членство в группе docker следует давать только полностью доверенным пользователям, с тем же уровнем осторожности, что и права sudo (материал Блока 8).

Практика

Шаг 1. Удалите потенциально существующие старые или конфликтующие пакеты, связанные с Docker, если они когда-либо устанавливались ранее (безопасная превентивная мера, даже если такие пакеты отсутствуют в вашей системе — команда просто ничего не сделает):

```bash

for пакет in docker.io docker-doc docker-compose podman-docker containerd runc; do sudo apt remove -y $пакет 2>/dev/null; done

```

(Использован цикл for из урока 15.5 для проверки сразу нескольких потенциально конфликтующих пакетов.)

Шаг 2. Обновите список пакетов и установите необходимые вспомогательные утилиты для добавления нового репозитория (аналогично тому, как мы добавляли сторонние репозитории в Блоке 9):

```bash

sudo apt update

sudo apt install ca-certificates curl

```

Шаг 3. Создайте директорию для GPG-ключей (механизм проверки подлинности пакетов, о котором мы говорили в Блоке 9) и добавьте официальный GPG-ключ Docker:

```bash

sudo install -m 0755 -d /etc/apt/keyrings

sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc

sudo chmod a+r /etc/apt/keyrings/docker.asc

```

Шаг 4. Добавьте официальный репозиторий Docker в список источников APT:

```bash

echo \

"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \

$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \

sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

```

(Обратите внимание на использование подстановки команд $(...), изученной в Блоке 15, для автоматического определения архитектуры процессора и кодового имени версии Ubuntu — это делает команду универсальной, работающей корректно на разных архитектурах и версиях Ubuntu без ручного редактирования.)

[Скриншот терминала]

Шаг 5. Обновите список пакетов, теперь уже включающий новый репозиторий, и установите Docker:

```bash

sudo apt update

sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

```

Шаг 6. Проверьте, что Docker успешно установлен и служба запущена (используя знакомые нам из Блока 10 команды):

```bash

sudo systemctl status docker

```

Что вы увидите: Active: active (running) — демон Docker работает.

[Скриншот терминала]

Шаг 7. Проверьте версию установленного Docker:

```bash

sudo docker --version

sudo docker compose version

```

Шаг 8. Выполните первую тестовую команду Docker (пока ещё с sudo, поскольку мы не настроили работу без него) — запустите официальный тестовый образ, специально созданный для проверки корректности установки:

```bash

sudo docker run hello-world

```

Что вы увидите: Docker скачает (если ещё не скачан) небольшой тестовый образ hello-world и запустит его — образ выведет приветственное сообщение, объясняющее, что произошло: Docker-клиент связался с демоном, демон скачал образ из реестра (Docker Hub), создал новый контейнер из этого образа, контейнер вывел сообщение, а затем демон остановил контейнер, так как задача была выполнена.

[Скриншот вывода hello-world]

Шаг 9. Настройте возможность работы с Docker без постоянного sudo — добавьте текущего пользователя в группу docker:

```bash

sudo usermod -aG docker $USER

```

(Мы уже использовали usermod -aG для добавления пользователей в группы в уроке 8.4; $USER — переменная окружения, автоматически содержащая имя текущего пользователя.)

Шаг 10. Примените изменение членства в группе для текущей сессии (напоминаем механизм из Блока 8 — изменения группы не применяются автоматически к уже открытым сессиям):

```bash

newgrp docker

```

(Альтернативный, более надёжный способ — просто закрыть терминал и открыть новый, или перезайти в систему полностью.)

Шаг 11. Проверьте, что теперь Docker работает без sudo:

```bash

docker run hello-world

```

Что вы увидите: то же приветственное сообщение, но на этот раз без необходимости вводить пароль через sudo — поскольку ваш пользователь теперь является членом группы docker.

[Скриншот терминала без sudo]

Шаг 12. Проверьте членство в группах (используя команду groups, знакомую из Блока 8):

```bash

groups

```

Что вы увидите: среди прочих групп теперь присутствует docker.

Шаг 13. Убедитесь, что Docker настроен на автоматический запуск при загрузке системы (стандартная практика для служб, которые должны быть постоянно доступны — материал Блока 10):

```bash

sudo systemctl is-enabled docker

```

Что вы увидите: enabled — Docker будет автоматически запускаться при каждой загрузке системы.

Разбор команд

Команда docker --version

Команда docker run

Команда usermod -aG docker $USER

Команда newgrp

Возможные ошибки

Почему возникает: пользователь ещё не добавлен в группу docker, либо добавлен, но изменение членства ещё не применено к текущей сессии (шаги 9-10 практики не были выполнены, либо не был выполнен newgrp/перезаход).

Как определить: именно это сообщение об ошибке; команда groups не показывает docker в списке.

Как исправить: выполнить sudo usermod -aG docker $USER, затем newgrp docker (или полностью перезайти в систему/переоткрыть терминал).

Почему возникает: служба Docker не запущена.

Как определить: sudo systemctl status docker покажет статус, отличный от active (running).

Как исправить: sudo systemctl start docker; если служба не запускается вовсе, изучить логи через sudo journalctl -u docker -n 50 (материал Блока 10) для диагностики конкретной причины.

Почему возникает: пропущен шаг 1 практики (удаление потенциально существующих старых пакетов) на системе, где Docker уже был ранее установлен через стандартный пакет Ubuntu.

Как исправить: полностью удалить старый пакет (sudo apt remove docker.io) перед установкой из официального репозитория Docker.

Практические задания

  1. Изучите вывод команды docker info (не разбиравшуюся подробно в этом уроке, но крайне информативную) — она выводит подробную информацию о текущей конфигурации демона Docker: количество контейнеров, образов, используемый драйвер хранения, версию ядра и многое другое. Запишите в конспект пять любых параметров из этого вывода, которые кажутся вам наиболее интересными или полезными для администратора.
  2. Проверьте, включён ли Docker в автозапуск при загрузке системы (шаг 13 практики), и если по какой-то причине не включён — включите его командой sudo systemctl enable docker (материал Блока 10).
  3. Изучите (через man или поиск в интернете) назначение флага --rm у команды docker run, и объясните в конспекте, для какой практической цели он может быть полезен при частом тестовом запуске контейнеров (мы вернёмся к этому флагу подробнее в практике следующего урока).

Проверка знаний

Вопрос 1. Почему рекомендуется устанавливать Docker из официального репозитория Docker, а не из стандартных репозиториев Ubuntu (пакет docker.io)?

Ответ: Официальный репозиторий Docker обычно предоставляет более актуальную версию Docker Engine, тогда как версия в стандартных репозиториях Ubuntu может заметно отставать от текущей официальной версии, поскольку следует собственному циклу тестирования и включения пакетов в дистрибутив. Использование официального репозитория гарантирует доступ к последним функциям, исправлениям безопасности и полному набору официально поддерживаемых компонентов (включая плагины docker-buildx-plugin и docker-compose-plugin).

Вопрос 2. Почему добавление пользователя в группу docker эквивалентно предоставлению ему фактических прав root на всей системе, даже если формально пользователь не имеет sudo-доступа?

Ответ: Через Docker можно запустить контейнер с расширенными привилегиями и с примонтированными директориями хост-системы (например, смонтировать корневую файловую систему хоста внутрь контейнера) — получив таким образом полный доступ на чтение и запись ко всей файловой системе хоста, минуя обычные ограничения прав пользователя. Поскольку членство в группе docker даёт возможность запускать произвольные контейнеры с такими настройками, оно фактически эквивалентно неограниченному доступу к системе — и должно предоставляться с той же осторожностью, что и права sudo.

Итоги урока

Вы изучили: различие между Docker Engine и Docker Desktop, компоненты, устанавливаемые вместе с Docker, значение группы docker для работы без постоянного sudo, соображения безопасности, связанные с членством в этой группе.

Вы умеете: устанавливать Docker Engine из официального репозитория, проверять корректность установки через тестовый образ hello-world, настраивать работу с Docker без постоянного sudo.

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

Урок 17.4. Основные команды: `run`, `ps`, `images`

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

Теория

Команда docker run — создание и запуск контейнера. Это, вероятно, самая важная и часто используемая команда Docker — она объединяет сразу несколько операций: если указанного образа ещё нет локально, Docker сначала скачивает его из реестра (эта операция называется pull, доступная и как отдельная команда docker pull), затем создаёт новый контейнер из этого образа, и наконец запускает его.

```bash

docker run [опции] образ [команда]

```

Важные опции команды docker run:

Команда docker ps — просмотр запущенных контейнеров.

```bash

docker ps

```

Показывает список только запущенных в данный момент контейнеров, с информацией: ID контейнера, использованный образ, команда, время создания, статус, проброшенные порты, имя.

```bash

docker ps -a

```

Флаг -a (all) показывает все контейнеры, включая уже остановленные — это важно понимать: остановленный контейнер не исчезает автоматически (если не был использован флаг --rm), он продолжает существовать в остановленном состоянии, занимая место на диске, пока не будет явно удалён.

Управление жизненным циклом контейнера:

```bash

docker stop ID_или_имя # аккуратно остановить работающий контейнер (отправляет сигнал SIGTERM, дожидается корректного завершения — вспомните сигналы из Блока 11)

docker start ID_или_имя # запустить ранее остановленный (но не удалённый) контейнер

docker restart ID_или_имя # перезапустить контейнер

docker kill ID_или_имя # принудительно, немедленно остановить контейнер (аналог SIGKILL из Блока 11)

docker rm ID_или_имя # удалить остановленный контейнер полностью

docker rm -f ID_или_имя # принудительно остановить и сразу удалить контейнер

```

Команда docker logs — просмотр вывода контейнера. Особенно важна для контейнеров, запущенных в фоновом режиме (-d), поскольку без неё вы не увидите, что контейнер вообще выводит (например, лог-сообщения работающего внутри веб-сервера):

```bash

docker logs ID_или_имя

docker logs -f ID_или_имя # "следить" за логами в реальном времени, аналогично tail -f (материал Блока 10)

```

Команда docker exec — выполнение команды внутри уже запущенного контейнера. В отличие от docker run (который создаёт новый контейнер), docker exec позволяет выполнить дополнительную команду внутри уже работающего контейнера — например, открыть интерактивную оболочку для исследования его текущего состояния:

```bash

docker exec -it ID_или_имя bash

```

Это одна из самых часто используемых команд при отладке — позволяет буквально «зайти внутрь» работающего контейнера и осмотреться, используя все уже знакомые нам команды исследования системы из ранних блоков курса (ls, ps, cat и так далее — контейнер, если он основан на полноценном дистрибутиве вроде Ubuntu, предоставляет практически такую же среду, к которой мы привыкли).

Команды для работы с образами:

```bash

docker images # список всех локально скачанных/собранных образов

docker pull имя_образа:тег # скачать образ из реестра без запуска контейнера

docker rmi имя_образа_или_ID # удалить локальный образ

```

Идентификация контейнеров и образов. Docker присваивает каждому контейнеру и каждому образу уникальный идентификатор (ID) — длинную шестнадцатеричную строку, аналогично хешам коммитов Git (материал Блока 16). Как и с хешами Git, для большинства команд достаточно указать лишь первые несколько символов ID — при условии, что этого достаточно для однозначной идентификации среди существующих контейнеров/образов на вашей машине. Кроме того, для контейнеров можно использовать явно заданное или автоматически сгенерированное имя вместо ID — что обычно удобнее для человека.

Практика

Шаг 1. Запустите первый интерактивный контейнер на основе Ubuntu и исследуйте его изнутри:

```bash

docker run -it --name my-first-container ubuntu bash

```

Что произойдёт: если образ ubuntu ещё не скачан локально, Docker сначала скачает его, затем создаст и запустит контейнер, и вы окажетесь внутри интерактивной bash-оболочки этого контейнера — обратите внимание на изменившееся приглашение командной строки, характерное для контейнера.

[Скриншот терминала внутри контейнера]

Шаг 2. Находясь внутри контейнера, изучите его окружение — примените уже хорошо знакомые команды из ранних блоков курса:

```bash

whoami

hostname

cat /etc/os-release

ps aux

ls /

```

Что вы увидите: контейнер выглядит как полноценная, хотя и весьма минималистичная, Linux-система — со своим собственным hostname (обычно совпадающим с началом ID контейнера), собственным списком процессов (обратите внимание, насколько он короче, чем на вашей хост-системе — контейнер по умолчанию не содержит множества фоновых служб, характерных для полноценной установки), и стандартной файловой структурой.

[Скриншот содержимого контейнера]

Шаг 3. Выйдите из контейнера:

```bash

exit

```

Что произойдёт: поскольку основной процесс контейнера — это как раз тот самый bash, в котором вы работали, выход из него завершает работу контейнера целиком (контейнер останавливается, но не удаляется, так как флаг --rm не был указан).

Шаг 4. Проверьте список запущенных контейнеров (должен быть пуст) и список всех контейнеров, включая остановленные:

```bash

docker ps

docker ps -a

```

Что вы увидите: docker ps покажет пустой список (нет запущенных контейнеров), а docker ps -a покажет ваш контейнер my-first-container в статусе Exited.

[Скриншот терминала с обеими командами]

Шаг 5. Запустите контейнер повторно, на этот раз в фоновом режиме, с примером простого долгоработающего процесса:

```bash

docker run -d --name background-container ubuntu sleep 300

```

(Команда sleep 300 внутри контейнера просто «спит» 5 минут — простой способ создать долгоработающий процесс для демонстрации без необходимости устанавливать реальное приложение.)

Шаг 6. Проверьте, что контейнер работает в фоне:

```bash

docker ps

```

Что вы увидите: контейнер background-container в списке запущенных, со статусом Up и временем работы.

[Скриншот терминала]

Шаг 7. Выполните дополнительную команду внутри уже работающего контейнера через docker exec:

```bash

docker exec background-container ps aux

```

Что вы увидите: список процессов внутри контейнера — в том числе процесс sleep 300, запущенный при старте контейнера.

Шаг 8. Откройте интерактивную оболочку внутри уже запущенного контейнера:

```bash

docker exec -it background-container bash

```

Изучите файловую систему изнутри, затем выйдите не завершая контейнер — в этом ключевое отличие от шага 3: exit из сессии, открытой через exec, закрывает только эту дополнительную сессию, но не влияет на основной процесс контейнера (sleep 300), который продолжает работать:

```bash

exit

```

Шаг 9. Убедитесь, что контейнер всё ещё работает после выхода из exec-сессии:

```bash

docker ps

```

Шаг 10. Запустите реальный веб-сервер в контейнере с пробросом порта — практический пример, предвосхищающий материал Блока 19:

```bash

docker run -d --name my-nginx -p 8080:80 nginx

```

Что произойдёт: Docker скачает официальный образ nginx, запустит контейнер в фоне, и свяжет порт 8080 на вашей хост-машине с портом 80 внутри контейнера (стандартный HTTP-порт, о котором мы говорили в Блоке 12).

Шаг 11. Проверьте, что веб-сервер действительно доступен через проброшенный порт, используя curl (мы уже применяли эту утилиту ранее в курсе):

```bash

curl http://localhost:8080

```

Что вы увидите: HTML-код приветственной страницы Nginx — сервер, работающий внутри изолированного контейнера, реально отвечает на запросы через проброшенный порт хост-машины.

[Скриншот ответа от Nginx в контейнере]

Шаг 12. Посмотрите логи этого веб-сервера:

```bash

docker logs my-nginx

```

Что вы увидите: строку с записью о запросе, который мы только что сделали через curl — Nginx логирует каждый обработанный HTTP-запрос, что мы подробно изучим в Блоке 19.

Шаг 13. Изучите список локальных образов:

```bash

docker images

```

Что вы увидите: как минимум ubuntu, nginx и hello-world (из предыдущего урока) — с указанием размера каждого образа. Обратите внимание на существенную разницу в размерах.

[Скриншот списка образов]

Шаг 14. Остановите и удалите все созданные в этом уроке контейнеры для чистоты (полезная практика после экспериментов, чтобы не накапливать ненужные ресурсы):

```bash

docker stop background-container my-nginx

docker rm my-first-container background-container my-nginx

docker ps -a

```

Что вы увидите: список контейнеров теперь пуст (или не содержит удалённых нами контейнеров).

Разбор команд

Команда docker run

Команда docker ps

Управление жизненным циклом: docker stop/start/restart/kill/rm.

Команда docker logs

Команда docker exec

Команды для образов: docker images (список), docker pull (скачать), docker rmi (удалить).

Возможные ошибки

Почему возникает: попытка создать новый контейнер с именем, которое уже занято существующим (даже остановленным!) контейнером — имена контейнеров должны быть уникальны.

Как исправить: удалить старый контейнер с этим именем (docker rm имя), либо использовать другое имя для нового контейнера.

Почему возникает: попытка пробросить порт хоста (-p), который уже занят другим контейнером или другим процессом на хост-машине (вспомните материал Блока 12 про занятые порты).

Как определить: проверить занятые порты через sudo ss -tlnp (материал Блока 12), либо docker ps для проверки, не занят ли порт другим контейнером.

Как исправить: выбрать другой порт хоста для проброса (например, -p 8081:80 вместо -p 8080:80), либо остановить конфликтующий контейнер/процесс.

Почему возникает: без флага -d контейнер запускается в «прикреплённом» (attached) режиме — терминал ожидает завершения работы контейнера.

Как исправить: для долгоработающих служб (веб-серверы и подобное) использовать -d; если контейнер уже запущен без -d и вы хотите вернуть управление терминалом, не останавливая контейнер, можно отсоединиться сочетанием клавиш Ctrl+P, затем Ctrl+Q (специальная последовательность именно для «отсоединения» без остановки контейнера).

Практические задания

  1. Запустите контейнер на основе образа alpine (значительно более компактный дистрибутив, чем ubuntu, как мы упоминали в теории урока 17.2) в интерактивном режиме, сравните вывод cat /etc/os-release с тем, что вы видели внутри контейнера Ubuntu, и сравните размеры обоих образов через docker images.
  2. Запустите три разных контейнера в фоновом режиме на основе одного и того же образа nginx, но с разными именами и разными пробросами портов хоста (например, 8081, 8082, 8083), убедитесь через curl, что все три независимо отвечают на своих портах, затем очистите все три контейнера.
  3. Изучите разницу между docker stop и docker kill на практике: запустите контейнер, выполняющий sleep 300, и сначала попробуйте остановить его через docker stop (засеките, сколько это заняло времени — обычно есть небольшая задержка, поскольку Docker сначала пытается корректно завершить процесс через SIGTERM, и только затем принудительно через SIGKILL, если процесс не отвечает за отведённое время), затем повторите эксперимент с docker kill и сравните разницу в скорости остановки.

Проверка знаний

Вопрос 1. В чём разница между docker run и docker exec, и когда следует использовать каждую из этих команд?

Ответ: docker run создаёт новый контейнер из указанного образа и запускает его. docker exec выполняет дополнительную команду внутри уже существующего, работающего контейнера, не создавая новый контейнер. docker run используется, когда контейнера ещё не существует и его нужно создать с нуля; docker exec используется, когда контейнер уже запущен (например, работает веб-сервер в фоновом режиме) и требуется выполнить в нём дополнительную команду — например, открыть интерактивную оболочку для отладки, не прерывая основной рабочий процесс контейнера.

Вопрос 2. Зачем нужен флаг -p при запуске контейнера, и что произойдёт, если его не указать при запуске веб-сервера в контейнере?

Ответ: Флаг -p хост:контейнер пробрасывает (связывает) порт на хост-машине с портом внутри контейнера, поскольку контейнер по умолчанию изолирован собственным сетевым namespace (материал урока 17.1) и недоступен снаружи без явного проброса. Если не указать -p при запуске веб-сервера, служба внутри контейнера будет прекрасно работать и слушать свой порт внутри изолированной сети контейнера, но останется совершенно недоступной с хост-машины или из внешней сети — обращения к localhost:порт с хоста не будут доходить до контейнера, поскольку явной связи между портом хоста и портом контейнера установлено не было.

Итоги урока

Вы изучили: команду docker run и её ключевые опции (-d, -it, --name, -p, -e, --rm), управление жизненным циклом контейнеров (stop/start/restart/kill/rm), команды docker ps/docker logs/docker exec, работу с локальными образами (docker images/pull/rmi).

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

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

Урок 17.5. Dockerfile: создание собственных образов

Освоить синтаксис Dockerfile для описания сборки собственных Docker-образов, понять назначение основных инструкций, научиться собирать образы командой docker build, и усвоить лучшие практики написания эффективных, компактных Dockerfile.

Теория

Зачем создавать собственные образы. Готовые образы с Docker Hub прекрасно подходят для запуска стандартного, широко используемого программного обеспечения (веб-серверов, баз данных и подобного), но рано или поздно возникает необходимость упаковать в контейнер своё собственное приложение — например, ваш bash-скрипт из Блока 15, или веб-приложение, которое вы разрабатываете. Именно для этого существует Dockerfile — текстовый файл с пошаговыми инструкциями, описывающими, как собрать образ с нуля (точнее, обычно на основе существующего базового образа, а не действительно «с нуля»).

Основные инструкции Dockerfile:

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

```dockerfile

FROM ubuntu:22.04

```

WORKDIR — задаёт рабочую директорию внутри контейнера, действующую для всех последующих инструкций (аналог cd, но выполняемый на этапе сборки образа, а не как обычная команда) — если указанная директория не существует, Docker создаёт её автоматически:

```dockerfile

WORKDIR /app

```

COPY — копирует файлы с локальной машины (точнее, из «контекста сборки», о котором чуть ниже) внутрь собираемого образа:

```dockerfile

COPY имя_локального_файла /путь/внутри/образа

COPY . /app

```

RUN — выполняет команду во время сборки образа, результат выполнения (изменения файловой системы) сохраняется как новый слой образа. Чаще всего используется для установки пакетов, зависимостей приложения:

```dockerfile

RUN apt update && apt install -y python3

```

ENV — устанавливает переменную окружения, которая будет доступна и во время дальнейшей сборки, и во всех контейнерах, запущенных из готового образа:

```dockerfile

ENV APP_ENV=production

```

EXPOSE — документирует, какой порт приложение внутри контейнера предполагает использовать (важно понимать: эта инструкция сама по себе не пробрасывает порт наружу — реальный проброс всё равно требует явного флага -p при запуске контейнера через docker run, как мы изучали в предыдущем уроке; EXPOSE служит скорее документацией и подсказкой для инструментов вроде Docker Compose, который мы изучим в следующем уроке):

```dockerfile

EXPOSE 8080

```

CMD — задаёт команду, которая будет выполняться при запуске контейнера (в отличие от RUN, выполняющегося на этапе сборки самого образа). В Dockerfile может быть только одна инструкция CMD (если указано несколько — действует только последняя):

```dockerfile

CMD ["python3", "app.py"]

```

(Обратите внимание на «exec-форму» записи — список строк в квадратных скобках, а не одну строку команды — это рекомендуемый, более предсказуемый способ записи команды, поскольку не требует прохождения через shell-интерпретацию.)

ENTRYPOINT — похожа на CMD, но задаёт команду, которая практически всегда будет выполнена при запуске контейнера, менее подверженную случайной перезаписи при запуске (CMD, в отличие от ENTRYPOINT, можно легко переопределить, указав другую команду прямо в docker run образ команда). Разница между CMD и ENTRYPOINT, и продвинутые способы их совместного использования — тема, выходящая за рамки этого вводного урока, но важно знать об их существовании и базовом различии.

Контекст сборки (build context). Когда вы запускаете сборку образа, вы указываете Docker путь к директории, называемой контекстом сборки — именно из неё инструкция COPY берёт файлы для копирования внутрь образа. Весь контекст сборки (все файлы указанной директории) отправляется демону Docker перед началом сборки — поэтому важно не включать в директорию сборки лишние, ненужные для образа файлы (большие файлы данных, временные файлы), что замедлит процесс сборки. Для управления этим существует специальный файл .dockerignore, работающий по тому же принципу, что уже знакомый нам .gitignore (материал Блока 16) — перечисленные в нём шаблоны файлов исключаются из отправляемого контекста сборки.

Команда docker build — сборка образа из Dockerfile:

```bash

docker build -t имя_образа:тег путь_к_контексту

```

Флаг -t (tag) задаёт имя (и опционально тег) для собираемого образа; путь к контексту сборки обычно указывается как . (текущая директория, если вы находитесь непосредственно там, где лежит Dockerfile).

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

Многоступенчатая сборка (multi-stage builds) — продвинутая, но крайне полезная техника (кратко, для общего представления). Позволяет использовать несколько инструкций FROM в одном Dockerfile — например, одна «ступень» сборки содержит все инструменты, необходимые для компиляции приложения (компилятор, инструменты сборки), а финальная, значительно более компактная ступень содержит только скомпилированный результат, без ненужных для реальной работы инструментов разработки — что заметно уменьшает итоговый размер образа. Детальное освоение этой техники выходит за рамки данного вводного курса, но полезно знать о её существовании для дальнейшего самостоятельного изучения.

Практика

Шаг 1. Создайте директорию для нашего первого собственного образа:

```bash

mkdir -p ~/my-docker-app

cd ~/my-docker-app

```

Шаг 2. Создайте простой bash-скрипт, который мы упакуем в контейнер (применение материала Блока 15):

```bash

cat > greet.sh << 'EOF'

#!/bin/bash

echo "Здравствуйте из контейнера, собранного своими руками!"

echo "Текущая дата внутри контейнера: $(date)"

echo "Полученные аргументы: $@"

EOF

chmod +x greet.sh

```

Шаг 3. Создайте Dockerfile:

```bash

nano Dockerfile

```

Содержимое:

```dockerfile

FROM ubuntu:22.04

WORKDIR /app

COPY greet.sh /app/greet.sh

RUN chmod +x /app/greet.sh

ENV GREETING_SOURCE="учебный контейнер курса Linux"

CMD ["/app/greet.sh"]

```

Сохраните: Ctrl+O, Enter, Ctrl+X.

[Скриншот содержимого Dockerfile]

Шаг 4. Соберите образ:

```bash

docker build -t my-greeting-app:1.0 .

```

Что вы увидите: пошаговый вывод выполнения каждой инструкции Dockerfile — Docker скачивает базовый образ (если его ещё нет), выполняет каждую инструкцию, создавая новый слой, и в конце сообщает об успешной сборке с ID итогового образа.

[Скриншот процесса сборки]

Шаг 5. Проверьте, что образ появился в списке локальных образов:

```bash

docker images

```

Что вы увидите: my-greeting-app с тегом 1.0 в списке, наряду с ранее использованными образами.

Шаг 6. Запустите контейнер из только что собранного образа:

```bash

docker run my-greeting-app:1.0

```

Что вы увидите: вывод вашего скрипта greet.sh, выполненного внутри контейнера — приветствие и текущую дату.

[Скриншот запуска собственного контейнера]

Шаг 7. Запустите тот же контейнер, передав дополнительные аргументы (продемонстрировав, как CMD может быть расширен аргументами командной строки):

```bash

docker run my-greeting-app:1.0 arg1 arg2

```

Что вы увидите: в выводе появятся переданные аргументы arg1 arg2, обработанные внутри скрипта через $@ (материал урока 15.3).

Шаг 8. Продемонстрируйте кэширование слоёв — внесите небольшое изменение в скрипт (не меняя Dockerfile) и пересоберите образ:

```bash

echo "echo \"Версия скрипта обновлена!\"" >> greet.sh

docker build -t my-greeting-app:1.0 .

```

Что вы увидите: инструкции FROM и WORKDIR будут взяты из кэша (обычно помечено CACHED в выводе), но COPY greet.sh ... выполнится заново, поскольку содержимое файла изменилось — а вслед за ней и все последующие инструкции тоже будут пересчитаны заново (поскольку кэш работает последовательно: если конкретный слой изменился, все последующие за ним слои тоже должны быть пересобраны, даже если их собственное содержимое не менялось).

[Скриншот пересборки с частичным использованием кэша]

Шаг 9. Создайте файл .dockerignore для демонстрации управления контекстом сборки:

```bash

echo "какие-то временные данные" > temp_data.txt

echo "temp_data.txt" > .dockerignore

```

Теперь при следующей сборке файл temp_data.txt не будет включён в контекст сборки, даже если бы Dockerfile содержал инструкцию COPY . /app, которая скопировала бы всё содержимое директории — файл .dockerignore исключает его заранее.

Шаг 10. Изучите историю слоёв собранного образа (полезная команда для понимания структуры образа):

```bash

docker history my-greeting-app:1.0

```

Что вы увидите: список слоёв образа, от базового (внизу списка) до самого верхнего, добавленного последней инструкцией Dockerfile, с указанием размера, который добавляет каждый слой.

[Скриншот истории слоёв образа]

Шаг 11. Присвойте образу дополнительный тег (полезная операция, если хотите пометить один и тот же образ несколькими способами — например, конкретной версией и одновременно latest):

```bash

docker tag my-greeting-app:1.0 my-greeting-app:latest

docker images

```

Что вы увидите: теперь в списке образов присутствуют обе строки (1.0 и latest), но, что важно, они указывают на один и тот же ID образа — тегирование не создаёт копию образа, лишь дополнительное «имя» для того же самого объекта.

Разбор команд

Основные инструкции Dockerfile:

| Инструкция | Назначение |

|---|---|

| FROM | Базовый образ (обязательная, обычно первая инструкция) |

| WORKDIR | Рабочая директория внутри образа для последующих инструкций |

| COPY | Скопировать файлы из контекста сборки внутрь образа |

| RUN | Выполнить команду во время сборки (создаёт новый слой) |

| ENV | Установить переменную окружения |

| EXPOSE | Задокументировать используемый портом (не пробрасывает его реально) |

| CMD | Команда по умолчанию при запуске контейнера (может быть переопределена) |

| ENTRYPOINT | Команда, менее подверженная переопределению при запуске |

Команда docker build

Команда docker tag

Команда docker history

Возможные ошибки

Почему возникает: попытка скопировать файл, который либо не существует в указанной директории контекста сборки, либо был исключён через .dockerignore.

Как исправить: проверить точное имя и расположение файла относительно директории, из которой запускается docker build; проверить содержимое .dockerignore.

Почему возникает: файл был скопирован через COPY, но забыта инструкция RUN chmod +x для установки прав на выполнение (мы специально включили эту инструкцию в практике этого урока).

Как определить: сообщение вида permission denied при попытке запуска.

Как исправить: добавить RUN chmod +x /путь/к/скрипту в Dockerfile после соответствующей инструкции COPY, и пересобрать образ.

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

Как исправить/избежать: следовать рекомендации из теории — размещать редко изменяющиеся инструкции (базовый образ, установка системных зависимостей) раньше в Dockerfile, а часто изменяющиеся (копирование кода приложения) — ближе к концу файла.

Практические задания

  1. Модифицируйте Dockerfile из практики, добавив инструкцию RUN apt update && apt install -y curl, чтобы внутри образа была доступна утилита curl (её нет в базовом образе ubuntu по умолчанию) — пересоберите образ и через docker run -it имя_образа bash проверьте, что curl теперь действительно доступна внутри контейнера.
  2. Создайте Dockerfile для образа на основе python:3-slim (базовый образ с предустановленным Python, значительно более компактный вариант, чем полный python:3), скопируйте в него простой Python-скрипт (print("Привет из Python-контейнера!") вполне подойдёт, даже если вы специально не изучали Python в этом курсе — базовые принципы синтаксиса интуитивно понятны), и настройте CMD для его запуска через ["python3", "имя_скрипта.py"].
  3. Изучите вывод docker history для официального образа nginx (docker history nginx, предварительно скачав его через docker pull nginx, если ещё не скачан) — сравните количество и размер слоёв с вашим собственным, значительно более простым образом, и опишите в конспекте, какие из увиденных слоёв кажутся вам соответствующими различным инструкциям в оригинальном Dockerfile этого образа.

Проверка знаний

Вопрос 1. В чём разница между инструкциями RUN и CMD в Dockerfile — когда выполняется каждая из них?

Ответ: RUN выполняется во время сборки образа — результат её выполнения (изменения файловой системы, например установленные пакеты) сохраняется как постоянная часть образа, становясь новым слоем. CMD определяет команду, которая должна быть выполнена при запуске контейнера из уже готового образа, а не во время его сборки — она не создаёт слой образа, а лишь задаёт поведение контейнера по умолчанию в момент его старта, и, в отличие от RUN, может быть свободно переопределена явным указанием другой команды в docker run образ другая_команда.

Вопрос 2. Почему рекомендуется размещать инструкцию установки зависимостей (RUN apt install ...) раньше в Dockerfile, чем инструкцию копирования исходного кода приложения (COPY . /app), если код приложения изменяется намного чаще, чем список зависимостей?

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

Итоги урока

Вы изучили: основные инструкции Dockerfile (FROM, WORKDIR, COPY, RUN, ENV, EXPOSE, CMD, ENTRYPOINT), концепцию контекста сборки и файла .dockerignore, механизм кэширования слоёв и практическую рекомендацию по порядку инструкций, краткое представление о многоступенчатой сборке.

Вы умеете: писать Dockerfile для упаковки собственных скриптов/приложений в образ, собирать образы командой docker build, оптимизировать порядок инструкций для эффективного использования кэша, изучать структуру слоёв через docker history.

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

Урок 17.6. Docker Compose: управление многоконтейнерными приложениями

Понять проблему координации нескольких взаимодействующих контейнеров, освоить синтаксис файла docker-compose.yml, научиться запускать, останавливать и управлять целыми группами контейнеров единой командой.

Теория

Проблема, которую решает Docker Compose. Реальные приложения редко состоят из одного-единственного контейнера. Типичное веб-приложение может включать: сам веб-сервер (например, Nginx), сервер приложения (обрабатывающий бизнес-логику), базу данных (например, PostgreSQL, которую мы изучим в Блоке 19), возможно, кэширующий сервис. Управлять каждым из этих контейнеров отдельными, длинными командами docker run со множеством опций (проброс портов, переменные окружения, тома для хранения данных, настройка сети для взаимодействия между контейнерами) — крайне неудобно, подвержено ошибкам, и плохо поддаётся version control (материал Блока 16).

Docker Compose — инструмент для определения и запуска многоконтейнерных Docker-приложений с помощью единого декларативного файла конфигурации, обычно называемого docker-compose.yml (или compose.yaml в более современном соглашении об именовании). Вместо запоминания и повторного набора длинных команд docker run, вы один раз описываете желаемое состояние всего приложения — какие контейнеры (сервисы) нужны, с какими параметрами, как они связаны между собой — и одной простой командой запускаете (или останавливаете) всё приложение целиком.

Формат YAML — краткое знакомство. Файл docker-compose.yml написан в формате YAML (YAML Ain't Markup Language) — человекочитаемом формате структурированных данных, широко используемом для файлов конфигурации во множестве современных инструментов (не только Docker Compose). Ключевые принципы синтаксиса YAML:

Базовая структура docker-compose.yml:

```yaml

services:

имя_сервиса_1:

image: имя_образа:тег

ports:

environment:

volumes:

имя_сервиса_2:

build: .

depends_on:

```

Ключевые директивы внутри описания каждого сервиса:

Автоматическая сеть между сервисами — одно из главных удобств Docker Compose. Все сервисы, описанные в одном файле docker-compose.yml, автоматически подключаются к общей, изолированной Docker-сети (детальнее сети рассмотрим в следующем уроке), и что особенно удобно — могут обращаться друг к другу просто по имени сервиса, как если бы это было доменное имя (вспомните материал про DNS из Блока 12). Например, если сервис приложения должен подключиться к базе данных, описанной как сервис с именем db в том же docker-compose.yml, внутри кода приложения достаточно указать адрес подключения db (вместо IP-адреса или localhost) — Docker автоматически обеспечивает разрешение этого «имени» в правильный внутренний адрес соответствующего контейнера.

Основные команды Docker Compose:

```bash

docker compose up # создать и запустить все описанные сервисы

docker compose up -d # то же самое, но в фоновом режиме

docker compose down # остановить и удалить все контейнеры, созданные этим docker-compose.yml

docker compose ps # список контейнеров, управляемых текущим docker-compose.yml

docker compose logs # логи всех сервисов

docker compose logs имя_сервиса # логи конкретного сервиса

docker compose stop # остановить сервисы, не удаляя контейнеры

docker compose start # запустить ранее остановленные сервисы

```

(Обратите внимание: в современных версиях Docker используется docker compose — как встроенная подкоманда самого Docker CLI, которую мы установили в уроке 17.3 через пакет docker-compose-plugin, — вместо более старого отдельного инструмента docker-compose с дефисом, который тоже можно встретить в старой документации и материалах, но постепенно вытесняется современным, интегрированным вариантом.)

Практика

Шаг 1. Создайте директорию для практики с Docker Compose:

```bash

mkdir -p ~/compose-practice

cd ~/compose-practice

```

Шаг 2. Создайте простой файл docker-compose.yml, описывающий два независимых сервиса — веб-сервер и вспомогательную страницу статуса (простая учебная демонстрация, предвосхищающая более сложные многоуровневые приложения, которые мы соберём в Блоке 19):

```bash

nano docker-compose.yml

```

Содержимое:

```yaml

services:

web:

image: nginx:alpine

ports:

restart: unless-stopped

status-page:

image: nginx:alpine

ports:

restart: unless-stopped

```

Сохраните.

[Скриншот содержимого docker-compose.yml]

Шаг 3. Запустите оба сервиса одной командой:

```bash

docker compose up -d

```

Что вы увидите: Docker Compose скачает образ nginx:alpine (если ещё не скачан), создаст изолированную сеть для этого проекта, и запустит оба контейнера.

[Скриншот запуска через docker compose]

Шаг 4. Проверьте статус запущенных сервисов:

```bash

docker compose ps

```

Что вы увидите: список из двух сервисов (web и status-page), их статус, и проброшенные порты.

Шаг 5. Убедитесь, что оба сервиса действительно доступны:

```bash

curl -s http://localhost:8080 | head -5

curl -s http://localhost:8081 | head -5

```

Шаг 6. Изучите логи обоих сервисов сразу:

```bash

docker compose logs

```

Шаг 7. Остановите и удалите все контейнеры этого проекта одной командой:

```bash

docker compose down

docker compose ps

```

Что вы увидите: docker compose ps теперь показывает пустой список — оба контейнера удалены, но образы остались локально скачанными (в отличие от контейнеров, образы не удаляются командой down).

[Скриншот после docker compose down]

Шаг 8. Создайте более реалистичный, продвинутый пример — приложение из собственного образа (собираемого через Dockerfile, аналогично уроку 17.5) и связанного с ним сервиса. Создайте отдельную директорию:

```bash

mkdir -p ~/compose-app-demo

cd ~/compose-app-demo

```

Шаг 9. Создайте простой Dockerfile для веб-страницы (используем nginx как базу, добавив собственную статическую HTML-страницу):

```bash

mkdir html

cat > html/index.html << 'EOF'

<!DOCTYPE html>

<html>

<head><title>Мой Compose-проект</title></head>

<body>

<h1>Привет из моего собственного контейнера!</h1>

<p>Собрано и запущено через Docker Compose.</p>

</body>

</html>

EOF

```

```bash

nano Dockerfile

```

Содержимое:

```dockerfile

FROM nginx:alpine

COPY html/ /usr/share/nginx/html/

```

Шаг 10. Создайте docker-compose.yml, использующий build вместо готового image:

```bash

nano docker-compose.yml

```

Содержимое:

```yaml

services:

my-web-app:

build: .

ports:

restart: unless-stopped

```

Шаг 11. Запустите — Docker Compose автоматически соберёт образ из Dockerfile перед запуском:

```bash

docker compose up -d --build

```

(Флаг --build явно указывает пересобрать образ перед запуском — полезно при внесении изменений в Dockerfile или связанные файлы после предыдущего запуска.)

Шаг 12. Проверьте результат:

```bash

curl http://localhost:9090

```

Что вы увидите: HTML-код вашей собственной страницы.

[Скриншот собственной страницы через Compose]

Шаг 13. Внесите изменение в HTML-файл, пересоберите и перезапустите:

```bash

sed -i 's/Привет из моего/Обновлённый привет из моего/' html/index.html

docker compose up -d --build

curl http://localhost:9090

```

Что вы увидите: обновлённый текст, подтверждающий, что пересборка образа и перезапуск контейнера прошли успешно.

Шаг 14. Очистите оба учебных проекта:

```bash

docker compose down

cd ~/compose-practice

docker compose down 2>/dev/null

```

Разбор команд

Файл docker-compose.yml — основные директивы:

| Директива | Назначение |

|---|---|

| services | Корневой раздел, содержащий определения всех сервисов |

| image | Использовать готовый образ |

| build | Собрать образ из Dockerfile по указанному пути |

| ports | Проброс портов (аналог -p в docker run) |

| environment | Переменные окружения (аналог -e) |

| volumes | Постоянное хранилище (детально в уроке 17.7) |

| depends_on | Порядок запуска сервисов |

| restart | Политика автоматического перезапуска |

Команда docker compose up

Команда docker compose down

Команда docker compose ps / logs

Возможные ошибки

Почему возникает: нарушение синтаксиса отступов YAML — например, использование табуляции вместо пробелов, или неправильное количество пробелов, нарушающее ожидаемую вложенность структуры.

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

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

Почему возникает: чаще всего — попытка использовать localhost вместо имени сервиса для внутреннего обращения между контейнерами; localhost внутри контейнера ссылается на сам этот контейнер, а не на другие сервисы.

Как исправить: использовать точное имя сервиса, заданное в docker-compose.yml (например, db, а не localhost), для обращения из кода одного сервиса к другому в пределах одного Compose-проекта.

Практические задания

  1. Расширьте docker-compose.yml из шага 2 практики, добавив третий сервис на основе образа httpd (альтернативный веб-сервер Apache) с собственным пробросом порта, и убедитесь, что все три сервиса запускаются и работают одновременно через одну команду docker compose up -d.
  2. Изучите директиву environment на практике — добавьте в один из сервисов docker-compose.yml переменную окружения (например, произвольную MY_VARIABLE=hello), пересоздайте контейнер, и через docker exec имя_сервиса env (команда env без аргументов выводит все переменные окружения текущего процесса) убедитесь, что переменная действительно присутствует внутри контейнера.
  3. Изучите (без обязательного практического выполнения, если материал покажется сложным) документацию по директиве depends_on с дополнительным условием condition: service_healthy — опишите в конспекте, для какой практической проблемы (кратко упомянутой в теории этого урока) предназначен этот более продвинутый механизм по сравнению с простым depends_on.

Проверка знаний

Вопрос 1. Какую конкретную проблему решает Docker Compose по сравнению с использованием отдельных команд docker run для каждого контейнера приложения?

Ответ: Docker Compose позволяет описать всё многоконтейнерное приложение целиком (несколько взаимосвязанных сервисов, их параметры — порты, переменные окружения, тома, зависимости между ними) в одном декларативном, версионируемом файле конфигурации, вместо необходимости помнить и вручную повторно набирать множество длинных, подверженных ошибкам команд docker run для каждого контейнера по отдельности. Это также позволяет управлять всей группой контейнеров как единым целым — запускать, останавливать, просматривать логи всего приложения одной простой командой, а не координировать множество отдельных команд для каждого контейнера.

Вопрос 2. Как сервисы, описанные в одном файле docker-compose.yml, обращаются друг к другу по сети, и почему для этого не нужно вручную настраивать IP-адреса?

Ответ: Docker Compose автоматически создаёт общую изолированную сеть для всех сервисов, описанных в одном файле, и предоставляет встроенное разрешение имён (аналогичное DNS, изученному в Блоке 12) — каждый сервис доступен другим сервисам в этой же сети просто по своему имени, указанному в docker-compose.yml (например, сервис с именем db доступен по адресу db для остальных сервисов того же проекта). Это избавляет от необходимости вручную узнавать, назначать или прописывать конкретные IP-адреса контейнеров, которые к тому же могут изменяться при каждом пересоздании контейнера.

Итоги урока

Вы изучили: проблему координации многоконтейнерных приложений, базовый синтаксис YAML, структуру файла docker-compose.yml и ключевые директивы (image, build, ports, environment, depends_on, restart), автоматическую сеть и разрешение имён между сервисами.

Вы умеете: описывать многоконтейнерные приложения в docker-compose.yml, запускать и останавливать целые группы контейнеров единой командой, комбинировать готовые образы с образами, собираемыми из собственного Dockerfile.

В следующем уроке потребуется: понимание жизненного цикла контейнеров — теперь мы разберёмся с постоянным хранением данных (тома) и более детально — с сетевым взаимодействием контейнеров.

Урок 17.7. Тома и сети

Понять проблему непостоянности данных внутри контейнеров и решить её через тома (volumes), разобраться в разных типах монтирования данных, и углубить понимание сетевого взаимодействия контейнеров через изучение сетевых драйверов Docker.

Теория

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

Тома (volumes) — механизм Docker для постоянного хранения данных. Том — это управляемая Docker область хранения данных, существующая независимо от жизненного цикла любого конкретного контейнера. Данные, помещённые в том, сохраняются даже после удаления контейнера, который его использовал, и могут быть подключены (примонтированы) к новому контейнеру, обеспечивая непрерывность данных.

Три основных способа управления данными в Docker:

  1. Именованные тома (named volumes) — управляются непосредственно Docker, хранятся в специальной, управляемой самим Docker области файловой системы хоста (обычно где-то в /var/lib/docker/volumes/, хотя точное расположение не должно иметь значения при обычной работе — взаимодействие с томами должно происходить только через команды Docker, а не через прямой доступ к этой директории). Это рекомендуемый, наиболее «Docker-нативный» способ для большинства случаев постоянного хранения данных.

```bash

docker volume create имя_тома

docker run -v имя_тома:/путь/внутри/контейнера образ

```

  1. Bind mounts (монтирование конкретной директории с хоста) — напрямую связывает конкретную директорию на хост-машине с директорией внутри контейнера. В отличие от именованных томов, здесь вы сами явно управляете и знаете точное расположение данных на хосте — что удобно, например, для разработки, когда нужно, чтобы изменения кода на хосте сразу отражались внутри работающего контейнера.

```bash

docker run -v /точный/путь/на/хосте:/путь/внутри/контейнера образ

```

  1. tmpfs mounts — данные хранятся исключительно в оперативной памяти хост-машины, никогда не записываются на физический диск. Используется относительно редко, для специфичных случаев, требующих временного хранения чувствительных данных без возможности их сохранения на диске (что выходит за рамки этого вводного урока).

Практическая разница между именованными томами и bind mounts:

| Характеристика | Именованный том | Bind mount |

|---|---|---|

| Управление | Docker управляет расположением | Вы точно знаете и указываете путь |

| Портируемость | Легко переносится между разными хост-машинами через команды Docker | Привязан к конкретному пути конкретной машины |

| Типичное применение | Постоянное хранение данных приложений (базы данных) | Разработка (монтирование кода), доступ к конкретным файлам конфигурации хоста |

| Синтаксис | -v имя_тома:путь | -v /абсолютный/путь/хоста:путь |

Основные команды управления томами:

```bash

docker volume create имя # создать именованный том

docker volume ls # список всех томов

docker volume inspect имя # подробная информация о томе (включая реальное расположение на диске)

docker volume rm имя # удалить том (только если он не используется ни одним контейнером)

docker volume prune # удалить все неиспользуемые тома

```

Сети Docker — более глубокий взгляд. Мы уже коснулись автоматических сетей в контексте Docker Compose (предыдущий урок), но полезно понять базовую структуру сетевых драйверов Docker в целом:

Пользовательские bridge-сети — важная практика для многоконтейнерных приложений. Хотя Docker Compose автоматически создаёт подходящую сеть для описанных в нём сервисов (как мы видели в предыдущем уроке), можно и вручную создавать собственные bridge-сети для группировки контейнеров, запускаемых напрямую через docker run (не через Compose), обеспечивая им то же самое удобство — обращение друг к другу по имени контейнера, аналогично тому, что мы наблюдали для сервисов Docker Compose:

```bash

docker network create моя-сеть

docker run --network моя-сеть --name container1 образ1

docker run --network моя-сеть --name container2 образ2

```

Находясь в одной пользовательской сети моя-сеть, container1 и container2 смогут обращаться друг к другу просто по имени (container1, container2), точно так же, как это происходит между сервисами одного Docker Compose проекта.

Основные команды управления сетями:

```bash

docker network ls # список сетей

docker network create имя # создать новую сеть (по умолчанию bridge-драйвер)

docker network inspect имя # подробная информация о сети, включая подключённые контейнеры

docker network rm имя # удалить сеть

```

Практика

Шаг 1. Продемонстрируйте проблему непостоянности данных — запустите контейнер, создайте внутри файл, удалите контейнер, и убедитесь в потере данных:

```bash

docker run --name temp-container ubuntu bash -c "echo 'важные данные' > /data.txt && cat /data.txt"

docker rm temp-container

docker run --name temp-container2 ubuntu bash -c "cat /data.txt 2>&1 || echo 'Файл не найден - данные потеряны!'"

docker rm temp-container2

```

Что вы увидите: во втором запуске файл /data.txt отсутствует — несмотря на то что мы «создали» его в первом контейнере, второй, новый контейнер (даже из того же образа) не имеет к нему никакого доступа, поскольку это два совершенно независимых экземпляра, и данные первого контейнера были уничтожены вместе с его удалением.

[Скриншот терминала, демонстрирующий потерю данных]

Шаг 2. Создайте именованный том и используйте его для сохранения данных между разными контейнерами:

```bash

docker volume create my-data-volume

docker volume ls

```

Шаг 3. Запустите контейнер с подключённым томом и создайте в нём файл:

```bash

docker run --name container-a -v my-data-volume:/data ubuntu bash -c "echo 'данные, сохранённые в томе' > /data/persistent.txt"

docker rm container-a

```

Шаг 4. Запустите другой контейнер, подключив тот же том, и проверьте, сохранились ли данные:

```bash

docker run --name container-b -v my-data-volume:/data ubuntu cat /data/persistent.txt

docker rm container-b

```

Что вы увидите: содержимое файла успешно выведено — данные пережили удаление первого контейнера, потому что они хранились не внутри файловой системы контейнера, а в независимом от него именованном томе.

[Скриншот терминала, демонстрирующий сохранность данных через том]

Шаг 5. Изучите подробную информацию о томе:

```bash

docker volume inspect my-data-volume

```

Что вы увидите: JSON-формат (структурированный текстовый формат данных, который мы, возможно, кратко видели ранее в курсе) с информацией о томе, включая точный путь на хост-машине, где физически хранятся данные (директория Mountpoint).

Шаг 6. Продемонстрируйте bind mount — свяжите конкретную директорию на хосте с директорией внутри контейнера:

```bash

mkdir -p ~/docker-bind-demo

echo "Файл, созданный на хосте" > ~/docker-bind-demo/host-file.txt

docker run --name bind-demo -v ~/docker-bind-demo:/mounted ubuntu ls -la /mounted

docker rm bind-demo

```

Что вы увидите: файл host-file.txt, созданный непосредственно на вашей хост-машине, доступен внутри контейнера благодаря bind mount.

Шаг 7. Продемонстрируйте, что изменения через bind mount работают в обе стороны — создайте файл изнутри контейнера, проверьте его наличие на хосте:

```bash

docker run --name bind-demo2 -v ~/docker-bind-demo:/mounted ubuntu bash -c "echo 'Файл, созданный внутри контейнера' > /mounted/container-file.txt"

docker rm bind-demo2

ls ~/docker-bind-demo/

cat ~/docker-bind-demo/container-file.txt

```

Что вы увидите: файл, созданный процессом внутри контейнера, реально появился на вашей хост-машине — bind mount обеспечивает полностью двустороннюю синхронизацию, поскольку это фактически одна и та же директория, просто видимая из двух разных «точек зрения» (хоста и контейнера).

[Скриншот терминала, демонстрирующий двустороннюю связь bind mount]

Шаг 8. Изучите список сетей Docker:

```bash

docker network ls

```

Что вы увидите: как минимум сети bridge (используемая по умолчанию), host, и none — созданные автоматически при установке Docker.

Шаг 9. Создайте собственную пользовательскую сеть и продемонстрируйте обращение контейнеров друг к другу по имени:

```bash

docker network create my-app-network

docker run -d --name web-service --network my-app-network nginx:alpine

docker run --rm --network my-app-network alpine ping -c 3 web-service

```

Что вы увидите: успешные ответы на ping — контейнер alpine смог обратиться к контейнеру web-service просто по его имени, благодаря общей пользовательской сети (флаг --rm, изученный в уроке 17.4, автоматически удаляет этот вспомогательный контейнер сразу после завершения команды ping).

[Скриншот успешного ping между контейнерами по имени]

Шаг 10. Изучите подробную информацию о созданной сети:

```bash

docker network inspect my-app-network

```

Что вы увидите: список контейнеров, подключённых к этой сети, их внутренние IP-адреса, и общие параметры конфигурации сети.

Шаг 11. Очистите все созданные в этом уроке ресурсы:

```bash

docker stop web-service

docker rm web-service

docker network rm my-app-network

docker volume rm my-data-volume

rm -rf ~/docker-bind-demo

```

Шаг 12. Изучите общую очистку неиспользуемых ресурсов Docker (полезная регулярная практика для поддержания системы в порядке, аналогично apt autoremove из Блока 9):

```bash

docker system df

```

(Показывает общее использование дискового пространства Docker — образами, контейнерами, томами.)

```bash

docker system prune

```

(Удаляет все остановленные контейнеры, неиспользуемые сети, и «висячие» — не привязанные ни к одному тегу — образы; запросит подтверждение перед выполнением.)

Разбор команд

Команда docker volume

Флаг -v команды docker run — два формата:

Команда docker network

Команда docker system df / docker system prune

Возможные ошибки

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

Как определить: сообщение об ошибке явно указывает на использование тома.

Как исправить: сначала удалить (docker rm) все контейнеры, использующие этот том, и только затем удалять сам том.

Почему возникает: оба варианта используют один и тот же флаг -v источник:назначение, различаясь лишь тем, начинается ли «источник» с / (абсолютный путь — bind mount) или является простым именем (именованный том).

Как избежать: внимательно проверять синтаксис перед выполнением — для bind mount всегда использовать полный, абсолютный путь (можно явно начинать с $(pwd) или ~/ для ясности), для именованного тома — простое, предварительно объявленное через docker volume create имя.

Практические задания

  1. Создайте именованный том специально для практики хранения «конфигурации» приложения — подключите его к контейнеру, создайте внутри файл конфигурации, удалите контейнер, создайте новый контейнер с тем же томом и убедитесь, что конфигурация сохранилась. Изучите вывод docker volume inspect и найдите реальный путь на диске, где физически хранятся эти данные (не открывайте и не изменяйте этот путь напрямую — только для ознакомления).
  2. Модифицируйте один из docker-compose.yml файлов, созданных в практике урока 17.6, добавив в описание сервиса директиву volumes для монтирования именованного тома (объявленного дополнительно в корневом разделе volumes: файла — эта деталь синтаксиса Compose не была подробно разобрана в теории, изучите через docker compose --help или поиск в интернете) — убедитесь, что данные, созданные внутри контейнера, сохраняются после docker compose down и последующего docker compose up.
  3. Создайте две отдельные пользовательские сети, подключите один контейнер к обеим сетям одновременно (флаг --network можно указать несколько раз, либо подключить существующий контейнер к дополнительной сети через docker network connect), и проверьте через docker network inspect обеих сетей, что этот контейнер действительно виден в обеих — опишите в конспекте практический сценарий, где могло бы понадобиться подключение одного контейнера сразу к нескольким изолированным сетям.

Проверка знаний

Вопрос 1. Почему данные, созданные внутри работающего контейнера, теряются при его удалении, и как тома решают эту проблему?

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

Вопрос 2. В чём разница между именованным томом и bind mount, и в какой ситуации предпочтительнее каждый из этих подходов?

Ответ: Именованный том полностью управляется Docker — вы не контролируете и обычно не знаете точное расположение данных на диске хоста, взаимодействуя с ним исключительно через команды Docker; это предпочтительный вариант для постоянного хранения данных приложений, особенно баз данных, поскольку такой подход более портируем и «Docker-нативен». Bind mount связывает конкретную, явно указанную вами директорию на хосте с директорией внутри контейнера — вы точно знаете и полностью контролируете путь на хосте; это особенно удобно для сценариев разработки, когда изменения исходного кода на хосте должны немедленно отражаться внутри работающего контейнера, а также для предоставления контейнеру доступа к конкретным, заранее существующим файлам конфигурации хоста.

Итоги урока

Вы изучили: проблему непостоянности данных контейнеров, три способа управления данными (именованные тома, bind mounts, tmpfs), команды управления томами, три основных сетевых драйвера Docker (bridge/host/none), пользовательские bridge-сети и обращение контейнеров друг к другу по имени, команды управления сетями и общую очистку неиспользуемых ресурсов Docker.

Вы умеете: создавать и использовать именованные тома для постоянного хранения данных, применять bind mounts для связи с конкретными директориями хоста, создавать пользовательские сети для взаимодействия контейнеров по имени, поддерживать чистоту системы через регулярную очистку неиспользуемых ресурсов Docker.

В следующем блоке потребуется: понимание работающих контейнеров и приложений внутри них — Блок 18 про мониторинг применит многие уже изученные принципы диагностики (материал Блоков 10-11) к более широкому кругу систем, потенциально включая и контейнеризованные приложения.

Мини-проект блока

Задача: собрать полноценное, реалистичное многоконтейнерное приложение, применяющее весь материал этого блока — собственный образ через Dockerfile, оркестрацию через Docker Compose, постоянное хранение данных через тома, и сетевое взаимодействие между сервисами.

Что нужно сделать:

  1. Создайте директорию проекта ~/docker-final-project со следующей структурой: поддиректория web/ с собственным Dockerfile и статическим HTML-содержимым (используя материал урока 17.5).
  1. Опишите как минимум два сервиса в docker-compose.yml:
  1. Настройте именованный том для сервиса web, монтирующий директорию для «логов» (даже если реального логирования там не происходит — цель практики в самой настройке тома) — убедитесь, что данные в этом томе переживают docker compose down и последующий docker compose up.
  1. Настройте пользовательскую сеть (либо используйте автоматически создаваемую Docker Compose сеть — но явно опишите её в файле через раздел networks:, что является более продвинутым и контролируемым подходом) и убедитесь, что оба сервиса находятся в общей сети и потенциально могут обращаться друг к другу по имени (продемонстрируйте это, например, через docker compose exec logger ping -c 3 web, если ping доступен в используемом базовом образе — иначе можно продемонстрировать через docker network inspect).
  1. Запустите весь проект через docker compose up -d --build, убедитесь через curl, что веб-страница доступна, и через docker compose logs logger, что второй сервис действительно работает и производит периодический вывод.
  1. Задокументируйте проект в файле README.md в корне директории проекта (применение материала Блока 16, если вы решите также инициализировать этот проект как Git-репозиторий и опубликовать на GitHub, что было бы отличной дополнительной практикой, объединяющей материал двух последних блоков курса) — опишите структуру проекта, назначение каждого сервиса, и инструкции по запуску.

Критерий готовности: у вас есть работающее, полноценно задокументированное многоконтейнерное приложение, запускаемое единой командой docker compose up, с как минимум одним собственным образом (не просто готовым с Docker Hub), постоянным хранилищем данных через том, и настроенным сетевым взаимодействием между сервисами.

Контрольные вопросы

  1. Объясните фундаментальную разницу между виртуальными машинами и контейнерами с точки зрения того, что каждая из этих технологий изолирует и виртуализирует.
  2. Что такое namespaces и cgroups в ядре Linux, и какую роль каждый из этих механизмов играет в реализации контейнеров?
  3. В чём разница между Docker-образом и Docker-контейнером?
  4. Перечислите минимум четыре важные опции команды docker run и объясните назначение каждой.
  5. Объясните разницу между инструкциями RUN и CMD в Dockerfile.
  6. Почему рекомендуется размещать редко изменяющиеся инструкции Dockerfile (например, установку зависимостей) раньше часто изменяющихся (например, копирование кода приложения)?
  7. Какую проблему решает Docker Compose по сравнению с использованием отдельных команд docker run для каждого контейнера?
  8. Объясните разницу между именованным томом и bind mount, и приведите пример ситуации, где каждый из этих подходов был бы предпочтителен.
  9. Почему данные, созданные внутри контейнера без подключённого тома, теряются при его удалении?

Выводы по блоку

Контейнеризация — это одна из тех технологий, которые за последнее десятилетие фундаментально изменили то, как разрабатывается, упаковывается и разворачивается программное обеспечение. Вы прошли путь от понимания базовых механизмов ядра Linux, делающих контейнеры возможными (namespaces, cgroups), через практическое освоение Docker — от простого запуска готовых образов до создания собственных через Dockerfile, оркестрации многоконтейнерных приложений через Docker Compose, и решения практических задач постоянного хранения данных и сетевого взаимодействия.

Важно осознавать, что этот блок дал вам прочный практический фундамент, но экосистема контейнеризации значительно шире — существуют продвинутые темы вроде оркестрации на масштабе множества серверов (Kubernetes — отдельная, весьма обширная технология, построенная на тех же базовых принципах контейнеров, но решающая задачи управления контейнерами в масштабах целых кластеров серверов), более глубокие аспекты безопасности контейнеров, продвинутые техники сборки образов (многоступенчатая сборка, которую мы упомянули лишь кратко). Материал этого блока — прочная основа для дальнейшего самостоятельного погружения в эти темы, если ваш профессиональный путь приведёт вас в этом направлении.

Материал этого блока пригодится и в оставшейся части курса: принципы диагностики процессов и ресурсов (Блоки 10-11), применённые здесь к контейнерам, будут расширены в Блоке 18 (мониторинг) уже в более общем контексте; в Блоке 19 мы познакомимся с Nginx и базами данных «напрямую» на хосте, но вы легко сможете провести параллель с тем, как эти же самые технологии часто разворачиваются именно в контейнерах на практике; а в Блоке 20 (серверное администрирование) контейнеризация нередко оказывается частью общей стратегии развёртывания и обслуживания реальных производственных серверов.

Список изученных тем

Рекомендации перед переходом

Убедитесь, что вы умеете:

В Блоке 18 мы обратимся к теме мониторинга, журналирования и диагностики — темам, тесно связанным со всем, что мы изучали о процессах (Блок 11), службах (Блок 10), и теперь контейнерах (этот блок). Вы научитесь глубже анализировать состояние системы, искать причины проблем производительности, и познакомитесь с обзором популярных инструментов мониторинга, используемых в реальной практике администрирования.