Docker для разработчиков: от основ контейнеризации до оркестрации приложений

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

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

Основы контейнеризации и базовое управление жизненным циклом контейнеров

В 2013 году на конференции PyCon разработчик Соломон Хайкс представил инструмент, который навсегда изменил способ доставки программного обеспечения. Проблема «на моей машине всё работает» годами преследовала индустрию: различия в версиях библиотек, операционных систем и конфигураций серверов приводили к тому, что код, успешно прошедший тесты у программиста, «падал» при попытке запуска в продакшене. Docker предложил элегантное решение — упаковывать приложение вместе со всем его окружением в изолированный стандартный блок.

Почему контейнеры — это не виртуальные машины

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

Традиционная виртуальная машина (ВМ) включает в себя полную копию операционной системы, приложение, необходимые библиотеки и драйверы. Гипервизор (например, VMware или VirtualBox) разделяет физические ресурсы сервера между несколькими ВМ. Каждая такая машина требует гигабайты дискового пространства и значительную часть оперативной памяти только для поддержания работы собственной гостевой ОС.

Контейнеры работают иначе. Они используют ядро хостовой операционной системы (Host OS) и изолируют процессы друг от друга на уровне операционной системы.

> Контейнеризация — это метод виртуализации на уровне ОС, позволяющий запускать несколько изолированных систем (контейнеров) на одном узле, используя единое общее ядро.

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

Архитектура Docker и первые шаги

Прежде чем переходить к практике, необходимо убедиться, что инструмент установлен корректно. Docker работает по клиент-серверной архитектуре. Когда вы вводите команду в терминале, вы используете Docker Client. Он отправляет запрос к Docker Daemon (службе dockerd), которая и выполняет всю тяжелую работу: сборку образов, запуск и остановку контейнеров.

Для проверки работоспособности системы используется команда: docker version

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

Первый запуск в мире Docker традиционно начинается с образа hello-world: docker run hello-world

Что происходит в этот момент «под капотом»?

  • Docker-клиент обращается к демону.
  • Демон ищет образ hello-world в локальном хранилище вашего компьютера.
  • Не найдя его локально (так как это первый запуск), демон скачивает (делает pull) образ из Docker Hub — публичного реестра образов.
  • На основе этого образа создается контейнер.
  • Контейнер запускает внутри себя программу, которая выводит приветственное сообщение в терминал и тут же завершает свою работу.
  • Образы и контейнеры: фундаментальное различие

    Для глубокого понимания Docker крайне важно различать два понятия: Image (Образ) и Container (Контейнер).

    Образ — это неизменяемый шаблон, «чертеж» или слепок файловой системы. Он содержит исполняемый код приложения, системные библиотеки, настройки и переменные окружения. Образы состоят из слоев, наложенных друг на друга. Вы не можете изменить запущенный образ — он доступен только для чтения.

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

    Для просмотра списка доступных на вашем компьютере образов используйте: docker image ls

    Эта команда покажет вам название репозитория, тег (версию), уникальный ID образа, дату создания и объем, который он занимает на диске.

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

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

    Просмотр активных процессов

    Когда вы запустили hello-world, он выполнил задачу и закрылся. Если вы введете: docker ps вы увидите пустую таблицу (или список только тех контейнеров, которые работают прямо сейчас). Чтобы увидеть вообще все контейнеры, когда-либо запущенные в системе и еще не удаленные, используйте флаг -a (all): docker ps -a

    В выводе этой команды обратите внимание на колонку STATUS. Там может быть написано Up (работает) или Exited (завершен).

    Чтение логов

    Часто приложения в контейнерах работают некорректно или мы просто хотим увидеть результат их деятельности после того, как они завершились. Для этого используется команда: docker logs <container_id_или_имя>

    Вам не обязательно вводить длинный ID контейнера целиком — Docker достаточно первых 3-4 уникальных символов. Логи — это стандартный вывод (stdout) и поток ошибок (stderr) процесса, запущенного внутри контейнера.

    Остановка и принудительное завершение

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

    Иногда нам нужно попасть «внутрь» контейнера, чтобы проверить состояние файлов или выполнить отладочные команды. Для этого при запуске используются флаги -i (interactive) и -t (tty). Обычно они объединяются в -it.

    Например, запуск контейнера с ОС Ubuntu и немедленный вход в его консоль: docker run -it ubuntu bash

    Теперь ваш терминал — это терминал внутри изолированной системы Ubuntu. Вы можете устанавливать пакеты, создавать файлы, но как только вы выйдете (exit), контейнер остановится. Если же контейнер уже запущен в фоновом режиме, и вы хотите подключиться к нему, используется команда exec: docker exec -it <name> bash

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

    Гигиена системы и очистка

    Docker очень быстро «съедает» дисковое пространство. Каждый запущенный тест, каждый скачанный образ и каждый остановленный контейнер оставляют след. Остановленные контейнеры не удаляются автоматически — они продолжают хранить свой записываемый слой на диске, ожидая, что вы захотите их снова запустить.

    Для удаления конкретного контейнера используется: docker rm <container_id>

    Для удаления образа: docker rmi <image_id> (или docker image rm)

    Однако вручную удалять десятки контейнеров неудобно. Для массовой очистки существует мощная команда prune. Например, чтобы удалить все остановленные контейнеры одним махом: docker container prune

    Будьте осторожны: эта команда не спрашивает подтверждения для каждого объекта, она просто стирает всё, что в данный момент не активно.

    Типичные ошибки новичков

    Одной из самых частых проблем является путаница с именами образов и тегами. По умолчанию Docker ищет тег latest. Если вы пытаетесь запустить my-app, а в системе есть только my-app:v1, Docker попытается скачать my-app:latest из интернета, не найдет его и выдаст ошибку manifest for my-app:latest not found. Всегда проверяйте соответствие имен и тегов через docker image ls.

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

    Третий нюанс касается фонового режима. Разработчики часто запускают веб-серверы или базы данных. Если вы запустите их просто через docker run, ваш терминал будет заблокирован выводом логов этого сервера. Чтобы запустить контейнер «в фоне», используйте флаг -d (detached): docker run -d nginx Docker выведет полный ID запущенного контейнера и вернет вам управление консолью.

    Контейнеризация — это не просто удобная обертка, это смена парадигмы. Мы перестаем думать о сервере как о «питомце», которого нужно лелеять и настраивать годами. Сервер и окружение становятся «скотом»: если что-то сломалось, мы просто удаляем контейнер и запускаем новый из того же образа. Это обеспечивает идентичность окружения на этапе разработки, тестирования и эксплуатации.

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

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

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

    Сборка собственного образа — это переход от потребления готовых решений (вроде стандартных образов Ubuntu или Nginx) к созданию кастомных инструментов, заточенных под конкретную бизнес-логику.

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

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

    Рассмотрим базовый синтаксис на примере абстрактного веб-приложения. Каждая команда начинается с инструкции в верхнем регистре.

    Инструкция FROM: фундамент образа

    Любой Dockerfile обязан начинаться с инструкции FROM. Она указывает, какой образ будет взят за основу. Это может быть «чистая» операционная система (например, debian или alpine) или уже подготовленная среда исполнения (node, python, openjdk).

    Выбор базового образа напрямую влияет на безопасность и вес системы. Использование полных дистрибутивов вроде ubuntu:latest может добавить в образ сотни мегабайт ненужных утилит (текстовые редакторы, сетевые инструменты), что увеличивает «поверхность атаки». Профессиональным стандартом считается использование легковесных версий, таких как -slim (минимизированный Debian) или -alpine (минималистичный дистрибутив на базе библиотеки musl libc).

    Инструкция WORKDIR: порядок в файловой системе

    Вместо того чтобы использовать cd внутри команд RUN, Docker предлагает инструкцию WORKDIR. Она создает указанную директорию, если её нет, и делает её текущей для всех последующих команд (COPY, RUN, CMD). Использование абсолютных путей (например, /app или /usr/src/app) — лучший способ избежать путаницы с относительными путями внутри контейнера.

    RUN против CMD и ENTRYPOINT

    Это одна из самых частых зон риска для новичков. * RUN выполняется на этапе сборки (build time). Она используется для установки пакетов, компиляции кода или создания пользователей. Результат выполнения RUN фиксируется в слое образа навсегда. * CMD выполняется на этапе запуска контейнера (run time). Это команда по умолчанию. Если пользователь при запуске контейнера укажет свою команду (например, docker run my-image bash), то инструкция CMD будет полностью проигнорирована. * ENTRYPOINT похожа на CMD, но её сложнее переопределить. Обычно она используется для превращения контейнера в «исполняемый файл», где аргументы из командной строки добавляются к основной команде.

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

    Docker использует объединительную файловую систему (Union File System). Каждый раз, когда вы пишете RUN, COPY или ADD, Docker создает новый слой. Эти слои доступны только для чтения и накладываются друг на друга.

    Почему это важно? Из-за кэширования. При повторной сборке (docker build) демон проверяет, изменилась ли инструкция или файлы, которые она затрагивает. Если изменений нет, Docker берет готовый слой из кэша.

    > Эффективный Dockerfile строится по принципу: от редко меняющихся данных к часто меняющимся. > > Docker Documentation: Best practices for writing Dockerfiles

    Именно поэтому в примере выше мы сначала копируем requirements.txt и запускаем pip install, а только потом копируем весь исходный код (COPY . .). Если вы измените одну строчку в коде приложения, но не тронете список зависимостей, Docker пропустит тяжелый этап установки библиотек и просто пересоберет последние слои за доли секунды. Если же поменять порядок и сначала копировать код, то любое изменение в app.py приведет к тому, что кэш для всех последующих строк сбросится, и вам придется ждать переустановки всех пакетов при каждой сборке.

    Процесс сборки и тегирование

    Когда Dockerfile готов, наступает этап сборки. Основная команда выглядит так:

    docker build -t my-app:v1.0 .

    Разберем аргументы:

  • -t (tag): присваивает образу понятное имя и метку (тег). Формат обычно выглядит как имя_репозитория:тег. Если тег не указан, Docker автоматически назначит latest. Однако в продакшн-средах использование latest считается плохой практикой, так как это лишает сборку воспроизводимости (сегодня latest — это одна версия, завтра — другая).
  • Точка (.): это путь к контексту сборки. Это критически важный момент. Контекст — это набор файлов, которые клиент Docker отправляет демону для сборки. Не стоит запускать сборку из корневой директории диска /, иначе Docker попытается отправить гигабайты системных файлов в демон, что приведет к зависанию.
  • Управление контекстом через .dockerignore

    По аналогии с .gitignore, файл .dockerignore позволяет исключить файлы из контекста сборки. В него обязательно следует вносить: * Локальные папки зависимостей (node_modules, venv, __pycache__). * Скрытые папки систем контроля версий (.git). * Логи и временные файлы. * Секретные файлы (ключи доступа, .env), чтобы они случайно не попали в слои образа.

    Практический кейс: сборка веб-сервиса на Node.js

    Рассмотрим более сложный пример, учитывающий нюансы безопасности и оптимизации.

    В этом примере мы использовали npm ci вместо npm install. Команда ci (Clean Install) предназначена для автоматизированных сред: она строго следует package-lock.json и работает быстрее. Также мы добавили создание пользователя. По умолчанию процессы в контейнере запускаются от имени root. Если злоумышленник найдет уязвимость в вашем приложении, он получит права суперпользователя внутри контейнера. Переключение на appuser — это стандарт безопасности "Defense in Depth".

    Проверка и инспекция образов

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

    docker image ls (или устаревший, но популярный вариант docker images).

    Вы увидите таблицу с колонками REPOSITORY, TAG, IMAGE ID, CREATED и SIZE. Обратите внимание на IMAGE ID — это уникальный SHA256-хэш образа. Даже если вы переименуете образ, его ID останется прежним, если содержимое слоев не менялось.

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

    docker history my-app:v1.0

    Она покажет список всех слоев, команды, которыми они были созданы, и их размер. Это отличный инструмент для поиска «жирных» слоев, которые раздувают размер образа. Если вы видите, что команда RUN apt-get update занимает 50 МБ, возможно, стоит объединить её с установкой пакетов и очисткой кэша в одну строку, чтобы уменьшить итоговый вес.

    Типичные ошибки при написании Dockerfile

  • Слишком много слоев. Каждая инструкция RUN создает слой. Вместо:
  • Лучше использовать объединение через && и перенос строки \: Это не только уменьшает количество слоев, но и позволяет удалить временные файлы (кэш apt) в том же слое, где они были созданы. Если удалить их в следующей инструкции RUN, физически они останутся в предыдущем слое, и размер образа не уменьшится.

  • Хранение секретов в образе. Никогда не используйте ENV для хранения паролей или API-ключей в Dockerfile. Любой, кто имеет доступ к образу, может увидеть их через docker inspect или docker history. Секреты должны передаваться в контейнер во время запуска через переменные окружения или тома (это мы разберем в следующих главах).
  • Использование нестабильных тегов. Базовый образ FROM python:3 может внезапно обновиться с версии 3.9 до 3.10, что сломает вашу сборку. Всегда фиксируйте хотя бы минорную версию: python:3.9.
  • Автоматизация и воспроизводимость

    Dockerfile превращает инфраструктуру в артефакт, который можно хранить в Git. Это позволяет внедрить процессы CI/CD (Continuous Integration / Continuous Deployment). При каждом пуше кода в репозиторий сервер сборки (например, GitHub Actions или GitLab CI) выполняет docker build, прогоняет тесты внутри созданного контейнера и, в случае успеха, отправляет (push) готовый образ в реестр.

    Таким образом, мы гарантируем, что образ, который тестировался разработчиком, — это тот же самый образ, который попадет на сервер тестирования и, в конечном итоге, в продакшн. Исключается человеческий фактор и ошибки ручной настройки серверов.

    Сборка образов — это баланс между скоростью (кэширование), размером (выбор базового образа и очистка) и безопасностью (непривилегированные пользователи). Овладев искусством написания Dockerfile, вы получаете полный контроль над средой исполнения вашего приложения, делая его по-настоящему портативным.