Перейти к содержимому

Лекция 6. Docker: контейнеризация


1. Проблема окружений

1.1. «У меня работает»

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

Типичные причины:

  • Разные версии языка/рантайма (Python 3.10 vs 3.12).
  • Отсутствие системных библиотек (libssl, libc разных версий).
  • Различия в ОС (macOS для разработки, Linux в продакшене).
  • Разные переменные окружения, конфигурации, пути к файлам.
  • «Забытые» зависимости, установленные вручную и не отражённые в документации.

1.2. Цепочка зависимостей

Любое приложение зависит от целой цепочки компонентов:

Код приложения
└── Зависимости (библиотеки, пакеты)
└── Рантайм (Python, Node.js, JVM)
└── Системные библиотеки (glibc, openssl)
└── Операционная система
└── Инфраструктура (железо / облако)

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


2. Виртуализация vs контейнеризация

2.1. Виртуальные машины (VM)

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

Типы гипервизоров:

ТипОписаниеПримеры
Type 1 (bare-metal)Устанавливается непосредственно на аппаратуру. Нет хостовой ОС. Высокая производительностьVMware ESXi, KVM, Microsoft Hyper-V, Xen
Type 2 (hosted)Работает как приложение поверх обычной ОС. Удобно для разработки и тестированияVirtualBox, VMware Workstation, Parallels
Type 1 (bare-metal) Type 2 (hosted)
┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
│ VM 1 │ │ VM 2 │ │ VM 3 │ │ VM 1 │ │ VM 2 │
│ Гост.│ │ Гост.│ │ Гост.│ │ Гост.│ │ Гост.│
│ ОС │ │ ОС │ │ ОС │ │ ОС │ │ ОС │
└──────┘ └──────┘ └──────┘ └──────┘ └──────┘
┌─────────────────────────┐ ┌─────────────────┐
│ Гипервизор │ │ Гипервизор │
└─────────────────────────┘ └─────────────────┘
┌─────────────────────────┐ ┌─────────────────┐
│ Аппаратура │ │ Хостовая ОС │
└─────────────────────────┘ └─────────────────┘
┌─────────────────┐
│ Аппаратура │
└─────────────────┘

2.2. Контейнеры

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

┌────────┐ ┌────────┐ ┌────────┐
│Контейн.│ │Контейн.│ │Контейн.│
│ App A │ │ App B │ │ App C │
│ + deps│ │ + deps│ │ + deps│
└────────┘ └────────┘ └────────┘
┌────────────────────────────────┐
│ Container Runtime │
│ (Docker Engine) │
└────────────────────────────────┘
┌────────────────────────────────┐
│ Операционная система │
│ (ядро Linux) │
└────────────────────────────────┘
┌────────────────────────────────┐
│ Аппаратура │
└────────────────────────────────┘

2.3. Сравнение VM и контейнеров

ХарактеристикаВиртуальная машинаКонтейнер
ИзоляцияПолная (отдельное ядро ОС)На уровне процессов (общее ядро)
Размер образаГигабайты (включает гостевую ОС)Мегабайты (только приложение + deps)
Время запускаМинутыСекунды (или доли секунды)
OverheadВысокий (гипервизор + гостевая ОС)Минимальный
ПлотностьДесятки VM на серверСотни/тысячи контейнеров на сервер
ПереносимостьОграничена (зависит от гипервизора)Высокая (работает везде, где есть Docker)
БезопасностьБолее сильная изоляцияИзоляция слабее (общее ядро)
ПрименениеРазные ОС, строгая изоляция, legacyМикросервисы, CI/CD, масштабирование

3. Технологии ядра Linux для контейнеризации

Контейнеры — это не «магия Docker». Они построены на фундаментальных механизмах ядра Linux.

3.1. Namespaces (пространства имён)

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

NamespaceЧто изолируетОписание
PIDПроцессыКонтейнер видит только свои процессы. PID 1 в контейнере — не init хоста
NETСетевой стекСобственные сетевые интерфейсы, IP-адреса, таблицы маршрутизации, порты
MNTФайловая системаСобственное дерево точек монтирования
UTSHostnameКонтейнер имеет собственное имя хоста
IPCМежпроцессное взаимодействиеИзоляция очередей сообщений, семафоров, разделяемой памяти
USERПользователиМаппинг UID/GID: root в контейнере ≠ root на хосте
Окно терминала
# Просмотр namespaces процесса
ls -la /proc/$$/ns/
# Запуск процесса в новом namespace (пример)
unshare --pid --fork --mount-proc bash

3.2. cgroups (Control Groups)

cgroups — механизм ядра Linux для ограничения и учёта ресурсов, потребляемых группой процессов.

РесурсОписание
CPUОграничение времени процессора (cpu.shares, cpu.cfs_quota_us)
MemoryЛимит оперативной памяти (memory.limit_in_bytes)
Block I/OОграничение скорости чтения/записи на диск
NetworkПриоритизация и ограничение сетевого трафика
PIDsМаксимальное количество процессов
Окно терминала
# Запуск контейнера с ограничением ресурсов
docker run --memory=512m --cpus=1.5 nginx

3.3. Union FS (OverlayFS)

Union File System — слоёная (layered) файловая система, позволяющая объединять несколько каталогов в один. Docker использует OverlayFS для создания файловых систем контейнеров.

┌──────────────────────────────┐
│ Writable layer │ ← Контейнер (изменения)
│ (container layer) │
├──────────────────────────────┤
│ Layer 3: COPY app.py │ ← Только для чтения
├──────────────────────────────┤
│ Layer 2: RUN pip install │ ← Только для чтения
├──────────────────────────────┤
│ Layer 1: FROM python:3.12│ ← Базовый образ (только для чтения)
└──────────────────────────────┘

Преимущества слоёной ФС:

  • Слои разделяются между образами и контейнерами — экономия дискового пространства.
  • Слои кэшируются — повторная сборка быстрее, если предыдущие слои не изменились.
  • Принцип copy-on-write — данные копируются в верхний слой только при модификации.

4. Docker: архитектура

4.1. Компоненты Docker

Docker использует клиент-серверную архитектуру:

┌──────────────┐ REST API ┌──────────────────┐
│ Docker CLI │ ◀────────────────────▶ │ Docker Daemon │
│ (docker) │ (Unix socket / │ (dockerd) │
└──────────────┘ TCP) │ │
│ ┌────────────┐ │
┌──────────────┐ │ │ Контейнеры │ │
│ Docker │ ◀── pull / push ──▶ │ ├────────────┤ │
│ Compose │ │ │ Образы │ │
└──────────────┘ │ ├────────────┤ │
│ │ Сети │ │
│ ├────────────┤ │
│ │ Volumes │ │
│ └────────────┘ │
└────────┬─────────┘
┌────────▼─────────┐
│ Docker Registry │
│ (Docker Hub) │
└──────────────────┘
КомпонентОписание
Docker CLI (docker)Командная строка для взаимодействия с Docker. Отправляет команды демону через REST API
Docker Daemon (dockerd)Фоновый процесс, управляющий контейнерами, образами, сетями, томами
Docker RegistryХранилище образов. Docker Hub — публичный реестр по умолчанию. Существуют и приватные реестры (GitHub Container Registry, GitLab CR, AWS ECR)

4.2. Взаимодействие компонентов

  1. Пользователь вводит команду: docker run nginx.
  2. Docker CLI отправляет запрос Docker Daemon.
  3. Daemon проверяет, есть ли образ nginx локально. Если нет — скачивает из Docker Hub (docker pull).
  4. Daemon создаёт контейнер из образа и запускает его.

5. Образы (Images)

5.1. Что такое Docker-образ

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

5.2. Слои и кэширование

docker build:
FROM ubuntu:22.04 → Layer 1 (скачивается из Hub, кэшируется)
RUN apt-get update → Layer 2 (кэшируется)
RUN apt-get install -y python3 → Layer 3 (кэшируется)
COPY app.py /app/ → Layer 4 (пересобирается при изменении app.py)
CMD ["python3", "/app/app.py"] → Layer 5

При повторной сборке Docker использует кэш: если инструкция и контекст не изменились, слой берётся из кэша. Поэтому порядок инструкций в Dockerfile влияет на скорость сборки: часто меняющиеся инструкции размещайте в конце.

5.3. Базовые образы

ОбразРазмерОписание
ubuntu~78 MBПолноценная Ubuntu (apt, bash, стандартные утилиты)
alpine~7 MBМинимальный дистрибутив Linux (musl, busybox). Популярен для продакшена
python:3.12~1 GBPython с полным набором инструментов
python:3.12-slim~150 MBPython без лишних пакетов
python:3.12-alpine~50 MBPython на базе Alpine
node:20~1 GBNode.js с полным набором
node:20-alpine~130 MBNode.js на базе Alpine
nginx:alpine~40 MBWeb-сервер Nginx на базе Alpine

5.4. Docker Hub

Docker Hub (hub.docker.com) — крупнейший публичный реестр Docker-образов.

Окно терминала
# Поиск образов
docker search python
# Скачивание образа
docker pull python:3.12-slim
# Просмотр локальных образов
docker images

Именование образов: [registry/]repository[:tag]

  • python:3.12 — образ python с тегом 3.12 из Docker Hub.
  • ghcr.io/owner/app:v1.0 — образ из GitHub Container Registry.
  • Без тега используется latest (не рекомендуется в продакшене — неопределённая версия).

6. Контейнеры

6.1. Что такое контейнер

Контейнер — это запущенный экземпляр образа. Поверх read-only слоёв образа добавляется тонкий writable layer (запись-при-копировании), в который попадают все изменения, произведённые во время работы контейнера.

Образ (read-only) Контейнер
┌────────────────┐ ┌────────────────┐
│ Layer 3 │ │ Writable layer │ ← изменения
├────────────────┤ ├────────────────┤
│ Layer 2 │ │ Layer 3 │
├────────────────┤ ├────────────────┤
│ Layer 1 │ │ Layer 2 │
└────────────────┘ ├────────────────┤
│ Layer 1 │
└────────────────┘

6.2. Жизненный цикл контейнера

docker create docker start docker pause
┌──────────┐ ┌───────────┐ ┌──────────┐
│ Created │ ────▶ │ Running │ ────▶ │ Paused │
└──────────┘ └─────┬─────┘ └────┬─────┘
│ │
docker stop docker unpause
│ │
┌─────▼─────┐ │
│ Stopped │ ◀───────────┘
└─────┬─────┘
docker rm
┌─────▼─────┐
│ Removed │
└───────────┘
СостояниеОписание
CreatedКонтейнер создан, но не запущен
RunningКонтейнер работает, процессы выполняются
PausedПроцессы приостановлены (SIGSTOP через cgroups freezer)
StoppedКонтейнер остановлен, процессы завершены, writable layer сохранён
RemovedКонтейнер удалён, все данные (writable layer) потеряны

7. Dockerfile

Dockerfile — текстовый файл с инструкциями для сборки Docker-образа. Каждая инструкция создаёт новый слой.

7.1. Основные инструкции

FROM — базовый образ

# Всегда первая инструкция (кроме ARG)
FROM python:3.12-slim

Определяет базовый образ, на основе которого строится новый. FROM scratch — пустой образ (для статически скомпилированных бинарников).

RUN — выполнение команд при сборке

# Каждый RUN создаёт новый слой
RUN apt-get update && apt-get install -y \
gcc \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*

Выполняет команду в процессе сборки. Результат фиксируется как новый слой. Объединяйте команды через && для минимизации слоёв.

COPY / ADD — копирование файлов

# COPY — копирование файлов из контекста сборки
COPY requirements.txt .
COPY src/ /app/src/
# ADD — то же, но с дополнительными возможностями:
# - распаковка архивов (.tar, .gz)
# - загрузка по URL (не рекомендуется)
ADD archive.tar.gz /app/

Рекомендация: используйте COPY в большинстве случаев. ADD — только когда нужна автораспаковка архивов.

WORKDIR — рабочая директория

WORKDIR /app
# Все последующие команды выполняются в /app
# Если директория не существует — она будет создана

ENV — переменные окружения

ENV PYTHONUNBUFFERED=1
ENV APP_PORT=8000

Переменные окружения доступны и при сборке, и при запуске контейнера.

EXPOSE — документирование портов

EXPOSE 8000

Не открывает порт! Это лишь документация — указание, что приложение в контейнере слушает данный порт. Для фактического проброса порта используется флаг -p при docker run.

CMD vs ENTRYPOINT

# CMD — команда по умолчанию (можно переопределить при запуске)
CMD ["python", "app.py"]
# ENTRYPOINT — фиксированная команда (аргументы добавляются к ней)
ENTRYPOINT ["python", "app.py"]
# Комбинация: ENTRYPOINT + CMD (CMD задаёт аргументы по умолчанию)
ENTRYPOINT ["python"]
CMD ["app.py"]
АспектCMDENTRYPOINT
НазначениеКоманда по умолчаниюОсновная команда контейнера
Переопределениеdocker run image <cmd> заменяет CMDdocker run image <args> добавляет аргументы к ENTRYPOINT
Типичное использованиеЗапуск приложения с возможностью заменыКонтейнер как исполняемый файл

ARG — build-time переменные

ARG PYTHON_VERSION=3.12
FROM python:${PYTHON_VERSION}-slim
ARG APP_VERSION=1.0.0
RUN echo "Building version ${APP_VERSION}"

ARG доступны только при сборке (в отличие от ENV, которые доступны и при запуске).

7.2. Пример: Dockerfile для Python Flask приложения

# Базовый образ
FROM python:3.12-slim
# Метаданные
LABEL maintainer="dev@example.com"
LABEL version="1.0"
# Переменные окружения
ENV PYTHONUNBUFFERED=1
ENV FLASK_APP=app.py
# Рабочая директория
WORKDIR /app
# Сначала копируем зависимости (для кэширования слоёв)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Затем копируем исходный код
COPY . .
# Документируем порт
EXPOSE 5000
# Создаём непривилегированного пользователя
RUN adduser --disabled-password --gecos "" appuser
USER appuser
# Команда запуска
CMD ["flask", "run", "--host=0.0.0.0"]

8. Лучшие практики Dockerfile

8.1. Минимизация слоёв

Объединяйте связанные команды RUN в одну инструкцию:

# Плохо — 3 слоя
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
# Хорошо — 1 слой
RUN apt-get update && apt-get install -y curl \
&& rm -rf /var/lib/apt/lists/*

8.2. Использование .dockerignore

Файл .dockerignore исключает файлы из контекста сборки (аналогично .gitignore):

.git
.gitignore
node_modules
__pycache__
*.pyc
.env
.venv
docker-compose*.yml
README.md

8.3. Multi-stage builds

Многоэтапная сборка позволяет использовать один образ для компиляции и другой (минимальный) для запуска. Финальный образ не содержит инструментов сборки.

Пример: сборка Go-приложения:

# === Этап 1: сборка ===
FROM golang:1.22 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o server .
# === Этап 2: финальный образ ===
FROM alpine:3.19
RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY --from=builder /app/server .
EXPOSE 8080
CMD ["./server"]

Результат: финальный образ — десятки МБ вместо сотен (не содержит Go SDK).

8.4. Другие рекомендации

  • Не запускать от root — используйте USER для переключения на непривилегированного пользователя.
  • Фиксировать версииFROM python:3.12.3-slim, а не FROM python:latest.
  • Располагать редко меняющиеся инструкции выше — для эффективного использования кэша.
  • Использовать minimal base imagesalpine, slim, distroless.
  • Не хранить секреты в образе — не использовать ENV для паролей, передавать через --env или Docker secrets.

9. Основные команды Docker

9.1. Сборка образа

Окно терминала
# Сборка из текущей директории (ищет Dockerfile)
docker build -t myapp:1.0 .
# Сборка с указанием Dockerfile
docker build -f Dockerfile.prod -t myapp:prod .
# Сборка без кэша
docker build --no-cache -t myapp:1.0 .

9.2. Запуск контейнера — docker run

Окно терминала
docker run [OPTIONS] IMAGE [COMMAND]
ФлагОписаниеПример
-dЗапуск в фоновом режиме (detached)docker run -d nginx
-pПроброс портов (хост:контейнер)docker run -p 8080:80 nginx
-vМонтирование тома/директорииdocker run -v ./data:/app/data nginx
--nameИмя контейнераdocker run --name web nginx
--rmУдалить контейнер после остановкиdocker run --rm nginx
-itИнтерактивный режим + терминалdocker run -it ubuntu bash
-eПеременная окруженияdocker run -e DB_HOST=db nginx
--networkПодключение к сетиdocker run --network mynet nginx

Примеры:

Окно терминала
# Запуск Nginx с пробросом порта
docker run -d --name web -p 8080:80 nginx
# Запуск PostgreSQL с переменными окружения и томом
docker run -d --name postgres \
-e POSTGRES_USER=admin \
-e POSTGRES_PASSWORD=secret \
-e POSTGRES_DB=mydb \
-v pgdata:/var/lib/postgresql/data \
-p 5432:5432 \
postgres:16
# Интерактивный запуск Ubuntu
docker run -it --rm ubuntu:22.04 bash

9.3. Управление контейнерами

Окно терминала
# Список запущенных контейнеров
docker ps
# Все контейнеры (включая остановленные)
docker ps -a
# Логи контейнера
docker logs web
docker logs -f web # следить в реальном времени (follow)
# Выполнить команду в работающем контейнере
docker exec -it web bash
docker exec web cat /etc/nginx/nginx.conf
# Остановить / запустить / перезапустить
docker stop web
docker start web
docker restart web
# Удалить контейнер
docker rm web
docker rm -f web # принудительное удаление (даже запущенного)

9.4. Управление образами

Окно терминала
# Список локальных образов
docker images
# Удалить образ
docker rmi nginx:latest
# Скачать образ
docker pull python:3.12-slim
# Отправить образ в реестр
docker push myregistry.com/myapp:1.0
# Удалить неиспользуемые образы
docker image prune

10. Volumes и bind mounts

10.1. Проблема: данные в контейнере эфемерны

При удалении контейнера все данные в его writable layer теряются. Для сохранения данных между перезапусками используется персистентное хранение.

10.2. Типы монтирования

ТипОписаниеКоманда
VolumeУправляется Docker. Хранится в /var/lib/docker/volumes/. Рекомендуемый способ-v myvolume:/data
Bind mountМонтирование конкретной директории хоста в контейнер-v /host/path:/container/path
tmpfs mountХранение в оперативной памяти. Данные не сохраняются при остановке--tmpfs /tmp

10.3. Работа с Docker Volumes

Окно терминала
# Создать том
docker volume create mydata
# Список томов
docker volume ls
# Информация о томе
docker volume inspect mydata
# Удалить том
docker volume rm mydata
# Удалить неиспользуемые тома
docker volume prune

10.4. Сравнение типов монтирования

ХарактеристикаVolumeBind mounttmpfs
УправлениеDockerПользовательЯдро ОС
Расположение/var/lib/docker/volumes/Любой путь на хостеRAM
ПерсистентностьДаДаНет (до остановки)
ПортируемостьВысокая (не зависит от структуры хоста)Низкая (привязка к пути)
ПроизводительностьХорошаяЗависит от хост-ФСМаксимальная
ПрименениеБазы данных, данные приложенийИсходный код при разработке, конфигиСекреты, кэш

Пример: разработка с bind mount:

Окно терминала
# Исходный код монтируется в контейнер — изменения видны сразу
docker run -d -p 5000:5000 \
-v $(pwd)/src:/app/src \
myapp:dev

11. Сети Docker

11.1. Типы сетей

Тип сетиОписание
bridgeСеть по умолчанию. Контейнеры могут общаться между собой по IP. Изолированы от хоста
hostКонтейнер использует сетевой стек хоста напрямую. Нет изоляции сети, но максимальная производительность
noneКонтейнер полностью изолирован от сети

11.2. Пользовательские сети (user-defined bridge)

По умолчанию контейнеры подключаются к сети bridge. Однако пользовательские сети предоставляют важное преимущество — автоматический DNS: контейнеры могут обращаться друг к другу по имени.

Окно терминала
# Создать пользовательскую сеть
docker network create mynet
# Запустить контейнеры в одной сети
docker run -d --name db --network mynet postgres:16
docker run -d --name app --network mynet \
-e DB_HOST=db \ # обращение по имени контейнера!
myapp:1.0
# Список сетей
docker network ls
# Информация о сети
docker network inspect mynet
# Удалить сеть
docker network rm mynet

11.3. DNS внутри Docker-сети

В пользовательской bridge-сети Docker запускает встроенный DNS-сервер (127.0.0.11). Контейнеры могут обращаться друг к другу по имени:

┌──────────────────── mynet ────────────────────────┐
│ │
│ ┌──────────┐ ┌──────────────┐ │
│ │ db │ ◀──────── │ app │ │
│ │ postgres │ db:5432 │ "DB_HOST=db"│ │
│ │ :5432 │ │ :8000 │ │
│ └──────────┘ └──────────────┘ │
│ │
│ Docker DNS: db → 172.18.0.2 │
└────────────────────────────────────────────────────┘

11.4. Port mapping (проброс портов)

Контейнеры по умолчанию не доступны с хоста. Для доступа используется проброс портов флагом -p:

Окно терминала
# Формат: -p <хост_порт>:<контейнер_порт>
docker run -d -p 8080:80 nginx # localhost:8080 → контейнер:80
docker run -d -p 127.0.0.1:3000:3000 app # только с localhost
docker run -d -p 80 nginx # случайный порт хоста → 80
# Просмотр маппинга портов
docker port <container_name>

12. Вопросы для самопроверки

  1. В чём суть проблемы окружений и как контейнеризация её решает?
  2. Чем контейнеры отличаются от виртуальных машин? Назовите преимущества и недостатки каждого подхода.
  3. Какова роль namespaces и cgroups в контейнеризации? Что изолирует каждый namespace?
  4. Опишите архитектуру Docker. Как взаимодействуют Docker CLI, Docker Daemon и Docker Registry?
  5. Что такое Docker-образ? Как устроена слоёная файловая система и зачем нужно кэширование слоёв?
  6. Опишите жизненный цикл Docker-контейнера. Какие состояния он проходит?
  7. Объясните назначение каждой инструкции Dockerfile: FROM, RUN, COPY, WORKDIR, ENV, EXPOSE, CMD, ENTRYPOINT. В чём разница между CMD и ENTRYPOINT?
  8. Что такое multi-stage build? Приведите пример, когда он полезен.
  9. Чем отличаются Docker volumes, bind mounts и tmpfs? Когда использовать каждый тип?
  10. Какие типы сетей поддерживает Docker? Почему пользовательские bridge-сети предпочтительнее сети по умолчанию?