Урок 2. Docker и сервисы

🎯 Цели урока

📖 Теория: раздел «Контейнеры и Apple Silicon».

Как устроен make lab

┌────────────── macOS ───────────────────────────────────────────┐
│  Ollama (Metal GPU) :11434        браузер → localhost:8888      │
│        ▲                                   │                    │
│        │ host.docker.internal              ▼                    │
│  ┌──── Linux VM (Docker Desktop) ────────────────────────────┐  │
│  │  контейнер lab: uv + JupyterLab                           │  │
│  │    /course      ← весь репозиторий (volume, правки видны) │  │
│  │    /venvs/M00   ← окружение модуля (отдельный том)        │  │
│  │  контейнеры сервисов: qdrant, postgres, …  (по запросу)   │  │
│  └───────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────┘

make lab M=00 делает три вещи:

  1. собирает (один раз) образ mlcourse/runtime: Python 3.12, uv и системные утилиты;
  2. запускает контейнер, монтируя репозиторий в /course;
  3. внутри выполняет uv sync для модуля и стартует JupyterLab на порту 8888.

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

Где мы сейчас: в контейнере или на хосте?

import os

import mlcourse
from mlcourse import in_docker

print("Внутри Docker:", in_docker())
print(mlcourse.describe_settings())
Внутри Docker: False
main   qwen3.8-27b  https://<удалённый сервер>/v1
local  qwen3:8b     http://localhost:11434/v1
embed  bge-m3       http://localhost:11434/v1
кэш    record       LLM_CACHE

Внутри контейнера localhost — это сам контейнер, а Ollama работает на Mac. Поэтому библиотека курса в контейнере автоматически заменяет localhost на host.docker.internal. Проверим это, притворившись контейнером:

from mlcourse import load_settings

os.environ["MLCOURSE_IN_DOCKER"] = "1"
print("Как видит контейнер:", load_settings().local.base_url)
del os.environ["MLCOURSE_IN_DOCKER"]
print("Как видит хост:     ", load_settings().local.base_url)
Как видит контейнер: http://host.docker.internal:11434/v1
Как видит хост:      http://localhost:11434/v1

Достучимся до локальной модели

Эндпоинт GET /v1/models есть у любого OpenAI-совместимого сервера — удобная проверка связи. get_http_client("local") возвращает обычный httpx.Client, уже настроенный на адрес Ollama (и с кассетами — поэтому ячейка выполняется и без запущенной Ollama).

from mlcourse import get_http_client

with get_http_client("local") as http:
    response = http.get("models")

print("HTTP", response.status_code, "| из кассеты:", response.headers.get("x-mlcourse-cassette") == "hit")
for model in response.json()["data"]:
    print(" •", model["id"])
HTTP 200 | из кассеты: False
 • bge-m3:latest
 • qwen3:8b

Если ячейка падает с ошибкой соединения — Ollama не запущена. Запустите её с нужным размером контекста: make ollama (это OLLAMA_CONTEXT_LENGTH=16384 ollama serve).

Сервисы курса

Кроме JupyterLab, в infra/compose/compose.yml описаны сервисы, которые понадобятся в следующих модулях. Они поднимаются по требованию через профили compose:

Сервис Зачем Где понадобится
Qdrant векторная база данных M11–M13, M15, M17
Postgres + pgvector реляционная БД с векторами, хранилище состояния агентов M11, M16–M17, M24
Redis очереди и кэш M17, M24

Команды (в корне репозитория):

make services S="qdrant postgres"   # поднять
docker compose -f infra/compose/compose.yml ps
make services-down                  # остановить все

Данные сервисов хранятся в именованных томах Docker и переживают перезапуск контейнеров.

✅ Итоги