DevOps
Модуль 2: Containerization



Модуль 2: Containerization
Контейнеризация — это стандарт упаковки приложений. Мы упаковываем код и все его зависимости (Node.js/Python runtime, системные библиотеки, конфиги) в единый Image. Это решает проблему «на моем ноутбуке работает, а на продакшене нет».

Быстрые определения:

  • Image — шаблон (слои файловой системы + метаданные).
  • Container — запущенный процесс (ы) из image с изолированными неймспейсами.
  • Registry — хранилище образов (Docker Hub, GHCR, ECR и т. д.).
Необходимый инструментарий
  • Docker Desktop или Docker Engine (на Linux).
  • Docker Hub Account (опционально, для пуша образов).
Полезно иметь:
  • docker compose (сейчас это часть Docker CLI; отдельный docker-compose почти не нужен)
Часть 1. Жизненный цикл контейнера
2.1 Запуск (docker run)
Самая важная команда с кучей флагов.
docker run -d --name my-nginx -p 8080:80 -v $(pwd):/usr/share/nginx/html nginx:stable-alpine
Разбор флагов:

  • -d (Detached): Запустить в фоне. Если не указать, логи посыпятся в консоль, и при закрытии терминала контейнер умрет.
  • --name: Имя для удобного обращения (docker stop my-nginx).
  • -p 8080:80 (Port Mapping): «Возьми порт 80 внутри контейнера и приклей его к порту 8080 на моем маке/сервере».
  • -v (Volume Binding): Монтирование файлов. «Замени папку html внутри на мою текущую папку». Это позволяет править файл на хосте, а Nginx внутри сразу увидит изменения.

На что обращать внимание:

  • Для одноразовых запусков удобно добавлять --rm (контейнер удалится после остановки).
  • В проде старайтесь не использовать: latest: лучше pin по версии или даже по digest (см. ниже).
2.2 Управление
  • docker ps: Что запущено прямо сейчас?
  • docker logs -f my-nginx: Читать логи (как tail -f).
  • docker exec -it my-nginx bash: «Зайти» внутрь контейнера (запустить баш-сессию внутри неймспейса).
  • Внутри можно сделать ls, ps, поправить конфиг (для дебага).

Ещё команды, которые реально используются каждый день:

  • docker inspect <container> - посмотреть IP, mounts, env, entrypoint.
  • docker stats — CPU/RAM/NET IO (быстрый «пульс»).
  • docker top <container> - какие процессы внутри.
  • Важно: в Alpine часто нет bash, используйте sh:
docker exec -it my-nginx sh
Часть 2. Сети и volumes (как контейнеры общаются и где живут данные)
2.3 Как работает networking по умолчанию
По умолчанию контейнер попадает в bridge-сеть Docker. Это означает:

  • контейнер имеет свой IP в виртуальной сети,
  • исходящие соединения (наружу) обычно работают «сразу» (NAT на хосте),
  • входящие требуют -p host: container.

Практика: посмотреть сети и сетевую привязку контейнера.
docker network ls
docker inspect my-nginx | grep -n "Network" | head
2.4 Volumes vs bind-mount: что выбирать
  • bind-mount (-v $(pwd):/app) — удобно для разработки.
  • named volume (-v pgdata:/var/lib/postgresql/data) — лучше для данных (Docker управляет местом хранения).
Практика: найти, где физически лежит named volume.
docker volume ls
docker volume inspect pgdata
Часть 3. Сборка образов (Dockerfile)
Dockerfile — это инструкция по сборке.

Правило № 1: слои — это кеш. Если меняется один файл — пересобирается только часть.
2.5 .dockerignore (быстрое ускорение сборки)
Без .dockerignore вы часто отправляете в build context лишнее: .git/, node_modules/, тестовые данные.

Минимальный пример:
.git
.venv
__pycache__
node_modules
dist
*.log
2.6 Пример оптимизированного Dockerfile (Python)
# 1. Base Image. Используем slim-версии (меньше мусора). Проверяйте актуальную версию на hub.docker.com
FROM python:3.12-slim

# 2. Переменные окружения для Python
ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

# 3. Рабочая директория
WORKDIR /app

# 4. Сначала копируем зависимости (Layer Caching!)
COPY requirements.txt .

# 5. Ставим пакеты
RUN pip install --no-cache-dir -r requirements.txt

# 6. Только теперь копируем остальной код
COPY . .

# 7. Указываем пользователя (Security - не запускать от root)
RUN useradd -m appuser
USER appuser

# 8. Точка входа
CMD ["python", "app.py"]
Пояснения «вживую»:
  • COPY requirements.txt. перед COPY. . — это самый простой способ получить кеш зависимостей.
  • --no-cache-dir уменьшает финальный размер слоя.
  • Пользователь не root — базовая гигиена. Иногда нужно, но это должно быть осознанно.
Сборка:
docker build -t my-app:v1 .
Проверить размер и слои:
docker images | head
docker history my-app:v1
2.7 CMD vs ENTRYPOINT (частый источник «почему не запускается»)
  • CMD — дефолтная команда, которую легко переопределить при docker run … <cmd>.
  • ENTRYPOINT — «основной» исполняемый файл (обычно не переопределяют целиком).

Для большинства учебных примеров достаточно CMD.
Часть 4. Multi-stage Build (Golang Пример)
Как сделать образ размером 10MB, если для компиляции нужен гигабайтный компилятор? Использовать два этапа.
# Stage 1: Build
FROM golang:1.23-alpine AS builder
WORKDIR /src
COPY . .
# Собираем статический бинарник
RUN go build -o myapp main.go

# Stage 2: Runtime
FROM alpine:latest
WORKDIR /root/
# Копируем ТОЛЬКО бинарник из первого стейджа
COPY --from=builder /src/myapp .

CMD ["./myapp"]
Пара практичных улучшений:
  • Если приложению нужен HTTPS, в минимальном рантайме может понадобиться CA bundle (ca-certificates).
  • В финальном образе лучше указывать конкретный tag (и идеальнее — digest), а не: latest.
Часть 5. Docker Compose: воспроизводимая среда из нескольких сервисов
Обычно в реальной жизни у вас не один контейнер, а набор: приложение + база + кэш + брокер.
2.8 Мини-лаба: app + postgres через docker compose
Создайте compose. yaml рядом с вашим проектом:
services:
    db:
        image: postgres:16
        environment:
            POSTGRES_PASSWORD: secret
            POSTGRES_DB: app
        volumes:
            - pgdata:/var/lib/postgresql/data
        healthcheck:
            test: ["CMD-SHELL", "pg_isready -U postgres -d app"]
            interval: 5s
            timeout: 3s
            retries: 10

    app:
        image: nginx:stable-alpine
        ports:
            - "8080:80"
        depends_on:
            db:
                condition: service_healthy

volumes:
    pgdata:
Команды:
docker compose up -d
docker compose ps
docker compose logs -f --tail=100
docker compose down
Что здесь важно:
  • depends_on не ждёт готовности по умолчанию, поэтому мы добавили healthcheck и условие.
  • Compose сам создаёт сеть, и сервисы общаются по DNS-именам (db, app).
2.9 Environment variables и secrets (минимум понимания)
  • environment: удобно, но старайтесь не хранить пароли прямо в git.
  • Для локальной разработки используйте .env (и добавляйте в. gitignore).
  • В проде обычно применяют секрет-хранилища (Vault/SSM/Secrets Manager/K8s secrets) — это будет дальше по курсу.
Часть 6. Образы, теги и digests (как сделать деплой предсказуемым)
Тег (:v1, :stable) — это «указатель». Он может переехать.

Digest (@sha256:…) — это конкретный неизменяемый образ.

Как посмотреть digest:
docker buildx imagetools inspect nginx:stable-alpine | head -n 40
Как pin-ить:
nginx:stable-alpine@sha256:<digest>
Часть 7. Минимальная безопасность контейнеров (без фанатизма)
Быстрые правила:
  • Не запускайте приложение от root, если можно иначе.
  • Не добавляйте лишние пакеты «на всякий случай».
  • Минимизируйте права и поверхность атаки: меньше процессов, меньше утилит.
  • Если контейнеру не нужно писать на диск — старайтесь сделать FS только для чтения (дальше пригодится в Compose/K8s).
Практика (посмотреть пользователя):
docker exec -it my-nginx id
Troubleshooting Guide
1. Exited (137) OOMKilled: Контейнер съел всю память.
  • Диагностика: docker inspect my-container | grep OOMKilled.
  • Решение: Оптимизировать приложение или добавить лимиты --memory="1g".
2. "Connection Refused" при запросе к localhost: Приложение внутри слушает 127.0.0.1. Внутри контейнера localhost - это сам контейнер. Снаружи до него не добраться.
  • Решение: Приложение должно слушать 0.0.0.0.
3. Контейнер сразу падает:
  • docker ps -a - увидите статус Exited.
  • docker logs <id> - читайте последнюю строку. Часто там Panic, Syntax Error или Config not found.
4. No space left on device (диск кончился из-за Docker):
  • Проверка:
docker system df
  • Аккуратная чистка неиспользуемого:
docker system prune
# если понимаете последствия:
docker system prune -a
5. Не получается зайти внутрь (bash not found): В минимальных образах часто нет bash.
Решение: используйте sh (или ставьте bash осознанно).

6. Сборка неожиданно медленная: Часто виноват огромный build context.
Решение: добавьте .dockerignore (см. выше) и проверьте, что вы не копируете лишнее.
Итоговая задача: "Stateful App"
Понять, как работают данные в контейнерах.
Задача:

  1. Запустить Postgres в контейнере.
docker run -d --name db -e POSTGRES_PASSWORD=secret -v pgdata:/var/lib/postgresql/data postgres
2. Зайти внутрь, создать таблицу, добавить запись.
docker exec -it db psql -U postgres
# CREATE TABLE test (id int); INSERT INTO test VALUES (1);
3. Убить контейнер: docker rm -f db.
4. Запустить новый с тем же вольюмом (-v pgdata:/...).
5. Проверить, что данные (таблица test) сохранились. Это доказывает, что данные живут отдельно от контейнера.
Дополнительная практика: «Собери и запусти своё приложение в Compose»
1. Возьмите простой сервис (можно на Python):
cat > app.py <<'PY'
from http.server import BaseHTTPRequestHandler, HTTPServer

class H(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.send_header("Content-Type", "text/plain")
        self.end_headers()
        self.wfile.write(b"ok\n")

HTTPServer(("0.0.0.0", 8000), H).serve_forever()
PY
2. Напишите Dockerfile (можно взять структуру из примера выше).

3. Сделайте compose.yaml, который:

  • собирает образ app через build: .
  • публикует порт 8000:8000
4. Проверьте:
docker compose up -d --build
curl -v http://127.0.0.1:8000
docker compose logs -f --tail=50
Цель: пройти полный цикл «код → image → compose → диагностика».