Практический Docker: от управления контейнерами до оркестрации микросервисов

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

1. Основы Docker: жизненный цикл контейнеров и базовое администрирование

Основы Docker: жизненный цикл контейнеров и базовое администрирование

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

Архитектура изоляции и первый запуск

Прежде чем вводить команды, важно понять, что именно происходит в системе, когда мы говорим «контейнер». В отличие от виртуальных машин, которые эмулируют аппаратное обеспечение и запускают внутри себя полноценную гостевую операционную систему, контейнеры Docker разделяют ядро основной ОС (Host OS). Это делает их невероятно легкими: запуск занимает доли секунды, а потребление оперативной памяти минимально.

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

docker version

Она выводит информацию не только о версии клиента, но и о состоянии Docker Engine (серверной части). Если вы видите данные в обоих разделах (Client и Server), значит, демон Docker запущен и готов принимать команды.

Самый простой способ проверить работоспособность — запустить классический тестовый образ:

docker run hello-world

В этот момент происходит целая цепочка событий. Docker ищет образ hello-world локально. Не найдя его, он обращается к Docker Hub (публичному реестру), скачивает слои образа, создает на их основе контейнер и запускает процесс внутри него. Этот процесс выводит текст в консоль и завершается. Контейнер переходит в статус «Exited», но он не исчезает из системы бесследно.

Управление жизненным циклом: от создания до удаления

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

docker ps

По умолчанию она показывает только запущенные контейнеры. Однако, как мы выяснили на примере с hello-world, многие контейнеры могут быть остановлены, но при этом они продолжают занимать место и хранить свое состояние. Чтобы увидеть «полную картину», включая завершенные процессы, добавьте флаг -a (all):

docker ps -a

В выводе этой команды вы заметите столбец STATUS. Контейнер может находиться в состояниях Created, Up (запущен), Paused, Exited или Dead. Понимание этих статусов критично для администрирования. Например, если ваше приложение упало с ошибкой, оно перейдет в Exited, и вам нужно будет выяснить причину.

Чтение логов и диагностика

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

docker logs <container_id_or_name>

Если приложение генерирует много данных, удобно использовать флаг -f (follow), который позволяет следить за логами в реальном времени, подобно команде tail -f в Linux. Это незаменимо при отладке сетевых соединений или проверке того, поднялась ли база данных.

Принудительная остановка и очистка

Иногда процессы внутри контейнера зависают или ведут себя некорректно. Команда docker stop посылает процессу сигнал SIGTERM, давая ему время (обычно 10 секунд) на корректное завершение работы (сохранение данных, закрытие соединений). Если же приложение не реагирует, применяется «грубая сила»:

docker kill <container_id>

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

docker container prune

Она удалит все контейнеры со статусом Exited. Будьте осторожны: если в остановленном контейнере были важные данные, которые не были вынесены в тома (volumes), они будут безвозвратно утеряны.

Интерактивный режим и проброс портов

Запуск hello-world полезен для теста, но в реальной разработке нам нужно взаимодействовать с контейнером. Допустим, мы хотим запустить веб-сервер Nginx. Если мы просто напишем docker run nginx, контейнер заберет на себя управление терминалом, и мы будем видеть логи сервера. Чтобы запустить его «в фоне», используется флаг -d (detached):

docker run -d nginx

Теперь сервер работает, но мы не можем на него зайти через браузер. Почему? Потому что контейнер живет в своей изолированной сети. Чтобы связать порт внутри контейнера с портом на вашем физическом компьютере, используется флаг -p (publish):

docker run -d -p 8080:80 nginx

Здесь мы говорим: «Весь трафик, приходящий на порт 8080 моего компьютера, перенаправляй на порт 80 внутри контейнера». Теперь, открыв localhost:8080, вы увидите приветственную страницу Nginx.

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

docker exec -it <container_name> /bin/bash

Флаги -i (interactive) и -t (tty) создают полноценную терминальную сессию. Вы оказываетесь внутри изолированной файловой системы контейнера. Важно помнить: любые изменения, внесенные вами вручную через exec (например, установка пакета через apt-get), исчезнут, как только контейнер будет удален и пересоздан из образа. Контейнеры должны быть эфемерными.

Работа с образами: фундамент контейнеризации

Образ (Image) — это неизменяемый шаблон, из которого создается контейнер. Если провести аналогию с программированием, то образ — это класс, а контейнер — экземпляр этого класса.

Для просмотра списка всех скачанных или собранных образов используйте:

docker image ls

Вы увидите колонки REPOSITORY (имя), TAG (версия, например latest или 18-bookworm) и IMAGE ID. Чтобы создать собственный образ, используется файл Dockerfile и команда сборки.

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

docker build . -t my-app:v1

Здесь . указывает на текущую директорию как контекст сборки, а -t (tag) задает человекочитаемое имя и версию. В процессе сборки Docker выполняет команды из Dockerfile послойно. Каждый слой кэшируется, что позволяет собирать обновленные версии образа за считанные секунды, если изменения коснулись только кода, а не системных зависимостей.

Персистентность данных и переменные окружения

Контейнеры по своей природе не сохраняют данные после удаления. Если вы запустите базу данных PostgreSQL, запишете в нее миллион строк, а затем удалите контейнер командой docker rm, ваши данные исчезнут. Для решения этой проблемы используются тома (volumes) и монтирование (mounts).

Рассмотрим запуск PostgreSQL версии 18-bookworm. Для работы базы данных нам обязательно нужно передать ей пароль администратора через переменную окружения (флаг -e):

docker run -d --name my-db -e POSTGRES_PASSWORD=secret_pass -v my_db_data:/var/lib/postgresql/data postgres:18-bookworm

Разберем флаг -v (или --volume):

  • my_db_data — это имя тома в хранилище Docker.
  • /var/lib/postgresql/data — путь внутри контейнера, где PostgreSQL хранит свои файлы.
  • Теперь, даже если вы удалите контейнер my-db, данные останутся в томе my_db_data. При запуске нового контейнера с тем же флагом -v, база данных «подхватит» все старые записи.

    От одиночных контейнеров к Docker Compose

    Запуск через docker run удобен для простых задач, но современные приложения состоят из множества сервисов: фронтенд, бэкенд, база данных, кэш (Redis). Писать огромные bash-скрипты с десятками флагов -p, -v и -e — путь к ошибкам.

    Здесь на сцену выходит Docker Compose. Это инструмент, который позволяет описать всю вашу инфраструктуру в одном YAML-файле. Вместо длинной команды в терминале вы создаете файл docker-compose.yml:

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

    * docker compose up — читает файл, скачивает образы, создает сети и запускает все сервисы. * docker compose up -d — запускает всю связку в фоновом режиме. * docker compose down — останавливает и полностью удаляет контейнеры и сети, созданные для этого проекта (но сохраняет тома, если они описаны как внешние или не указан флаг удалений волюмов).

    Разница между docker run и docker compose фундаментальна: первый управляет отдельными «кирпичиками», второй — всем «зданием» целиком. Compose автоматически создает внутреннюю сеть, где сервисы могут обращаться друг к другу по именам (например, бэкенд может подключиться к базе по хосту db, а не по IP-адресу).

    Практические сценарии и администрирование

    В повседневной работе администратора или разработчика важно уметь быстро диагностировать состояние системы. Если docker ps показывает, что контейнер постоянно перезапускается (статус Restarting), первым делом нужно проверить логи. Часто ошибка кроется в неправильно переданных переменных окружения или конфликте портов (если порт 8080 уже занят другим приложением на хосте).

    Еще один нюанс — использование тегов образов. Никогда не используйте тег latest в продакшене. Это «плавающий» тег, который сегодня указывает на одну версию, а завтра — на другую. Всегда фиксируйте версии, например postgres:18-bookworm, чтобы гарантировать воспроизводимость среды.

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

    2. Сборка кастомных образов: логика Dockerfile и управление слоями

    Сборка кастомных образов: логика Dockerfile и управление слоями

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

    Анатомия Dockerfile: от декларации к реальности

    Dockerfile — это текстовый документ, содержащий все команды, которые пользователь мог бы вызвать в командной строке для создания образа. Docker читает этот файл сверху вниз, выполняя инструкции одну за другой. Однако за этой простотой скрывается сложная логика слоистой файловой системы (Union File System).

    Каждая значимая инструкция в Dockerfile создает новый «слой». Слой — это дельта, разница между текущим и предыдущим состоянием файловой системы. Когда вы запускаете сборку командой docker build . -t my-app:v1, Docker Engine интерпретирует ваш файл и создает цепочку неизменяемых слоев.

    Рассмотрим базовую структуру типичного Dockerfile для приложения на Python:

    В этом примере каждая строка выполняет конкретную роль. Инструкция FROM всегда идет первой. Она определяет «фундамент». Если вы укажете FROM scratch, вы начнете с абсолютно пустой файловой системы, что часто используется для бинарных файлов на Go или Rust. В большинстве же случаев используются официальные образы из Docker Hub, такие как python, node, ubuntu или alpine.

    Механизм кэширования и иерархия слоев

    Ключевой навык профессионала — понимание того, как Docker использует кэш. При повторной сборке Docker проверяет, изменилась ли инструкция и файлы, на которые она ссылается. Если изменений нет, Docker берет готовый слой из кэша вместо того, чтобы выполнять команду заново.

    Это критически важно для скорости разработки. Посмотрите на порядок команд в примере выше. Почему мы сначала копируем requirements.txt и запускаем pip install, а только потом копируем весь остальной код (COPY . .)?

    Если бы мы сначала скопировали весь код, то любое изменение в одном комментарии вашего Python-скрипта привело бы к тому, что Docker счел бы слой COPY . . изменившимся. Поскольку кэш работает по принципу цепочки, инвалидация (аннулирование) одного слоя автоматически инвалидирует все последующие слои. В итоге Docker пришлось бы заново скачивать и устанавливать все библиотеки через pip install, даже если список зависимостей не менялся.

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

    Глубокое погружение в инструкции: RUN, CMD и ENTRYPOINT

    Часто возникает путаница между командами, которые выполняются в момент сборки, и теми, что срабатывают при запуске контейнера.

    Инструкция RUN

    RUN выполняется в процессе сборки образа. Она создает новый слой. Обычно её используют для установки софта, создания пользователей или настройки прав доступа. Важно помнить о «мусоре»: если вы скачали архив, распаковали его и удалили исходный файл в разных инструкциях RUN, размер образа не уменьшится. Удаленный файл останется в предыдущем слое. Чтобы образ был легким, объединяйте команды через оператор &&:

    CMD vs ENTRYPOINT

    Эти инструкции определяют, что будет делать контейнер после запуска.
  • CMD — это аргументы по умолчанию. Их легко переопределить при запуске. Если вы напишете docker run my-image bash, команда bash заменит всё, что указано в CMD.
  • ENTRYPOINT — делает контейнер похожим на исполняемый файл. Его сложнее переопределить (нужен флаг --entrypoint).
  • В профессиональной среде их часто комбинируют. ENTRYPOINT задает основную команду (например, python), а CMD — скрипт по умолчанию (main.py). Это позволяет пользователю запускать другой скрипт, просто передав его имя в конце команды docker run, не меняя саму программу запуска.

    Оптимизация размера: выбор базового образа и .dockerignore

    Размер образа напрямую влияет на скорость деплоя и затраты на хранение. Использование образа ubuntu:latest в качестве базы может добавить 70-100 МБ веса еще до того, как вы добавите свой код.

    Существует три основных стратегии выбора базы:

  • Full-images (стандартные): Содержат полный набор инструментов. Хороши для отладки, но тяжелы.
  • Slim-версии: Например, python:3.11-slim. В них удалены исходники документации и редкие утилиты, но сохранена совместимость с большинством библиотек.
  • Alpine Linux: Образы на базе alpine весят всего около 5 МБ. Они используют библиотеку musl вместо стандартной glibc. Это может вызвать проблемы с компиляцией некоторых C-зависимостей (например, в pandas или numpy), поэтому переход на Alpine требует тщательного тестирования.
  • Еще один инструмент оптимизации — файл .dockerignore. Он работает аналогично .gitignore. При выполнении COPY . . Docker сначала отправляет весь контекст сборки (файлы из текущей папки) демону Docker. Если у вас в папке лежит папка .git на 500 МБ или тяжелые логи, сборка будет долгой, а образ — раздутым. Добавьте их в .dockerignore, чтобы исключить из процесса.

    Многоэтапная сборка (Multi-stage builds)

    Это «высший пилотаж» в написании Dockerfile. Представьте, что для компиляции вашего приложения на Java или Go нужен тяжелый JDK или компилятор, но для работы готового файла достаточно маленькой JRE или вообще пустой системы.

    Multi-stage позволяет использовать несколько инструкций FROM в одном файле.

    В результате в итоговый образ попадет только один бинарный файл весом в пару десятков мегабайт, а все исходники и инструменты компиляции (весом в сотни МБ) будут отброшены после завершения сборки. Это не только экономит место, но и повышает безопасность: в вашем продакшн-контейнере нет компиляторов и лишнего софта, который мог бы использовать злоумышленник.

    Управление версиями через тегирование

    Когда вы запускаете docker build -t my-app ., Docker автоматически присваивает образу тег latest. В профессиональной разработке полагаться на latest — плохая практика. Это «плавающий» тег: сегодня это версия 1.0, завтра — 2.0, и вы никогда не знаете точно, какой код запущен в контейнере.

    Используйте семантическое версионирование или хэши коммитов из Git: docker build -t my-app:1.0.5 . docker build -t my-app:sha-a1b2c3d .

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

    Переменные сборки ARG и переменные окружения ENV

    Часто нужно передать какие-то параметры в процессе сборки (например, версию библиотеки) или настроить поведение приложения внутри контейнера.

  • ARG (Build Arguments) доступны только во время сборки. Вы можете передать их через флаг --build-arg VERSION=1.2. В самом образе после завершения сборки их не останется.
  • ENV (Environment Variables) сохраняются в образе и доступны запущенному приложению. Их можно переопределить при запуске контейнера через флаг -e.
  • Пример использования:

    Важно помнить: значения ENV видны всем, кто имеет доступ к образу (через docker inspect). Никогда не зашивайте в ENV внутри Dockerfile секретные ключи, пароли от баз данных или API-токены. Для этого существуют механизмы Secrets в Docker Compose или внешние хранилища конфигураций, которые мы обсудим позже.

    Безопасность и права доступа

    По умолчанию процессы в контейнере запускаются от имени пользователя root. Это удобно для установки пакетов, но опасно в эксплуатации. Если злоумышленник взломает ваше приложение и «вырвется» из контейнера, он может получить права суперпользователя на хост-машине.

    Хорошим тоном считается создание несистемного пользователя в конце Dockerfile:

    После инструкции USER все последующие команды (включая CMD) будут выполняться от имени этого пользователя. Убедитесь, что вы предварительно дали этому пользователю права на чтение и запись в рабочую директорию вашего приложения.

    Логика слоев, грамотное кэширование и многоэтапная сборка превращают Docker из простого инструмента упаковки в мощный механизм доставки софта. Понимая, как каждый байт попадает в итоговый образ, вы создаете не просто «работающие», а быстрые, безопасные и предсказуемые системы.