Docker и деплой

⏱ 45 мин · 🧩 заданий: 4

Зачем контейнеры

«У меня работает» — главная боль деплоя: другая версия Python, другие системные библиотеки, забытая переменная окружения. Контейнер упаковывает приложение вместе со всем окружением (Python, библиотеки, системные пакеты) в образ, который одинаково запускается на ноутбуке, в CI и на сервере.

Виртуальная машина Контейнер ┌──────────┐ ┌──────────┐ ┌────────┐ ┌────────┐ │ App │ │ App │ │ App │ │ App │ │ Libs │ │ Libs │ │ Libs │ │ Libs │ │ Гостевая │ │ Гостевая │ └────────┘ └────────┘ │ ОС │ │ ОС │ ┌─────────────────────┐ └──────────┘ └──────────┘ │ Docker (общее ядро) │ ┌──────────────────────┐ ├─────────────────────┤ │ Гипервизор │ │ ОС хоста │ └──────────────────────┘ └─────────────────────┘ гигабайты, минуты на старт мегабайты, секунды на старт

Контейнер — это обычный процесс Linux, изолированный механизмами ядра: namespaces (свои процессы, сеть, файловая система) и cgroups (лимиты CPU и памяти). Своего ядра у него нет, поэтому он лёгкий.

  • Образ (image) — неизменяемый шаблон из слоёв: python:3.12-slim, postgres:16.
  • Контейнер — запущенный экземпляр образа со своим записываемым слоем.
  • Реестр (registry) — хранилище образов: Docker Hub, GitHub Container Registry, приватный.

Dockerfile для Python-приложения

# syntax=docker/dockerfile:1
FROM python:3.12-slim

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PIP_NO_CACHE_DIR=1

# системные зависимости одним слоем, без кеша apt
RUN apt-get update \
    && apt-get install -y --no-install-recommends libpq5 \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

# сначала только зависимости — этот слой кешируется, пока requirements.txt не меняется
COPY requirements.txt .
RUN pip install -r requirements.txt

# потом код — меняется часто
COPY . .

# не запускаем приложение от root
RUN useradd --create-home app
USER app

EXPOSE 8000
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "3"]
Инструкция Что делает
FROM базовый образ; указывайте точный тег (3.12-slim), не latest
RUN выполнить команду при сборке → новый слой
COPY скопировать файлы из контекста сборки (ADD умеет распаковывать архивы и качать URL — обычно не нужен)
WORKDIR текущая папка
ENV переменные окружения
USER от чьего имени запускать
EXPOSE документирует порт (не публикует его)
CMD / ENTRYPOINT команда запуска контейнера

PYTHONUNBUFFERED=1 — чтобы print и логи сразу шли в stdout контейнера, а не копились в буфере.

Слои и кеш

Каждая инструкция создаёт слой. При пересборке Docker берёт слой из кеша, если инструкция и её входные файлы не менялись, — до первого изменения, после него всё пересобирается.

import hashlib

def build(dockerfile, files, cache):
    parent = ""
    rebuilt = []
    for instr in dockerfile:
        inputs = ""
        if instr.startswith("COPY"):
            src = instr.split()[1]
            inputs = "".join(v for k, v in sorted(files.items()) if src == "." or k == src)
        key = hashlib.sha256((parent + instr + inputs).encode()).hexdigest()
        rebuilt.append(("кеш " if key in cache else "СБОРКА ") + instr)
        cache.add(key)
        parent = key
    return rebuilt

good = ["FROM python:3.12-slim", "COPY requirements.txt .", "RUN pip install -r requirements.txt", "COPY . ."]
bad = ["FROM python:3.12-slim", "COPY . .", "RUN pip install -r requirements.txt"]
files = {"requirements.txt": "django==5.2", "app.py": "print('v1')"}

for name, df in (("правильный порядок", good), ("COPY . . в начале", bad)):
    cache = set()
    build(df, files, cache)
    changed = {**files, "app.py": "print('v2')"}         # поменяли одну строку кода
    print(name)
    print("\n".join("  " + line for line in build(df, changed, cache)))

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

.dockerignore

Всё, что не нужно внутри образа, исключают — сборка быстрее, образ меньше, секреты не утекают:

.git
.venv
__pycache__/
*.pyc
.pytest_cache/
.env
node_modules/
media/

Многоэтапная сборка

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

FROM python:3.12-slim AS builder
RUN apt-get update && apt-get install -y --no-install-recommends build-essential libpq-dev \
    && rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip wheel --wheel-dir /wheels -r requirements.txt

FROM python:3.12-slim
RUN apt-get update && apt-get install -y --no-install-recommends libpq5 && rm -rf /var/lib/apt/lists/*
COPY --from=builder /wheels /wheels
RUN pip install --no-cache-dir /wheels/* && rm -rf /wheels
WORKDIR /app
COPY . .
USER nobody
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

Основные команды

docker build -t shop-api:1.4.0 .              # собрать образ из Dockerfile в текущей папке
docker images                                 # список образов
docker run -d --name api -p 8000:8000 --env-file .env shop-api:1.4.0   # запустить в фоне
docker ps                                     # запущенные контейнеры (-a — все)
docker logs -f api                            # логи (stdout/stderr контейнера)
docker exec -it api bash                      # зайти внутрь работающего контейнера
docker stop api && docker rm api
docker run --rm -it python:3.12 python        # одноразовый контейнер с REPL
docker system prune                           # почистить неиспользуемое
docker push ghcr.io/team/shop-api:1.4.0       # отправить в реестр

-p 8000:8000 — «порт хоста : порт контейнера». Без него сервис внутри недоступен снаружи.

Тома: данные вне контейнера

Контейнер одноразовый: при удалении его файлы пропадают. Данные, которые нужно сохранить, хранят в томах:

  • volume — том, которым управляет Docker (-v pgdata:/var/lib/postgresql/data) — для баз данных;
  • bind mount — папка хоста (-v $(pwd):/app) — для разработки с горячей перезагрузкой.

docker compose

Описание нескольких сервисов в одном файле compose.yaml:

services:
  web:
    build: .
    command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
    ports: ["8000:8000"]
    env_file: .env
    environment:
      DATABASE_URL: postgresql+psycopg://shop:shop@db:5432/shop   # имя сервиса = имя хоста
      REDIS_URL: redis://redis:6379/0
    volumes: [".:/app"]
    depends_on:
      db: {condition: service_healthy}
      redis: {condition: service_started}

  db:
    image: postgres:16
    environment: {POSTGRES_USER: shop, POSTGRES_PASSWORD: shop, POSTGRES_DB: shop}
    volumes: ["pgdata:/var/lib/postgresql/data"]
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U shop"]
      interval: 5s
      retries: 10

  redis:
    image: redis:7-alpine

  worker:
    build: .
    command: celery -A app.worker worker -l info
    env_file: .env
    depends_on: [db, redis]

volumes:
  pgdata:
docker compose up -d                 # поднять всё в фоне
docker compose logs -f web
docker compose exec web alembic upgrade head    # команда внутри сервиса
docker compose down                  # остановить (с -v — удалить и тома)

Сервисы в одном compose видят друг друга по имени сервиса: из web база доступна как db:5432, а не localhost.

Python в продакшене

  • WSGI (Django, Flask) — gunicorn с несколькими воркерами-процессами; ASGI (FastAPI, async Django) — uvicorn или gunicorn -k uvicorn.workers.UvicornWorker. runserver / --reload — только для разработки.
  • Число воркеров: стартовая точка 2 × CPU + 1 для синхронных, затем по замерам.
  • Перед приложением — обратный прокси (nginx, Traefik, облачный балансировщик): TLS, статика, сжатие, лимиты, раздача на несколько экземпляров.
  • Логи — в stdout/stderr, их собирает платформа (Docker, Kubernetes, Loki, ELK). Не пишите в файлы внутри контейнера.
  • Health check — эндпоинт /health (жив ли процесс) и /ready (готов ли принимать трафик: есть ли связь с базой).
  • Миграции — отдельным шагом деплоя (alembic upgrade head, manage.py migrate) до переключения трафика. Делайте их обратно совместимыми: старая версия кода должна работать с новой схемой, пока идёт выкатка.
  • Graceful shutdown: по SIGTERM дообработать текущие запросы и выйти.

CI/CD

# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env: {POSTGRES_PASSWORD: test}
        ports: ["5432:5432"]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: {python-version: "3.12", cache: pip}
      - run: pip install -r requirements.txt -r requirements-dev.txt
      - run: ruff check . && ruff format --check .
      - run: mypy app
      - run: pytest --cov=app
        env: {DATABASE_URL: "postgresql://postgres:test@localhost:5432/postgres"}
  deploy:
    needs: test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: docker build -t ghcr.io/${{ github.repository }}:${{ github.sha }} .
      # push в реестр и выкатка: kubectl / helm / ssh + docker compose pull && up -d

CI (continuous integration) — на каждый push: линтеры, типы, тесты. CD (continuous delivery/deployment) — сборка образа с тегом коммита и автоматическая выкатка. Стратегии выкатки: rolling (по одному экземпляру), blue-green (две копии окружения, переключение трафика), canary (сначала на небольшой процент пользователей).

12-factor и Kubernetes в двух словах

12-factor app — принципы приложения, удобного для облака: конфигурация в переменных окружения, процессы без состояния (сессии и файлы — во внешних сервисах), логи как поток в stdout, одинаковые dev и prod, быстрый старт и корректное завершение.

Kubernetes управляет контейнерами на кластере машин: Pod — один или несколько контейнеров; Deployment держит нужное число реплик и делает rolling update; Service — стабильный адрес и балансировка; Ingress — вход HTTP снаружи; ConfigMap/Secret — конфигурация; liveness/readiness-пробы — те самые /health и /ready. Для бэкенд-разработчика важно понимать эти слова и уметь читать манифест; администрирование кластера — обычно задача DevOps.

Итог

  • Образ = слои; порядок инструкций определяет кеш: зависимости раньше кода.
  • Точные теги, slim-образ, .dockerignore, многоэтапная сборка, USER не root, PYTHONUNBUFFERED=1.
  • compose описывает приложение с базой и кешем; сервисы видят друг друга по имени.
  • В продакшене: gunicorn/uvicorn за обратным прокси, логи в stdout, health check, миграции отдельным шагом, CI/CD.

Практика решено 0 из 4

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Программа на Python

Линтер Dockerfile. Напишите lint_dockerfile(text), возвращающую отсортированный список кодов найденных проблем:

  • "latest" — FROM без тега или с тегом latest (FROM python, FROM python:latest);
  • "add" — используется ADD (для обычных файлов нужен COPY);
  • "apt-cache" — в строке RUN есть apt-get install, но в ней же нет rm -rf /var/lib/apt/lists;
  • "cache-order" — COPY . . (или COPY . <куда угодно>) встречается раньше, чем RUN pip install (тогда любое изменение кода сбрасывает кеш установки зависимостей);
  • "root" — нет ни одной инструкции USER (контейнер работает от root).

Инструкции регистронезависимы, пустые строки и комментарии # пропускайте. Многострочные RUN с \ в задании не встречаются.

Какие тесты проверяют код (3)
good = """
# хороший Dockerfile
FROM python:3.12-slim
RUN apt-get update && apt-get install -y libpq5 && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
USER app
CMD ["gunicorn", "config.wsgi"]
"""
assert lint_dockerfile(good) == []

bad = """
from python
add . /app
workdir /app
copy . .
run pip install -r requirements.txt
run apt-get install -y curl
cmd python main.py
"""
assert lint_dockerfile(bad) == ["add", "apt-cache", "cache-order", "latest", "root"]

assert lint_dockerfile("FROM python:latest\nUSER app") == ["latest"]
assert lint_dockerfile("FROM ghcr.io/org/base:1.2\nUSER app") == []
assert lint_dockerfile("FROM localhost:5000/python\nUSER app") == ["latest"]
assert lint_dockerfile("FROM python:3.12\nRUN pip install x\nCOPY . /srv\nUSER app") == []

Задание 2 Один вариант ответа

Почему в Dockerfile сначала копируют requirements.txt и ставят зависимости, а только потом копируют весь код?

Задание 3 Заполнить таблицу

Что использовать?

volumebind mountпеременная окруженияслой образа
Данные PostgreSQL, которые должны пережить пересоздание контейнера
Код на машине разработчика, чтобы изменения сразу были видны в контейнере
Пароль к базе для продакшена
Установленные Python-зависимости
Отметьте ячейку — «+», пустая — «−».
Задание 4 Ввод ответа

Какой командой docker compose поднимают все сервисы в фоне? (без слова docker)

Войдите, чтобы отмечать пройденные уроки.