
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_stackcd ~/nrk_data_stack/db_stack
mkdir -p ~/nrk_data_stack/db_stackcd ~/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: 30srestart: unless-stopped
healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] interval: 10s timeout: 5s retries: 5 start_period: 30srestart: 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# PostgreSQLdocker exec -it nrk-postgres psql -U nrk -d nrk_db -c "SELECT version();"# ClickHousedocker 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# PostgreSQLdocker exec -it nrk-postgres psql -U nrk -d nrk_db -c "SELECT version();"# ClickHousedocker 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: ваш_ключ"
Веб‑интерфейсы из коробки:
-
ClickHouse →
http://localhost:8123/play -
Qdrant →
http://localhost:6333/dashboard
Три ловушки, о которых молчат гайды
Ловушка 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.
Ссылки:
-
Репозиторий со всеми файлами: GitHub nrk_data_stack
-
Видеоразбор: 3 БД в Docker
-
Telegram‑канал: NRK DATA STACK
Источник: habr.com