1. Фундамент контейнеризации: работа с Docker и Docker Compose для начинающих
Фундамент контейнеризации: работа с Docker и Docker Compose для начинающих
До 2013 года развертывание любого серверного приложения напоминало сборку карточного домика на палубе корабля. Разработчик писал код на своей машине с определенной версией операционной системы, конкретными версиями библиотек и системных утилит. При переносе этого кода на боевой сервер выяснялось, что там установлена другая версия языка программирования, конфликтующие зависимости или иные настройки сети. Приложение падало с ошибками, порождая классическую фразу: «На моем компьютере всё работает». Решение этой проблемы привело к массовому переходу индустрии на контейнеризацию.
Контейнеризация меняет саму парадигму доставки программного обеспечения. Вместо того чтобы переносить код и пытаться адаптировать сервер под него, мы упаковываем код, все его зависимости, библиотеки и даже базовые системные утилиты в единый, стандартизированный блок — контейнер. Этот блок будет работать абсолютно идентично на ноутбуке разработчика, на тестовом сервере и в мощном облачном кластере.
Механика изоляции: почему контейнер — не виртуальная машина
Часто контейнеры путают с виртуальными машинами (VM), так как обе технологии решают задачу изоляции приложений. Однако архитектурно это принципиально разные подходы.
Виртуальная машина эмулирует аппаратное обеспечение. Для запуска приложения в VM требуется гипервизор (программа, управляющая виртуализацией), поверх которого устанавливается полноценная гостевая операционная система (Guest OS) со своим собственным ядром. Если вам нужно запустить три изолированных приложения, вы запустите три виртуальные машины, каждая из которых будет потреблять гигабайты оперативной памяти и ресурсы процессора просто на поддержание работы своих ОС.
Docker использует другой подход, опираясь на механизмы ядра Linux: namespaces (пространства имен) и cgroups (контрольные группы).
Главное отличие: все контейнеры на одном сервере делят одно общее ядро хостовой операционной системы. Внутри контейнера нет своей ОС, есть лишь необходимые для приложения файлы и библиотеки.
| Характеристика | Виртуальная машина (VM) | Контейнер Docker | | :--- | :--- | :--- | | Архитектура | Эмуляция железа + полноценная гостевая ОС | Изоляция процессов на уровне ядра хостовой ОС | | Размер | Гигабайты (включает ядро и все системные службы) | Мегабайты (только приложение и его зависимости) | | Время старта | Минуты (загрузка ОС) | Миллисекунды (запуск обычного процесса) | | Изоляция | Максимальная (аппаратный уровень) | Высокая (уровень операционной системы) |
Три кита Docker: Dockerfile, Image и Container
Чтобы уверенно управлять инфраструктурой, необходимо четко различать три базовых понятия, формирующих жизненный цикл любого приложения в Docker.
!Жизненный цикл: от Dockerfile к запущенному контейнеру
Важнейшее свойство контейнера — его эфемерность. Контейнер спроектирован так, чтобы его можно было в любой момент остановить, удалить и пересоздать заново из образа. Любые изменения файловой системы, которые происходят внутри запущенного контейнера (например, запись логов или сохранение загруженных файлов), по умолчанию записываются во временный слой (read-write layer). Если контейнер удалить, этот слой исчезнет навсегда.
Сохранение состояния: Тома (Volumes)
Поскольку мы готовимся разворачивать систему общения (Rocket.Chat) и базу данных (MongoDB), эфемерность контейнеров становится проблемой. Нам нужно, чтобы история переписки и учетные записи пользователей сохранялись даже при полном удалении и обновлении контейнера с базой данных.
Для этого в Docker существует механизм монтирования данных. Выделяют два основных типа:
/opt/rocket/data) с папкой внутри контейнера (например, /data/db). Любой файл, созданный контейнером в /data/db, физически появляется на вашем сервере в /opt/rocket/data. Это удобно для конфигурационных файлов, которые вы хотите редактировать прямо на хосте.mongo_data и подключи его к /data/db в контейнере». Docker сам решает, где физически на диске хранить эти файлы (обычно в /var/lib/docker/volumes/). Это предпочтительный способ для баз данных, так как он исключает проблемы с правами доступа (permissions), которые часто возникают при использовании Bind Mounts.Сетевое взаимодействие: Проброс портов
По умолчанию контейнер полностью изолирован от внешней сети. Если внутри контейнера запущен веб-сервер на порту 80, вы не сможете получить к нему доступ, просто введя IP-адрес сервера в браузер.
Чтобы пустить трафик внутрь, используется механизм проброса портов (Port Mapping). При запуске контейнера создается правило трансляции: запрос, пришедший на определенный порт хост-сервера, перенаправляется на определенный порт внутри контейнера.
Синтаксис всегда строится по принципу ХОСТ:КОНТЕЙНЕР. Если вы видите правило 8080:80, это означает: «Слушай порт 8080 на Ubuntu-сервере и перенаправляй весь трафик на порт 80 внутрь этого конкретного контейнера». Внутренний порт определяется тем, как настроено само приложение (Nginx обычно слушает 80, MongoDB — 27017, Rocket.Chat — 3000). Внешний порт вы выбираете сами, исходя из свободных портов на вашем сервере.
Переход к оркестрации: зачем нужен Docker Compose
Чистый Docker отлично работает, когда нужно запустить один изолированный сервис. Но современные системы состоят из множества компонентов. Например, для работы Rocket.Chat требуется:
Запускать всё это вручную через команды docker run в терминале — неэффективно. Придется прописывать длинные команды с десятками флагов для портов, томов, переменных окружения, а главное — вручную настраивать сеть между этими контейнерами, чтобы Rocket.Chat «увидел» MongoDB. Малейшая опечатка приведет к неработоспособности системы.
Здесь на сцену выходит Docker Compose — инструмент для декларативного описания и запуска многоконтейнерных приложений.
Вместо того чтобы вводить команды в консоль (императивный подход: «сделай это, потом сделай то»), вы создаете один конфигурационный файл docker-compose.yml (декларативный подход: «я хочу, чтобы система выглядела вот так»). В этом файле описываются все сервисы, их связи, настройки сети и тома. Затем одной командой вы поднимаете всю инфраструктуру.
Анатомия файла docker-compose.yml
Файл пишется на языке разметки YAML. Главное правило YAML — строгая иерархия, которая задается отступами (пробелами, использование табуляции запрещено). Ошибка в один пробел сделает файл невалидным.
Разберем структуру на примере связки абстрактного веб-приложения и базы данных:
Разберем ключевые блоки:
services: — корневой раздел, внутри которого мы объявляем наши контейнеры. В данном случае их два: database и webapp. Имена сервисов вы придумываете сами.image: — указывает, какой образ скачать из реестра. Формат обычно имя:тег. Тег указывает на конкретную версию (например, 6.0). Использование тега :latest (последняя версия) удобно для тестов, но в production-среде считается плохой практикой, так как при следующем перезапуске может скачаться мажорное обновление, ломающее совместимость.restart: always — политика перезапуска. Критически важная настройка для отказоустойчивости. Если процесс внутри контейнера упадет из-за ошибки, или если сам сервер Ubuntu перезагрузится по питанию, демон Docker автоматически запустит этот контейнер снова.environment: — переменные окружения. Это способ передать настройки внутрь контейнера при его старте. Именно так мы задаем пароли для баз данных или указываем приложению, по какому адресу искать базу.depends_on: — управляет порядком запуска. В нашем примере webapp начнет запускаться только после того, как стартует database.volumes: (в самом низу файла) — здесь мы объявляем именованный том db_data, который затем используем внутри сервиса database.Внутренняя сеть и встроенный DNS
Один из самых мощных механизмов Docker Compose — автоматическое создание изолированной виртуальной сети (bridge network) для всех сервисов, описанных в одном docker-compose.yml.
Вам не нужно знать IP-адреса контейнеров. Более того, при каждом пересоздании контейнера его внутренний IP-адрес может меняться. Как же тогда webapp подключается к database?
Docker Compose включает встроенный DNS-сервер. Имена сервисов (те самые database и webapp, которые мы задали в YAML) автоматически становятся доменными именами внутри этой сети.
!Схема внутреннего DNS-резолвинга в Docker Compose
Когда код приложения webapp пытается подключиться к хосту с именем database, внутренний DNS Docker мгновенно переводит это имя в текущий IP-адрес контейнера с MongoDB. В конфигурации приложения достаточно указать строку подключения вида mongodb://admin:securepassword123@database:27017. Это делает конфигурацию абсолютно переносимой: она будет работать на любом сервере без изменений.
Важный нюанс: эта внутренняя сеть полностью изолирована от внешнего мира. Сервис database из нашего примера не имеет блока ports, а значит, к базе данных невозможно подключиться из интернета. К ней имеет доступ только webapp, находящийся в той же внутренней сети. Это формирует надежный периметр безопасности: наружу торчит только веб-интерфейс, а критичные данные надежно спрятаны во внутреннем контуре.
Управление жизненным циклом через Compose
Работа с docker-compose.yml сводится к нескольким базовым командам, которые выполняются в директории, где лежит этот файл:
docker-compose up -d — читает файл, скачивает нужные образы (если их нет на сервере), создает сети, тома и запускает контейнеры. Флаг -d (detached) означает запуск в фоновом режиме, чтобы терминал оставался свободным.docker-compose down — останавливает и удаляет контейнеры, а также внутреннюю сеть. Важно: эта команда по умолчанию НЕ удаляет именованные тома (volumes). Ваши базы данных останутся в сохранности. Если вы снова сделаете up -d, новые контейнеры подхватят старые данные.docker-compose ps — показывает статус сервисов из текущего файла (работают, перезапускаются или остановлены).docker-compose logs -f — выводит объединенный поток логов от всех сервисов в реальном времени. Если система ведет себя странно, это первое место, куда нужно смотреть. Можно указать имя конкретного сервиса, например docker-compose logs -f webapp, чтобы не отвлекаться на логи базы данных.Если вы внесли изменения в docker-compose.yml (например, изменили проброс порта или добавили новую переменную окружения), вам не нужно вручную останавливать систему. Достаточно снова выполнить docker-compose up -d. Docker проанализирует изменения и пересоздаст только те контейнеры, конфигурация которых поменялась, оставив остальные работать без прерывания обслуживания.
Понимание того, как эфемерные контейнеры взаимодействуют с постоянными томами, и как Compose связывает их в единую сеть через встроенный DNS, формирует тот самый фундамент. Переход от ручного администрирования к декларативному описанию инфраструктуры в YAML-файле позволяет сделать развертывание сложных систем предсказуемым, безопасным и легко воспроизводимым на любом оборудовании.