Вернуться к главной странице, списку всех тем
4. Упаковка приложений со всем необходимым для их работы, передача и запуск таких «контейнеров» другим, запуск нескольких контейнеров вместе
Что ты узнаешь: как упаковать приложение вместе со всем его окружением в контейнер, который одинаково работает на любой машине, и как запускать несколько связанных контейнеров вместе
Что нужно знать заранее: темы 1–3 — процессы, порты, YAML, git
Сколько времени займёт: 3 часа теории + 8–10 часов практики
Твой шаг в сквозном проекте: упакуешь «Заметки» в собственный образ и поднимешь их вместе с nginx и базой данных одной командой
DevOps — это упаковка, тестирование и доставка приложений. Значит, нам критически важно уметь запускать, обновлять и собирать приложения в любом месте. Но приложению для работы нужны библиотеки, конфигурационные файлы, определённая версия языка программирования, переменные окружения. Отсюда классическая беда: «на моём компьютере работает, а на сервере нет»
Вспомни тему 1: мы запускали «Заметки» через python3 app.py. Это работает ровно до тех пор, пока на сервере стоит нужная версия Python. Поставили другую — приложение сломалось. Контейнер решает именно эту проблему
Контейнеризация
Docker — инструмент для упаковки программ так, чтобы их можно было запускать на разных компьютерах, не настраивая окружение заново. Он кладёт программу вместе со всеми зависимостями и настройками в контейнер. Этот контейнер запустится где угодно, где есть Docker, и будет работать одинаково
Ключевые термины:
- Docker — приложение, работающее как systemd-сервис (то есть запущено постоянно), предназначенное для создания, запуска и управления контейнерами
- Контейнер (Container) — изолированная среда, содержащая всё необходимое для запуска приложения: код, зависимости, библиотеки, конфиги. Контейнеры используют ядро хоста, но не видят друг друга
- Образ (Image) — шаблон из слоёв, на основе которого создаются контейнеры. Образ — рецепт слоёного пирога, контейнер — готовый пирог. Из одного образа можно испечь сколько угодно пирогов
- Реестр / Registry — хранилище образов. Бывает общедоступным (Docker Hub) или частным, поднятым внутри компании
- Тег (Tag) — метка версии образа.
nginx:1.27.3— конкретная версия,nginx:latest— «последняя на момент скачивания» - Dockerfile — файл с инструкциями сборки образа: с какого базового образа начать и что в него добавить
- Docker CLI — командный интерфейс: собрать, запустить, остановить, удалить
- Volume (Том) — механизм хранения данных вне контейнера. Данные переживают удаление контейнера
- Network (Сеть) — способ соединить контейнеры друг с другом и с внешним миром
Как контейнер устроен внутри
Контейнер — это не «маленькая виртуальная машина». Это обычный процесс Linux, которому ядро наложило три ограничения:
- namespaces — процесс видит только своё: свой список процессов, свою сеть, свою файловую систему. Внутри контейнера твоё приложение будет иметь PID 1 и думать, что оно одно в системе
- cgroups — сколько ему можно ресурсов: столько-то CPU, столько-то памяти. Превысил лимит памяти — ядро убивает процесс (в логах это будет
OOMKilled) - union filesystem — файловая система собирается из слоёв образа плюс тонкий записываемый слой сверху
Отсюда следуют важные практические вещи: контейнер стартует за доли секунды (это же просто запуск процесса), не может использовать чужое ядро (Linux-контейнер не запустится на ядре Windows напрямую) и по умолчанию теряет все изменения файлов при удалении
Преимущества контейнеризации
- Скорость — контейнеры создаются и удаляются мгновенно, полноценную ОС ставить не нужно
- Переносимость — работают везде, где есть Docker
- Экономия ресурсов — общее ядро, поэтому меньше накладных расходов, чем у виртуальных машин
- Одинаковое окружение — разработчик, тестировщик и сервер запускают буквально один и тот же образ
Недостатки контейнеризации
- Изоляция слабее — ядро общее, поэтому уязвимость в ядре потенциально затрагивает все контейнеры на машине
- Только своё ядро — на Linux-хосте запускаются Linux-контейнеры. Docker Desktop на Windows и macOS незаметно поднимает Linux-виртуалку
Виртуализация
Виртуализация — создание виртуальных машин (VM): на одном физическом компьютере работают несколько виртуальных, у каждой своя копия операционной системы. Управляет ими гипервизор, распределяющий ресурсы железа
Примеры: VMware ESXi (популярна в крупных компаниях), VirtualBox (бесплатный инструмент для локальной работы), KVM (встроен в ядро Linux, на нём работает большинство облаков)
Преимущества: полная изоляция (своя ОС у каждой машины), безопасность (сбой одной не затрагивает другие), возможность запускать разные операционные системы на одном железе
Недостатки: каждая VM требует много памяти и процессорного времени, разворачивается минутами, а не секундами, тяжело масштабируется
Как выбирать. На практике их не противопоставляют, а складывают: облако даёт тебе виртуальные машины, а на них ты запускаешь контейнеры. Виртуализация даёт жёсткую границу между клиентами провайдера, контейнеры — быструю упаковку приложений внутри твоей части
| Виртуальная машина | Контейнер | |
|---|---|---|
| Что изолируется | Целая ОС со своим ядром | Процесс, ядро общее с хостом |
| Запуск | Минуты | Доли секунды |
| Размер | Гигабайты | Десятки мегабайт |
| Изоляция | Сильная | Слабее |
Слои образа и почему сборка бывает мгновенной
Каждая инструкция в Dockerfile создаёт новый слой — «разницу» с предыдущим состоянием. Слои складываются друг на друга и доступны только для чтения; сверху контейнер получает тонкий записываемый слой
Из этого следует главное практическое правило: Docker кеширует слои. При повторной сборке он переиспользует всё до первой изменившейся инструкции, а дальше пересобирает. Поэтому порядок инструкций влияет на скорость: сначала кладут то, что меняется редко (установка зависимостей), в конце — то, что меняется каждый раз (твой код). Если поставить копирование кода первой строкой, кеш будет сбрасываться при каждой правке и сборка займёт минуты вместо секунд
Второе следствие: удалённый в следующем слое файл всё ещё занимает место в образе и остаётся доступным тому, кто разберёт образ по слоям. Поэтому секрет, скопированный в образ и удалённый следующей строкой, не удалён
Запуск нескольких контейнеров вместе
Compose — инструмент для запуска многоконтейнерных приложений. Он работает по файлу compose.yml, в котором описано, какие контейнеры запустить, с какими переменными среды, сетями и томами
Пример: интернет-магазин — это как минимум два контейнера, которые должны видеть друг друга:
- Веб-приложение — принимает HTTP-запросы, обращается к базе за данными, отдаёт HTML-страницы
- База данных — хранит товары и учётные записи
Compose поднимает их одной командой, создаёт общую сеть и позволяет контейнерам обращаться друг к другу по имени сервиса вместо IP-адреса. Это и есть встроенный DNS Docker: сервис web может открыть http://db:5432, не зная никаких адресов
Теоретические вопросы
-
В чём ключевые различия между виртуализацией и контейнеризацией? Ответ: Виртуальная машина эмулирует железо и запускает полноценную ОС со своим ядром — сильная изоляция, но гигабайты и минуты на запуск. Контейнер — процесс на общем с хостом ядре, ограниченный namespaces и cgroups: десятки мегабайт и доли секунды, но изоляция слабее
-
Как Docker обеспечивает изоляцию контейнеров? Ответ: Средствами ядра Linux: namespaces изолируют процессы, сеть, файловую систему и имена; cgroups ограничивают ресурсы (CPU, память, ввод-вывод); union-файловая система собирает корневой каталог из слоёв образа
-
Сохраняются ли данные контейнера после его остановки? Ответ: После
stop— да, записываемый слой остаётся, иdocker startвернёт файлы. Послеrm— нет, всё пропадает. Поэтому данные, которые нельзя терять, хранят в томах -
Зачем нужны тома (volumes)? Ответ: Чтобы данные жили дольше контейнера: база данных, загруженные файлы, логи. Том можно переподключить к новому контейнеру после обновления образа
-
Чем именованный том отличается от bind mount? Ответ: Именованный том (
-v mydata:/var/lib/postgresql/data) управляется Docker и лежит в/var/lib/docker/volumes/— так хранят данные. Bind mount (-v $(pwd)/html:/usr/share/nginx/html) подключает конкретный каталог хоста — так удобно разрабатывать, правки видны сразу без пересборки -
Что такое логи контейнера и где они лежат? Ответ: Всё, что приложение пишет в стандартный вывод и поток ошибок. Docker перехватывает это и хранит в
/var/lib/docker/containers/<id>/, а показывает поdocker logs. Именно поэтому приложение в контейнере должно писать логи в консоль, а не в файл -
Как формируется образ? Ответ: По Dockerfile: берётся базовый образ, каждая инструкция добавляет слой. Готовый образ — упорядоченный набор слоёв только для чтения плюс метаданные (какую команду запускать, какие порты, какой пользователь)
-
Что происходит при создании контейнера? Ответ: Docker берёт слои образа, добавляет сверху пустой записываемый слой, создаёт namespaces и cgroups и запускает в них указанный процесс
-
В чём разница между образом и контейнером? Ответ: Образ — неизменяемый шаблон на диске. Контейнер — запущенный из него экземпляр со своим записываемым слоем. Из одного образа можно запустить сто контейнеров
- Приведи пример простого Dockerfile.
Ответ:
FROM python:3.12-slim WORKDIR /app COPY app.py . EXPOSE 8080 CMD ["python", "app.py"] -
Как создаются слои и что происходит при добавлении нового? Ответ: Каждая инструкция
FROM,RUN,COPY,ADDсоздаёт слой — зафиксированную разницу с предыдущим состоянием файловой системы. Новый слой не изменяет старые: удаление файла лишь помечает его скрытым, но место в образе он занимать продолжает -
Какие основные инструкции есть в Dockerfile? Ответ:
FROM(базовый образ),RUN(выполнить команду при сборке),COPY(скопировать файлы),WORKDIR(рабочий каталог),ENV(переменная окружения),EXPOSE(документировать порт),USER(от какого пользователя работать),HEALTHCHECK(как проверять живость),CMDиENTRYPOINT(что запускать) -
Почему важен порядок инструкций в Dockerfile? Ответ: Из-за кеша слоёв: всё до первой изменившейся инструкции переиспользуется. Редко меняющееся (установка зависимостей) ставят выше, часто меняющееся (свой код) — ниже. Иначе каждая правка строки кода вызывает полную пересборку
-
В чём разница между
ENTRYPOINTиCMD? Ответ:ENTRYPOINT— что запускать, обычно не переопределяется.CMD— аргументы по умолчанию, их легко заменить вdocker run. ПриENTRYPOINT ["ping"]иCMD ["localhost"]командаdocker run образ ya.ruвыполнитping ya.ru -
Можно ли использовать
ENTRYPOINTиCMDодновременно? Ответ: Да, это и есть рекомендуемый способ:ENTRYPOINTзадаёт программу,CMD— аргументы по умолчанию. Важно использовать форму списка (["ping"]), а не строку — иначе процесс запустится через shell и не получит сигналSIGTERMпри остановке -
Почему нельзя полагаться на тег
latest? Ответ:latest— обычная подвижная метка: сегодня это одна версия, завтра другая. Сборка перестаёт быть воспроизводимой, а обновление приезжает неожиданно и может всё сломать. В работе указывают точную версию, а в критичных местах — цифровой отпечаток (nginx@sha256:...) -
Как остановить контейнер, но не уничтожить его? Ответ:
docker stop имя— посылаетсяSIGTERM, через 10 секундSIGKILL. Контейнер останется в спискеdocker ps -a, запустить снова —docker start имя -
Допустимо ли запускать несколько контейнеров на одной машине? Ответ: Да, это обычная практика — их могут быть десятки. Ограничения только по ресурсам и по портам: два контейнера не могут занять один и тот же порт хоста
-
Какой командой посмотреть скачанные образы? Ответ:
docker images(она жеdocker image ls). Занимаемое место —docker system df -
Как выгрузить образ в реестр? Ответ: Пометить именем реестра и отправить:
docker tag notes:1.0 логин/notes:1.0, затемdocker loginиdocker push логин/notes:1.0 -
Какие основные типы сетей поддерживает Docker? Ответ:
bridge— по умолчанию, свой изолированный сегмент со встроенным DNS по именам контейнеров;host— контейнер использует сеть хоста напрямую, без изоляции портов;none— сети нет вовсе;overlay— сеть между контейнерами на разных машинах -
Чем сборка через Kaniko лучше сборки через Docker? Ответ: Kaniko собирает образ внутри контейнера, не обращаясь к демону Docker, и потому не требует привилегированного режима. В CI это важно: пробрасывать
/var/run/docker.sockв задачу пайплайна означает фактически отдать ей root на машине сборки -
Чем базовый образ alpine отличается от ubuntu? Ответ: Alpine занимает около 5 МБ против примерно 78 МБ у Ubuntu, потому что использует облегчённую библиотеку musl вместо glibc. Обратная сторона — часть программ ведёт себя иначе или требует пересборки, а отладка сложнее. Компромисс — образы
-slimна базе Debian -
В чём разница между
COPYиADD? Ответ:COPYпросто копирует файлы.ADDдополнительно умеет распаковывать локальные архивы и скачивать по URL. Из-за неявного поведения рекомендуется всегда использоватьCOPY, аADD— только когда нужна распаковка архива -
Можно ли ограничить память и CPU для отдельного контейнера? Ответ: Да. В
docker run:--memory=256m --cpus=0.5. В Compose — секцияdeploy.resources.limits. Без лимитов один сбойный контейнер способен съесть память всей машины -
Как сделать, чтобы контейнер перезапускался сам после падения? Ответ: В
docker run— флаг--restart unless-stopped. В Compose —restart: unless-stopped. Варианты:no(по умолчанию),on-failure(только при ненулевом коде выхода),always,unless-stopped(не поднимать, если остановили руками) -
Зачем в Dockerfile указывают
USER? Ответ: По умолчанию процесс в контейнере работает от root. Если злоумышленник использует уязвимость приложения, он получит root внутри контейнера — а это заметно ближе к root на хосте. Создать непривилегированного пользователя и переключиться на него — обязательная практика -
Что такое multi-stage build? Ответ: Сборка в несколько этапов: в первом образе, где есть компилятор и инструменты, собирают приложение; во второй, минимальный, копируют только результат. Итоговый образ меньше в разы, и в нём нет ни исходников, ни компилятора
-
Зачем нужен
.dockerignore? Ответ: Он исключает файлы из контекста сборки. Без него в сборку уедут.git, логи и локальные секреты: контекст будет весить сотни мегабайт, сборка замедлится, а секреты могут попасть в образ - Что означает статус
OOMKilled? Ответ: Контейнер превысил лимит памяти, и ядро убило процесс. Смотрятdocker inspectи разбираются: либо у приложения утечка памяти, либо лимит поставлен слишком низкий
Вопросы с собеседований
-
Где хранить собранные образы? Ответ: В реестре (registry). Публичный — Docker Hub. Внутри компании поднимают свой: Harbor, Nexus, Artifactory, либо используют встроенный в GitLab. Причины держать свой: не зависеть от внешнего сервиса и его лимитов на скачивание, сканировать образы на уязвимости, ограничивать доступ и хранить рядом с кластером
-
Что такое базовый образ и как его выбирать? Ответ: Образ, указанный в
FROM, поверх которого строятся твои слои. Выбирают по трём критериям: минимальный размер, наличие обновлений безопасности и совместимость с твоим приложением. Практическое правило: официальный образ языка в варианте-slim, аalpine— только если проверил, что зависимости с ним работают -
Что происходит с данными, записанными приложением в контейнер, после его перезапуска? Ответ: После
restartиstop/startданные на месте — записываемый слой сохраняется. Послеrmи пересоздания контейнера они исчезают. Поэтому всё, что нельзя терять, монтируют в том -
Каким приложениям обязательно нужны тома? Ответ: Всем, кто хранит состояние: базам данных, очередям сообщений, хранилищам файлов, сервисам с загрузками пользователей. Приложению без состояния том не нужен — и это правильно спроектированное приложение, которое легко масштабировать копиями (тема 5)
-
Где лежат тома и логи Docker? Ответ: Тома — в
/var/lib/docker/volumes/, логи контейнеров — в/var/lib/docker/containers/<id>/<id>-json.log. Логам обязательно настраивают ротацию черезlog-opts(max-size,max-file), иначе один разговорчивый контейнер способен забить весь диск — это классическая авария -
Контейнер завершился с кодом 137. Что это значит? Ответ:
128 + 9, то есть процесс убит сигналом SIGKILL. Почти всегда это превышение лимита памяти — вdocker inspectбудет"OOMKilled": true. Реже — кто-то выполнилdocker killили приложение не успело завершиться за отведённоеdocker stopвремя. Код143(128 + 15) означает штатное завершение по SIGTERM -
В чём разница между формой списка и строковой формой у
CMDиENTRYPOINT? Ответ: Форма списка (CMD ["python", "app.py"]) запускает процесс напрямую, и он получает PID 1 и сигналы от Docker. Строковая форма (CMD python app.py) запускает всё через/bin/sh -c, и PID 1 достаётся оболочке, которая сигналы дальше не передаёт. Результат: приложение не получает SIGTERM, не завершается корректно и через 10 секунд убивается принудительно. Всегда используй форму списка -
Что такое проблема PID 1 в контейнере? Ответ: Процесс с PID 1 в Linux имеет особый статус: он обязан подбирать «осиротевшие» дочерние процессы, иначе те остаются зомби и копятся, съедая таблицу процессов. Обычные приложения этого не умеют. Если твоё приложение порождает потомков, запускай контейнер с флагом
--init(илиinit: trueв Compose) — он подставит крошечный корректный PID 1 -
Как уменьшить размер образа? Ответ: Многоэтапная сборка — в итог попадает только результат, без компилятора и исходников. Лёгкий базовый образ (
-slim,alpine,distroless). Объединение команд установки в одинRUNс очисткой кеша пакетов в той же строке — иначе удалённое останется в предыдущем слое. Полноценный.dockerignore. Проверить, что именно занимает место, помогаетdocker history -
Что такое distroless-образ и в чём сложность работы с ним? Ответ: Образ без оболочки, пакетного менеджера и утилит — только приложение и его зависимости. Плюсы: минимальный размер и очень маленькая поверхность атаки. Минус: внутрь не зайти через
docker exec ... sh, потому что оболочки нет. Отлаживают такие контейнеры временным подключением отладочного контейнера к тому же пространству имён -
Что такое контекст сборки и почему он бывает огромным? Ответ: Каталог, который клиент Docker целиком отправляет демону перед сборкой. Если собирать из каталога с
.git,node_modulesи логами, туда уедут сотни мегабайт, и сборка будет медленной ещё до первой инструкции. Лечится.dockerignore. Видно это в самом начале вывода:Sending build context to Docker daemon -
Как отлаживать контейнер, который сразу падает? Ответ:
docker logs имя— что он успел написать.docker ps -a— код выхода.docker inspect— причина завершения и был ли OOM. Если логов нет, запускают образ с заменой команды:docker run -it --entrypoint sh образ— попадёшь внутрь и сможешь выполнить команду руками и увидеть ошибку -
Чем
docker stopотличается отdocker kill? Ответ:stopпосылает SIGTERM, ждёт (по умолчанию 10 секунд) и только потом SIGKILL — у приложения есть шанс дописать данные и закрыть соединения.killсразу посылает SIGKILL. Время ожидания меняется флагом-t. Это ровно та же логика вежливого завершения, что и в теме 1 -
Чем
depends_onотличается отhealthcheckв Compose? Ответ:depends_onв простом виде управляет только порядком запуска: контейнер стартовал — значит, зависимость выполнена, хотя приложение внутри может ещё не подняться. Чтобы дождаться реальной готовности, у зависимости описываютhealthcheck, а вdepends_onуказываютcondition: service_healthy. Без этого связка приложение-база регулярно падает на старте -
Как контейнеры находят друг друга и почему
localhostне работает? Ответ: В пользовательской сети Docker поднимает DNS, и контейнеры доступны по именам сервисов.localhostвнутри контейнера означает его собственное сетевое пространство, а не хост и не соседний контейнер — поэтому обращение к базе поlocalhostне находит ничего. Обращаться нужно по имени сервиса; к самому хосту — поhost.docker.internalтам, где оно поддерживается -
Что такое dangling-образы и откуда они берутся? Ответ: Слои без тега, отображаемые как
<none>. Появляются, когда пересобираешь образ с тем же тегом: тег переезжает на новую сборку, а старая остаётся безымянной. Со временем они занимают десятки гигабайт. Удаляютсяdocker image prune; посмотреть общий расход —docker system df -
Почему опасно пробрасывать
/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 …
Типичные ошибки:
Cannot connect to the Docker daemon— сервис не запущен. На Linux:sudo systemctl start docker, на Windows/macOS — запусти Docker Desktoppermission denied while trying to connect— твой пользователь не в группе docker. Исправляетсяsudo usermod -aG docker $USERи перезаходом в систему. Учти: членство в этой группе равносильно правам root на машине
Задание 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?
Типичные ошибки:
docker pull nginxбез тега молча скачивает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
Типичные ошибки:
- Контейнер сразу переходит в
Exited (0)— это нормально для контейнеров, чья главная команда завершилась. Контейнер живёт ровно столько, сколько живёт процесс с PID 1 внутри него - Имя занято (
name is already in use) — старый контейнер не удалён, помогаетdocker rm имя
Задание 3. Свой образ
Цель: собрать собственный образ и понять кеш слоёв
Шаги:
-
Создай каталог и страницу:
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 -
Узнай, откуда nginx берёт файлы сайта:
docker run --rm nginx:1.27.3 cat /etc/nginx/conf.d/default.conf | grep rootФлаг
--rmудаляет временный контейнер сразу после выполнения команды -
Создай
Dockerfile:# Базовый образ, на котором строим свой FROM nginx:1.27.3 # Кладём свою страницу туда, где nginx её ищет COPY index.html /usr/share/nginx/html/ # Документируем порт: это подсказка для человека, порт всё равно нужно пробрасывать через -p EXPOSE 80 # CMD не нужен: базовый образ уже знает, как запускать nginx -
Собери и запусти:
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 -
Почувствуй кеш. Пересобери образ без изменений — сборка займёт доли секунды и все шаги будут помечены
CACHED. Теперь измениindex.htmlи пересобери снова: пересоберётся только слойCOPYdocker 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 напротив неизменившихся шагов
Типичные ошибки:
COPY failed: file not found— файл вне контекста сборки. Копировать можно только то, что лежит внутри каталога, указанного последним аргументомdocker build- Пересобрал образ, а в браузере старая страница — контейнер по-прежнему запущен из старого образа. Образ и контейнер — разные вещи: после пересборки контейнер нужно пересоздать
- Кеш не срабатывает вовсе — обычно потому, что копирование кода стоит слишком высоко в Dockerfile
Задание 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). Именно так база данных переживает обновление своего образа
Типичные ошибки:
- Монтировать bind mount в каталог, где у образа уже есть файлы — они окажутся скрыты содержимым каталога хоста. Это не ошибка Docker, а ожидаемое поведение
- Ждать, что данные сохранятся без тома. Нет:
docker 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 сделает ровно то же самое, только на масштабе кластера
Типичные ошибки:
depends_onгарантирует только порядок запуска, но не готовность. Контейнерwebможет быть запущен, а nginx внутри ещё не поднялся. Правильное решение — проверка готовности (healthcheck), пример ниже в задании 6- Отступы в YAML табами — Compose откажется читать файл
- Ключ
version: '3.8'в начале файла — устаревший, современный Compose на него ругается. Просто не пиши его
Задание 6. Сквозной проект: «Заметки» в контейнере
Цель: упаковать своё приложение в образ, поднять его вместе с nginx и базой и получить конфигурацию, которую можно запустить на любой машине одной командой
Шаги:
-
Перейди в репозиторий «Заметок» из темы 3:
cd ~/notes -
Создай
.dockerignore— что не должно попасть в образ:.git *.log notes.txt .env *.key *.crt __pycache__/ -
Создай
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"] -
Собери и проверь:
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 для этого больше не нужен -
Опиши весь стенд в
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: -
Создай конфиг 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; } } -
Запускай:
docker compose up -d --build docker compose ps # все три сервиса, notes и db со статусом healthy curl http://localhost:8080/ docker compose logs -f notes -
Проверь, что данные переживают контейнер:
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/ # заметка на месте -
Сломай специально. Убей приложение и посмотри, как связка отработает:
docker compose stop notes curl -i http://localhost:8080/ # 502 от nginx — знакомо по теме 2 docker compose start notes sleep 15 curl -i http://localhost:8080/ # снова 200 -
Закоммить результат — теперь стенд разворачивается из репозитория:
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 не теряет данные
Типичные ошибки:
permission denied: /data/notes.txt— забытchownпередUSER notes: приложение работает не от root и не может писать в каталог, принадлежащий root- Приложение вечно
startingв healthcheck — проверь команду проверки отдельно:docker compose exec notes python -c "..." docker compose upпересобирает не тот образ — при наличииbuild:иimage:одновременно образ собирается и получает указанное имя; чтобы точно пересобрать, нужен флаг--build- Пароль базы прямо в
compose.yml— да, так делать нельзя, и мы это исправим в теме 9. Пока хотя бы добавьcompose.override.ymlв.gitignoreи знай, что это временный компромисс
Полезные команды на каждый день
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 # справка
Проверь себя: тема освоена, если ты можешь
- Объяснить, чем контейнер отличается от виртуальной машины, не используя слово «легче»
- Написать Dockerfile с нуля и объяснить, почему инструкции стоят именно в таком порядке
- Объяснить, почему приложение в контейнере должно писать логи в консоль
- Сказать, где будут данные после
docker rm, и настроить том так, чтобы они не потерялись - Объяснить, почему в контейнере не работают от root и как это исправить
- Поднять стенд из нескольких сервисов через Compose и объяснить, как они находят друг друга
- Найти причину, по которой контейнер сразу переходит в
Exited