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_stackmkdir -p ~/nrk_data_stack/db_stack
cd ~/nrk_data_stack/db_stackКладём рядом два файла из репозитория — .env и docker-compose-db.yml:
Что внутри 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-stoppedhealthcheck:
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/storagevolumes:
- ${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 -ddocker 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: ваш_ключ"Веб‑интерфейсы из коробки:
ClickHouse →
http://localhost:8123/playQdrant →
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

Комментарии 0