Skip to the content.

Вернуться к главной странице, списку всех тем

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

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

Что нужно знать заранее: темы 1–3 — процессы, порты, YAML, git

Сколько времени займёт: 3 часа теории + 8–10 часов практики

Твой шаг в сквозном проекте: упакуешь «Заметки» в собственный образ и поднимешь их вместе с nginx и базой данных одной командой


DevOps — это упаковка, тестирование и доставка приложений. Значит, нам критически важно уметь запускать, обновлять и собирать приложения в любом месте. Но приложению для работы нужны библиотеки, конфигурационные файлы, определённая версия языка программирования, переменные окружения. Отсюда классическая беда: «на моём компьютере работает, а на сервере нет»

Вспомни тему 1: мы запускали «Заметки» через python3 app.py. Это работает ровно до тех пор, пока на сервере стоит нужная версия Python. Поставили другую — приложение сломалось. Контейнер решает именно эту проблему

Контейнеризация

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

Ключевые термины:

  1. Docker — приложение, работающее как systemd-сервис (то есть запущено постоянно), предназначенное для создания, запуска и управления контейнерами
  2. Контейнер (Container) — изолированная среда, содержащая всё необходимое для запуска приложения: код, зависимости, библиотеки, конфиги. Контейнеры используют ядро хоста, но не видят друг друга
  3. Образ (Image) — шаблон из слоёв, на основе которого создаются контейнеры. Образ — рецепт слоёного пирога, контейнер — готовый пирог. Из одного образа можно испечь сколько угодно пирогов
  4. Реестр / Registry — хранилище образов. Бывает общедоступным (Docker Hub) или частным, поднятым внутри компании
  5. Тег (Tag) — метка версии образа. nginx:1.27.3 — конкретная версия, nginx:latest — «последняя на момент скачивания»
  6. Dockerfile — файл с инструкциями сборки образа: с какого базового образа начать и что в него добавить
  7. Docker CLI — командный интерфейс: собрать, запустить, остановить, удалить
  8. Volume (Том) — механизм хранения данных вне контейнера. Данные переживают удаление контейнера
  9. Network (Сеть) — способ соединить контейнеры друг с другом и с внешним миром

Как контейнер устроен внутри

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

Отсюда следуют важные практические вещи: контейнер стартует за доли секунды (это же просто запуск процесса), не может использовать чужое ядро (Linux-контейнер не запустится на ядре Windows напрямую) и по умолчанию теряет все изменения файлов при удалении

Преимущества контейнеризации

  1. Скорость — контейнеры создаются и удаляются мгновенно, полноценную ОС ставить не нужно
  2. Переносимость — работают везде, где есть Docker
  3. Экономия ресурсов — общее ядро, поэтому меньше накладных расходов, чем у виртуальных машин
  4. Одинаковое окружение — разработчик, тестировщик и сервер запускают буквально один и тот же образ

Недостатки контейнеризации

  1. Изоляция слабее — ядро общее, поэтому уязвимость в ядре потенциально затрагивает все контейнеры на машине
  2. Только своё ядро — на Linux-хосте запускаются Linux-контейнеры. Docker Desktop на Windows и macOS незаметно поднимает Linux-виртуалку

Виртуализация

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

Примеры: VMware ESXi (популярна в крупных компаниях), VirtualBox (бесплатный инструмент для локальной работы), KVM (встроен в ядро Linux, на нём работает большинство облаков)

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

Недостатки: каждая VM требует много памяти и процессорного времени, разворачивается минутами, а не секундами, тяжело масштабируется

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

  Виртуальная машина Контейнер
Что изолируется Целая ОС со своим ядром Процесс, ядро общее с хостом
Запуск Минуты Доли секунды
Размер Гигабайты Десятки мегабайт
Изоляция Сильная Слабее

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

Каждая инструкция в Dockerfile создаёт новый слой — «разницу» с предыдущим состоянием. Слои складываются друг на друга и доступны только для чтения; сверху контейнер получает тонкий записываемый слой

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

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

Запуск нескольких контейнеров вместе

Compose — инструмент для запуска многоконтейнерных приложений. Он работает по файлу compose.yml, в котором описано, какие контейнеры запустить, с какими переменными среды, сетями и томами

Пример: интернет-магазин — это как минимум два контейнера, которые должны видеть друг друга:

  1. Веб-приложение — принимает HTTP-запросы, обращается к базе за данными, отдаёт HTML-страницы
  2. База данных — хранит товары и учётные записи

Compose поднимает их одной командой, создаёт общую сеть и позволяет контейнерам обращаться друг к другу по имени сервиса вместо IP-адреса. Это и есть встроенный DNS Docker: сервис web может открыть http://db:5432, не зная никаких адресов

Теоретические вопросы

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

  2. Как Docker обеспечивает изоляцию контейнеров? Ответ: Средствами ядра Linux: namespaces изолируют процессы, сеть, файловую систему и имена; cgroups ограничивают ресурсы (CPU, память, ввод-вывод); union-файловая система собирает корневой каталог из слоёв образа

  3. Сохраняются ли данные контейнера после его остановки? Ответ: После stop — да, записываемый слой остаётся, и docker start вернёт файлы. После rm — нет, всё пропадает. Поэтому данные, которые нельзя терять, хранят в томах

  4. Зачем нужны тома (volumes)? Ответ: Чтобы данные жили дольше контейнера: база данных, загруженные файлы, логи. Том можно переподключить к новому контейнеру после обновления образа

  5. Чем именованный том отличается от bind mount? Ответ: Именованный том (-v mydata:/var/lib/postgresql/data) управляется Docker и лежит в /var/lib/docker/volumes/ — так хранят данные. Bind mount (-v $(pwd)/html:/usr/share/nginx/html) подключает конкретный каталог хоста — так удобно разрабатывать, правки видны сразу без пересборки

  6. Что такое логи контейнера и где они лежат? Ответ: Всё, что приложение пишет в стандартный вывод и поток ошибок. Docker перехватывает это и хранит в /var/lib/docker/containers/<id>/, а показывает по docker logs. Именно поэтому приложение в контейнере должно писать логи в консоль, а не в файл

  7. Как формируется образ? Ответ: По Dockerfile: берётся базовый образ, каждая инструкция добавляет слой. Готовый образ — упорядоченный набор слоёв только для чтения плюс метаданные (какую команду запускать, какие порты, какой пользователь)

  8. Что происходит при создании контейнера? Ответ: Docker берёт слои образа, добавляет сверху пустой записываемый слой, создаёт namespaces и cgroups и запускает в них указанный процесс

  9. В чём разница между образом и контейнером? Ответ: Образ — неизменяемый шаблон на диске. Контейнер — запущенный из него экземпляр со своим записываемым слоем. Из одного образа можно запустить сто контейнеров

  10. Приведи пример простого Dockerfile. Ответ:
    FROM python:3.12-slim
    WORKDIR /app
    COPY app.py .
    EXPOSE 8080
    CMD ["python", "app.py"]
    
  11. Как создаются слои и что происходит при добавлении нового? Ответ: Каждая инструкция FROM, RUN, COPY, ADD создаёт слой — зафиксированную разницу с предыдущим состоянием файловой системы. Новый слой не изменяет старые: удаление файла лишь помечает его скрытым, но место в образе он занимать продолжает

  12. Какие основные инструкции есть в Dockerfile? Ответ: FROM (базовый образ), RUN (выполнить команду при сборке), COPY (скопировать файлы), WORKDIR (рабочий каталог), ENV (переменная окружения), EXPOSE (документировать порт), USER (от какого пользователя работать), HEALTHCHECK (как проверять живость), CMD и ENTRYPOINT (что запускать)

  13. Почему важен порядок инструкций в Dockerfile? Ответ: Из-за кеша слоёв: всё до первой изменившейся инструкции переиспользуется. Редко меняющееся (установка зависимостей) ставят выше, часто меняющееся (свой код) — ниже. Иначе каждая правка строки кода вызывает полную пересборку

  14. В чём разница между ENTRYPOINT и CMD? Ответ: ENTRYPOINT — что запускать, обычно не переопределяется. CMD — аргументы по умолчанию, их легко заменить в docker run. При ENTRYPOINT ["ping"] и CMD ["localhost"] команда docker run образ ya.ru выполнит ping ya.ru

  15. Можно ли использовать ENTRYPOINT и CMD одновременно? Ответ: Да, это и есть рекомендуемый способ: ENTRYPOINT задаёт программу, CMD — аргументы по умолчанию. Важно использовать форму списка (["ping"]), а не строку — иначе процесс запустится через shell и не получит сигнал SIGTERM при остановке

  16. Почему нельзя полагаться на тег latest? Ответ: latest — обычная подвижная метка: сегодня это одна версия, завтра другая. Сборка перестаёт быть воспроизводимой, а обновление приезжает неожиданно и может всё сломать. В работе указывают точную версию, а в критичных местах — цифровой отпечаток (nginx@sha256:...)

  17. Как остановить контейнер, но не уничтожить его? Ответ: docker stop имя — посылается SIGTERM, через 10 секунд SIGKILL. Контейнер останется в списке docker ps -a, запустить снова — docker start имя

  18. Допустимо ли запускать несколько контейнеров на одной машине? Ответ: Да, это обычная практика — их могут быть десятки. Ограничения только по ресурсам и по портам: два контейнера не могут занять один и тот же порт хоста

  19. Какой командой посмотреть скачанные образы? Ответ: docker images (она же docker image ls). Занимаемое место — docker system df

  20. Как выгрузить образ в реестр? Ответ: Пометить именем реестра и отправить: docker tag notes:1.0 логин/notes:1.0, затем docker login и docker push логин/notes:1.0

  21. Какие основные типы сетей поддерживает Docker? Ответ: bridge — по умолчанию, свой изолированный сегмент со встроенным DNS по именам контейнеров; host — контейнер использует сеть хоста напрямую, без изоляции портов; none — сети нет вовсе; overlay — сеть между контейнерами на разных машинах

  22. Чем сборка через Kaniko лучше сборки через Docker? Ответ: Kaniko собирает образ внутри контейнера, не обращаясь к демону Docker, и потому не требует привилегированного режима. В CI это важно: пробрасывать /var/run/docker.sock в задачу пайплайна означает фактически отдать ей root на машине сборки

  23. Чем базовый образ alpine отличается от ubuntu? Ответ: Alpine занимает около 5 МБ против примерно 78 МБ у Ubuntu, потому что использует облегчённую библиотеку musl вместо glibc. Обратная сторона — часть программ ведёт себя иначе или требует пересборки, а отладка сложнее. Компромисс — образы -slim на базе Debian

  24. В чём разница между COPY и ADD? Ответ: COPY просто копирует файлы. ADD дополнительно умеет распаковывать локальные архивы и скачивать по URL. Из-за неявного поведения рекомендуется всегда использовать COPY, а ADD — только когда нужна распаковка архива

  25. Можно ли ограничить память и CPU для отдельного контейнера? Ответ: Да. В docker run: --memory=256m --cpus=0.5. В Compose — секция deploy.resources.limits. Без лимитов один сбойный контейнер способен съесть память всей машины

  26. Как сделать, чтобы контейнер перезапускался сам после падения? Ответ: В docker run — флаг --restart unless-stopped. В Compose — restart: unless-stopped. Варианты: no (по умолчанию), on-failure (только при ненулевом коде выхода), always, unless-stopped (не поднимать, если остановили руками)

  27. Зачем в Dockerfile указывают USER? Ответ: По умолчанию процесс в контейнере работает от root. Если злоумышленник использует уязвимость приложения, он получит root внутри контейнера — а это заметно ближе к root на хосте. Создать непривилегированного пользователя и переключиться на него — обязательная практика

  28. Что такое multi-stage build? Ответ: Сборка в несколько этапов: в первом образе, где есть компилятор и инструменты, собирают приложение; во второй, минимальный, копируют только результат. Итоговый образ меньше в разы, и в нём нет ни исходников, ни компилятора

  29. Зачем нужен .dockerignore? Ответ: Он исключает файлы из контекста сборки. Без него в сборку уедут .git, логи и локальные секреты: контекст будет весить сотни мегабайт, сборка замедлится, а секреты могут попасть в образ

  30. Что означает статус OOMKilled? Ответ: Контейнер превысил лимит памяти, и ядро убило процесс. Смотрят docker inspect и разбираются: либо у приложения утечка памяти, либо лимит поставлен слишком низкий

Вопросы с собеседований

  1. Где хранить собранные образы? Ответ: В реестре (registry). Публичный — Docker Hub. Внутри компании поднимают свой: Harbor, Nexus, Artifactory, либо используют встроенный в GitLab. Причины держать свой: не зависеть от внешнего сервиса и его лимитов на скачивание, сканировать образы на уязвимости, ограничивать доступ и хранить рядом с кластером

  2. Что такое базовый образ и как его выбирать? Ответ: Образ, указанный в FROM, поверх которого строятся твои слои. Выбирают по трём критериям: минимальный размер, наличие обновлений безопасности и совместимость с твоим приложением. Практическое правило: официальный образ языка в варианте -slim, а alpine — только если проверил, что зависимости с ним работают

  3. Что происходит с данными, записанными приложением в контейнер, после его перезапуска? Ответ: После restart и stop/start данные на месте — записываемый слой сохраняется. После rm и пересоздания контейнера они исчезают. Поэтому всё, что нельзя терять, монтируют в том

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

  5. Где лежат тома и логи Docker? Ответ: Тома — в /var/lib/docker/volumes/, логи контейнеров — в /var/lib/docker/containers/<id>/<id>-json.log. Логам обязательно настраивают ротацию через log-opts (max-size, max-file), иначе один разговорчивый контейнер способен забить весь диск — это классическая авария

  6. Контейнер завершился с кодом 137. Что это значит? Ответ: 128 + 9, то есть процесс убит сигналом SIGKILL. Почти всегда это превышение лимита памяти — в docker inspect будет "OOMKilled": true. Реже — кто-то выполнил docker kill или приложение не успело завершиться за отведённое docker stop время. Код 143 (128 + 15) означает штатное завершение по SIGTERM

  7. В чём разница между формой списка и строковой формой у CMD и ENTRYPOINT? Ответ: Форма списка (CMD ["python", "app.py"]) запускает процесс напрямую, и он получает PID 1 и сигналы от Docker. Строковая форма (CMD python app.py) запускает всё через /bin/sh -c, и PID 1 достаётся оболочке, которая сигналы дальше не передаёт. Результат: приложение не получает SIGTERM, не завершается корректно и через 10 секунд убивается принудительно. Всегда используй форму списка

  8. Что такое проблема PID 1 в контейнере? Ответ: Процесс с PID 1 в Linux имеет особый статус: он обязан подбирать «осиротевшие» дочерние процессы, иначе те остаются зомби и копятся, съедая таблицу процессов. Обычные приложения этого не умеют. Если твоё приложение порождает потомков, запускай контейнер с флагом --init (или init: true в Compose) — он подставит крошечный корректный PID 1

  9. Как уменьшить размер образа? Ответ: Многоэтапная сборка — в итог попадает только результат, без компилятора и исходников. Лёгкий базовый образ (-slim, alpine, distroless). Объединение команд установки в один RUN с очисткой кеша пакетов в той же строке — иначе удалённое останется в предыдущем слое. Полноценный .dockerignore. Проверить, что именно занимает место, помогает docker history

  10. Что такое distroless-образ и в чём сложность работы с ним? Ответ: Образ без оболочки, пакетного менеджера и утилит — только приложение и его зависимости. Плюсы: минимальный размер и очень маленькая поверхность атаки. Минус: внутрь не зайти через docker exec ... sh, потому что оболочки нет. Отлаживают такие контейнеры временным подключением отладочного контейнера к тому же пространству имён

  11. Что такое контекст сборки и почему он бывает огромным? Ответ: Каталог, который клиент Docker целиком отправляет демону перед сборкой. Если собирать из каталога с .git, node_modules и логами, туда уедут сотни мегабайт, и сборка будет медленной ещё до первой инструкции. Лечится .dockerignore. Видно это в самом начале вывода: Sending build context to Docker daemon

  12. Как отлаживать контейнер, который сразу падает? Ответ: docker logs имя — что он успел написать. docker ps -a — код выхода. docker inspect — причина завершения и был ли OOM. Если логов нет, запускают образ с заменой команды: docker run -it --entrypoint sh образ — попадёшь внутрь и сможешь выполнить команду руками и увидеть ошибку

  13. Чем docker stop отличается от docker kill? Ответ: stop посылает SIGTERM, ждёт (по умолчанию 10 секунд) и только потом SIGKILL — у приложения есть шанс дописать данные и закрыть соединения. kill сразу посылает SIGKILL. Время ожидания меняется флагом -t. Это ровно та же логика вежливого завершения, что и в теме 1

  14. Чем depends_on отличается от healthcheck в Compose? Ответ: depends_on в простом виде управляет только порядком запуска: контейнер стартовал — значит, зависимость выполнена, хотя приложение внутри может ещё не подняться. Чтобы дождаться реальной готовности, у зависимости описывают healthcheck, а в depends_on указывают condition: service_healthy. Без этого связка приложение-база регулярно падает на старте

  15. Как контейнеры находят друг друга и почему localhost не работает? Ответ: В пользовательской сети Docker поднимает DNS, и контейнеры доступны по именам сервисов. localhost внутри контейнера означает его собственное сетевое пространство, а не хост и не соседний контейнер — поэтому обращение к базе по localhost не находит ничего. Обращаться нужно по имени сервиса; к самому хосту — по host.docker.internal там, где оно поддерживается

  16. Что такое dangling-образы и откуда они берутся? Ответ: Слои без тега, отображаемые как <none>. Появляются, когда пересобираешь образ с тем же тегом: тег переезжает на новую сборку, а старая остаётся безымянной. Со временем они занимают десятки гигабайт. Удаляются docker image prune; посмотреть общий расход — docker system df

  17. Почему опасно пробрасывать /var/run/docker.sock внутрь контейнера? Ответ: Это сокет управления демоном Docker. Получив к нему доступ, контейнер может запустить другой контейнер с подключённой корневой файловой системой хоста — то есть фактически получить root на сервере. Именно поэтому в CI предпочитают сборщики, не требующие демона, например Kaniko

Практическая часть

Что понадобится: компьютер с Linux (или WSL), интернет и терминал. Примеры даны для Linux/macOS; в PowerShell отличается только запись текущего каталога — вместо $(pwd) пиши ${PWD}, а файлы создавай редактором, а не через cat > файл

Задание 0. Установка и проверка Docker

Цель: убедиться, что Docker установлен и работает

Шаги:

docker --version      # версия — значит, клиент установлен
docker ps             # список запущенных контейнеров

Ожидаемый вывод: версия вида Docker version 27.3.1 и пустая таблица со столбцами CONTAINER ID, IMAGE, COMMAND

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

Задание 1. Образы

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

Шаги:

docker pull nginx:1.27.3        # скачать конкретную версию
docker images nginx             # посмотреть, что скачалось
docker history nginx:1.27.3     # из каких слоёв состоит образ
docker system df                # сколько места занято образами

Ожидаемый вывод: таблица со столбцами REPOSITORY, TAG, IMAGE ID, SIZE. В history видно каждый слой и его вклад в размер

Ответь письменно: почему в реальных проектах не используют тег latest?

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

Задание 2. Первый контейнер

Цель: запустить контейнер и получить к нему доступ снаружи

Шаги:

docker run nginx:1.27.3          # терминал «повиснет» и покажет логи — так и надо

Останови по Ctrl+C. Подключиться к нему не получилось — порт не проброшен. Запустим правильно:

docker run -d -p 8081:80 --name my-nginx nginx:1.27.3

Разбор: -d — в фоне, -p 8081:80 — порт 8081 на твоей машине ведёт на порт 80 внутри контейнера, --name — понятное имя

docker ps                          # контейнер в списке работающих
curl http://localhost:8081/        # приветственная страница nginx
docker logs my-nginx               # видно твой запрос
docker exec -it my-nginx bash      # зайти внутрь контейнера
  ls /usr/share/nginx/html         # выполнить внутри
  exit
docker stop my-nginx && docker rm my-nginx

Ожидаемый вывод: curl возвращает HTML Welcome to nginx!; в docker logs появляется строка запроса с кодом 200

Почему 8081, а не 8080: порт 8080 у нас занят сервисом «Заметки» из темы 1. Если запустить контейнер на занятом порту, Docker честно скажет port is already allocated — это хороший повод вспомнить ss -tulpn

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

Задание 3. Свой образ

Цель: собрать собственный образ и понять кеш слоёв

Шаги:

  1. Создай каталог и страницу:

    mkdir ~/my-first-docker-project && cd ~/my-first-docker-project
    cat > index.html << 'EOF'
    <!DOCTYPE html>
    <html lang="ru">
    <head><meta charset="UTF-8"><title>Моя первая страница в Docker</title></head>
    <body>
      <h1>Получилось!</h1>
      <p>Студент: <strong>Твоё Имя</strong></p>
      <p>Версия: 1.0</p>
    </body>
    </html>
    EOF
    
  2. Узнай, откуда nginx берёт файлы сайта:

    docker run --rm nginx:1.27.3 cat /etc/nginx/conf.d/default.conf | grep root
    

    Флаг --rm удаляет временный контейнер сразу после выполнения команды

  3. Создай Dockerfile:

    # Базовый образ, на котором строим свой
    FROM nginx:1.27.3
    
    # Кладём свою страницу туда, где nginx её ищет
    COPY index.html /usr/share/nginx/html/
    
    # Документируем порт: это подсказка для человека, порт всё равно нужно пробрасывать через -p
    EXPOSE 80
    
    # CMD не нужен: базовый образ уже знает, как запускать nginx
    
  4. Собери и запусти:

    docker build -t devops:1.0 .
    docker images devops
    docker run -d -p 8081:80 --name my-first-app devops:1.0
    curl http://localhost:8081/
    

    Разбор docker build -t devops:1.0 .: -t — имя и тег образа, . — контекст сборки, то есть каталог, содержимое которого отправляется демону Docker

  5. Почувствуй кеш. Пересобери образ без изменений — сборка займёт доли секунды и все шаги будут помечены CACHED. Теперь измени index.html и пересобери снова: пересоберётся только слой COPY

    docker build -t devops:1.0 .              # всё CACHED
    sed -i 's/Версия: 1.0/Версия: 2.0/' index.html
    docker build -t devops:2.0 .              # пересобрался только последний слой
    docker stop my-first-app && docker rm my-first-app
    docker run -d -p 8081:80 --name my-app-v2 devops:2.0
    curl http://localhost:8081/
    

Ожидаемый вывод: в браузере и в curl — твоя страница; при второй сборке видно слово CACHED напротив неизменившихся шагов

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

Задание 4. Тома: данные, которые переживают контейнер

Цель: понять разницу между bind mount и именованным томом

Шаги:

Bind mount — подключаем каталог хоста, правки видны сразу:

mkdir html-files && cp index.html html-files/
docker run -d -p 8081:80 -v $(pwd)/html-files:/usr/share/nginx/html \
  --name nginx-with-volume nginx:1.27.3

sed -i 's/Версия: 2.0/Версия: 3.0 (без пересборки!)/' html-files/index.html
curl http://localhost:8081/          # изменения видны сразу, образ не пересобирали
docker stop nginx-with-volume && docker rm nginx-with-volume

Именованный том — так хранят данные:

docker volume create notes-data
docker run --rm -v notes-data:/data alpine:3.20 sh -c 'echo "важные данные" > /data/file.txt'
docker run --rm -v notes-data:/data alpine:3.20 cat /data/file.txt
docker volume inspect notes-data     # посмотри, где физически лежит том

Ожидаемый вывод: второй контейнер читает файл, созданный первым, хотя сам первый контейнер уже удалён (--rm). Именно так база данных переживает обновление своего образа

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

Задание 5. Compose: несколько контейнеров вместе

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

Шаги: создай compose.yml:

# Ключ version больше не нужен: современный Compose его игнорирует и предупреждает

services:
  web:
    image: devops:2.0
    container_name: my-web-app
    ports:
      - "8081:80"
    restart: unless-stopped

  # Контейнер, который раз в 10 секунд стучится к web — чтобы увидеть сеть в действии
  tester:
    image: alpine:3.20
    container_name: web-tester
    command: >
      sh -c "
      apk add --no-cache curl &&
      while true; do
        echo '=== запрос к web ===' &&
        curl -s http://web:80 | head -n 3 &&
        sleep 10;
      done
      "
    depends_on:
      - web
docker compose up               # запуск с логами в терминале, выход Ctrl+C
docker compose up -d            # то же в фоне
docker compose ps               # статус сервисов
docker compose logs -f web      # логи одного сервиса
docker compose down             # остановить и удалить контейнеры и сеть

Ожидаемый вывод: в логах чередуются строки tester (отправляет запрос) и web (получил запрос с кодом 200)

Обрати внимание на главное: tester обращается по адресу http://web:80 — по имени сервиса, а не по IP. Compose создал сеть и поднял в ней DNS. В теме 5 Kubernetes сделает ровно то же самое, только на масштабе кластера

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

Задание 6. Сквозной проект: «Заметки» в контейнере

Цель: упаковать своё приложение в образ, поднять его вместе с nginx и базой и получить конфигурацию, которую можно запустить на любой машине одной командой

Шаги:

  1. Перейди в репозиторий «Заметок» из темы 3:

    cd ~/notes
    
  2. Создай .dockerignore — что не должно попасть в образ:

    .git
    *.log
    notes.txt
    .env
    *.key
    *.crt
    __pycache__/
    
  3. Создай Dockerfile:

    # slim — образ на базе Debian без лишнего: 130 МБ вместо ~1 ГБ у полного python
    FROM python:3.12-slim
    
    # Не писать .pyc-файлы и не буферизовать вывод —
    # иначе логи приложения будут появляться в docker logs с задержкой
    ENV PYTHONDONTWRITEBYTECODE=1 \
        PYTHONUNBUFFERED=1
    
    WORKDIR /app
    
    # Создаём непривилегированного пользователя: работать от root в контейнере нельзя
    RUN useradd --create-home --shell /bin/bash notes
    
    # Код копируем последним: он меняется чаще всего, и так кеш слоёв выше не сбрасывается
    COPY app.py .
    
    ENV NOTES_FILE=/data/notes.txt \
        PORT=8080
    
    # Каталог для данных, который мы подключим томом
    RUN mkdir -p /data && chown notes:notes /data
    
    USER notes
    
    EXPOSE 8080
    
    # Docker сам будет проверять живость приложения по нашему эндпоинту из темы 1
    HEALTHCHECK --interval=10s --timeout=2s --start-period=5s --retries=3 \
      CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/healthz')"
    
    # Форма списка обязательна: так процесс получит SIGTERM напрямую,
    # без прослойки shell, и успеет корректно завершиться
    CMD ["python", "app.py"]
    
  4. Собери и проверь:

    docker build -t notes:1.0 .
    docker run -d -p 8080:8080 --name notes notes:1.0
    docker ps                                  # в столбце STATUS через ~15 секунд появится (healthy)
    curl http://localhost:8080/healthz
    docker logs notes
    

    Если порт 8080 занят сервисом из темы 1 — останови его: sudo systemctl stop notes. Теперь приложение живёт в контейнере, systemd для этого больше не нужен

  5. Опиши весь стенд в compose.yml:

    services:
      notes:
        build: .                     # собрать образ из Dockerfile в этом каталоге
        image: notes:1.0
        restart: unless-stopped
        environment:
          PORT: "8080"
          NOTES_FILE: /data/notes.txt
        volumes:
          - notes-data:/data         # заметки переживут пересоздание контейнера
        # Порт наружу не публикуем: снаружи к приложению ходит только nginx
        expose:
          - "8080"
        healthcheck:
          test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8080/healthz')"]
          interval: 10s
          timeout: 2s
          retries: 3
        deploy:
          resources:
            limits:
              cpus: "0.5"
              memory: 128M
    
      nginx:
        image: nginx:1.27.3
        restart: unless-stopped
        ports:
          - "8080:80"                # наружу торчит только nginx
        volumes:
          - ./deploy/notes-docker.conf:/etc/nginx/conf.d/default.conf:ro
        depends_on:
          notes:
            condition: service_healthy   # ждать не запуска, а готовности
    
      db:
        image: postgres:17-alpine
        restart: unless-stopped
        environment:
          POSTGRES_USER: notes
          POSTGRES_PASSWORD: notes_dev_password   # так делать нельзя, чиним в теме 9
          POSTGRES_DB: notes
        volumes:
          - db-data:/var/lib/postgresql/data
        healthcheck:
          test: ["CMD-SHELL", "pg_isready -U notes"]
          interval: 10s
          retries: 5
    
    volumes:
      notes-data:
      db-data:
    
  6. Создай конфиг nginx для контейнерной версии — deploy/notes-docker.conf:

    server {
        listen 80;
        server_name _;
    
        location / {
            # notes — имя сервиса из compose.yml, а не IP-адрес
            proxy_pass http://notes:8080;
            proxy_set_header Host              $host;
            proxy_set_header X-Real-IP         $remote_addr;
            proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        }
    }
    
  7. Запускай:

    docker compose up -d --build
    docker compose ps                  # все три сервиса, notes и db со статусом healthy
    curl http://localhost:8080/
    docker compose logs -f notes
    
  8. Проверь, что данные переживают контейнер:

    docker compose exec notes sh -c 'echo "Заметка из контейнера" > /data/notes.txt'
    curl http://localhost:8080/
    docker compose down                # контейнеры удалены, тома остались
    docker compose up -d
    curl http://localhost:8080/        # заметка на месте
    
  9. Сломай специально. Убей приложение и посмотри, как связка отработает:

    docker compose stop notes
    curl -i http://localhost:8080/     # 502 от nginx — знакомо по теме 2
    docker compose start notes
    sleep 15
    curl -i http://localhost:8080/     # снова 200
    
  10. Закоммить результат — теперь стенд разворачивается из репозитория:

    git add Dockerfile .dockerignore compose.yml deploy/
    git commit -m "Упаковать «Заметки» в контейнер и описать стенд в Compose"
    git push
    

Ожидаемый вывод: docker compose up -d одной командой поднимает приложение, веб-сервер и базу; curl http://localhost:8080/ отдаёт заметки; docker compose down && up не теряет данные

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

Полезные команды на каждый день

docker ps -a                       # все контейнеры, включая остановленные
docker logs -f --tail=50 имя       # последние 50 строк логов и дальше в реальном времени
docker exec -it имя bash           # зайти внутрь работающего контейнера
docker stats                       # потребление CPU и памяти в реальном времени
docker inspect имя                 # вся информация о контейнере в JSON
docker image prune                 # удалить образы без тегов
docker container prune             # удалить остановленные контейнеры
docker system df                   # сколько места занято
docker system prune -a             # удалить всё неиспользуемое (осторожно: и образы тоже)
docker <команда> --help            # справка

Проверь себя: тема освоена, если ты можешь

Следующая тема: Kubernetes и Helm →