TL;DR: Поднял локально полный аналитический стек — реляционная БД, OLAP‑аналитика и векторный поиск для RAG. Всё в одном compose‑файле, данные — на томах WSL, ноль облачных счетов. Под катом — установка за три шага и три ловушки, о которых молчат гайды.

Зачем ещё один «поднять базы в Docker»?

Для пет‑проектов на стыке классического SQL, аналитики и поиска по эмбеддингам нужен сразу целый зоопарк: реляционная БД для транзакций, колоночная — для аналитики, векторная — для семантического поиска. Облачные managed‑решения для такого экспериментатора — стрельба из пушки по воробьям: счёт капает даже на простаивающих инстансах.

Поэтому альтернатива: один docker-compose.yml, три контейнера, данные на томах WSL. Состав стека:

Сервис

Версия

Роль в стеке

PostgreSQL

18

Реляционные данные, история диалогов, статистика

ClickHouse

26.8

OLAP: аналитические запросы по большим таблицам

Qdrant

1.19

Векторное хранилище для RAG и семантического поиска

Всё это — часть моего проекта, где я собираю локальный AI‑стек без облаков. Следующие статьи серии — про локального AI‑репетитора английского на Ollama и Telegram‑боте, это — фундамент для его данных.

Установка (3 шага)

Создаём папку:

bashmkdir -p ~/nrk_data_stack/db_stack
cd ~/nrk_data_stack/db_stack
mkdir -p ~/nrk_data_stack/db_stack
cd ~/nrk_data_stack/db_stack

Кладём рядом два файла из репозитория — .env и docker-compose-db.yml:

👉 GitHub db_stack

Что внутри compose‑файла

PostgreSQL: healthcheck и правильный restart

yamlhealthcheck:
  test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
  interval: 10s
  timeout: 5s
  retries: 5
  start_period: 30s
restart: unless-stopped
healthcheck:
  test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
  interval: 10s
  timeout: 5s
  retries: 5
  start_period: 30s
restart: unless-stopped

Без healthcheck Docker считает контейнер «запущенным» сразу после старта процесса — даже если PostgreSQL ещё не принимает соединения. pg_isready честно проверяет готовность именно нашей базы, а start_period: 30s даёт базе время на первичную инициализацию: в этот окно неудачные проверки не съедают лимит retries. Это критично, если другие сервисы стека ждут БД через depends_on: condition: service_healthy.

И важная мелочь: именно restart: unless-stopped, а не restart_policy — работает только в Swarm‑стеке, в обычном compose она молча игнорируется. (Проверка: в вашем случае — или «молча ничего не делала», или явно ругалась на unknown key.)

Публикация порта сделана через "${POSTGRES_PORT}:5432" — распространённая ошибка в гайдах: написать просто ${POSTGRES_PORT}. В таком виде значение‑число из .env публикует порт с тем же номером на хосте, а значение вида 5433 без двоеточия вовсе не публикует ничего — compose ждёт формат хост:контейнер.

ClickHouse: ulimits и два порта

yamlulimits:
  nofile:
    soft: 262144
    hard: 262144
ulimits:
  nofile:
    soft: 262144
    hard: 262144

ClickHouse активно работает с открытыми файлами, и дефолтный лимит Docker (1024) на реальных нагрузках кончается быстро — с трудно диагностичными ошибками вида «Too many open files». nofile: 262144 — рекомендованное самим ClickHouse значение.

Портов два, и это не дублирование: 8123 — HTTP‑интерфейс (им на него ходят веб‑консоль /play, драйверы HTTP, curl), 9000 — нативный протокол, по которому работает консольный clickhouse-client и многие клиентские библиотеки. Забыть пробросить 9000 — значит получить работающий дашборд и неработающего клиента.

Qdrant: gRPC и где живут данные

yamlports:
  - "${QDRANT_API_PORT}:6333"   # REST API и веб-дашборд
  - "${QDRANT_GRPC_PORT}:6334"  # gRPC — нужен Python-клиенту!
ports:
  - "${QDRANT_API_PORT}:6333"   # REST API и веб-дашборд
  - "${QDRANT_GRPC_PORT}:6334"  # gRPC — нужен Python-клиенту!

Официальный Python‑клиент Qdrant по умолчанию ходит через gRPC на порту 6334. Если пробросить только 6333 — REST и дашборд будут работать, а клиент упадёт с загадочным connection error. Второй порт не опция, а необходимость для Python.

С томами здесь тонкость, специфичная для WSL:

yamlvolumes:
  - ${QDRANT_VOLUME_PATH}:/qdrant/storage
volumes:
  - ${QDRANT_VOLUME_PATH}:/qdrant/storage

Папка лежит внутри файловой системы Ubuntu (ext4), поэтому bind mount безопасен. А вот bind mount с путей /mnt/c, /mnt/f (диски Windows) или \wsl.localhost со стороны Windows — известная мина: медленный I/O через 9P‑протокол и реальный риск потери данных при записи из Linux на файловую систему Windows. Все «живые» данные стека поэтому храним внутри WSL, а Windows‑диски используем только для статики.

Запускаем:

bashdocker compose -f docker-compose-db.yml up -d
docker compose -f docker-compose-db.yml up -d

Проверка, что всё живо

bash# Статус контейнеров (postgres должен быть healthy)
docker compose -f docker-compose-db.yml ps

# PostgreSQL
docker exec -it nrk-postgres psql -U nrk -d nrk_db -c "SELECT version();"

# ClickHouse
docker exec -it nrk-clickhouse clickhouse-client \
  --user nrk --password "$CLICKHOUSE_PASSWORD" \
  --query "SELECT version()"

# Qdrant (с API-ключом из .env)
curl http://localhost:6333/collections -H "api-key: ваш_ключ"
# Статус контейнеров (postgres должен быть healthy)
docker compose -f docker-compose-db.yml ps

# PostgreSQL
docker exec -it nrk-postgres psql -U nrk -d nrk_db -c "SELECT version();"

# ClickHouse
docker exec -it nrk-clickhouse clickhouse-client \
  --user nrk --password "$CLICKHOUSE_PASSWORD" \
  --query "SELECT version()"

# Qdrant (с API-ключом из .env)
curl http://localhost:6333/collections -H "api-key: ваш_ключ"

Веб‑интерфейсы из коробки:

Три ловушки, о которых молчат гайды

Ловушка 1. PostgreSQL 18: путь данных изменился

В официальных образах PostgreSQL 18 расположение каталога данных сместилось: теперь это /var/lib/postgresql, а не /var/lib/postgresql/data, как в образах прежних версий. Скопированная из старого гайда строка монтирования с /data на конце выглядит рабочей — контейнер стартует, база отвечает. Но том монтируется мимо реального PGDATA: запишете данные, пересоздадите контейнер — и обнаружите пустую базу. Молча, без единой ошибки в логах. Самое противное в этой ловушке — она не стреляет сразу: «успешно работает» до первого пересоздания контейнера. Монтируйте /var/lib/postgresql.

Ловушка 2. ClickHouse: одиночный файл конфига как мина замедленного действия

Классический паттерн «смонтирую config.xml в контейнер» ломается двумя способами. Если файла нет на хосте, Docker создаст вместо него пустую директорию с именем config.xml — и сервер упадёт при старте с невнятной ошибкой. Если файл есть, но не соответствует ожиданиям образа — упадёт уже по‑другому. Надёжный путь: не монтировать основной config.xml, а класть свои переопределения в config.d/ и users.d/ — ClickHouse подхватит их поверх дефолтов. А если всё‑таки монтируете целиком — сначала достаньте дефолтный конфиг из образа и правьте его на хосте.

Ловушка 3. Qdrant: защиты через ADMIN_USER/PASSWORD не существует

В гайдах по Docker‑стекам часто мелькает конструкция с переменными вроде QDRANT__SERVICE__ADMIN_USER — копирующие её получают веб‑дашборд и API без какой‑либо аутентификации, а иллюзию защищённости. У Qdrant авторизация одна: QDRANT__SERVICE__API_KEY (двойные подчёркивания — это вложенность секций конфига: service.api_key). Ключ задаётся в .env, клиент обязан передавать его в каждом запросе через заголовок api-key. Проверить, что защита работает, просто: запрос к /collections без заголовка должен вернуть 403.

Заключение

Три контейнера, один compose‑файл — и полноценный полигон для данных: SQL, OLAP и векторный поиск работают локально, пока вы спите, а облачный счётчик — нет. Из следующего шага проекта: подключение стека к AI‑репетитору в телеграмм на Ollama.

Ссылки: