MAATRIX / Блог / Elasticsearch в Docker Compose: готовый файл

Elasticsearch в Docker Compose: готовый файл

MAATRIX

Elasticsearch — стандарт полнотекстового поиска и аналитики, основа ELK-стека, и один из самых капризных сервисов для первого запуска: не тот vm.max_map_count — контейнер падает при старте, не те лимиты памяти — падает под нагрузкой, забыли про security — открытый кластер находят боты за часы. Ниже рабочий compose-файл с уже пройденными граблями.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →

Что нужно узнать до запуска

Elasticsearch написан на Java и требует JVM с выделенной памятью отдельно от файлового кеша ОС. Прежде чем писать compose-файл, определитесь с тремя вещами.

Память. Минимум для теста — 2 ГБ RAM на контейнер, для рабочей нагрузки — от 4 ГБ, для прод-кластера с несколькими индексами и активной записью — от 8 ГБ. Правило Elastic: heap JVM (-Xms/-Xmx) не должен превышать 50% RAM контейнера и не должен превышать примерно 31 ГБ — после этой границы JVM теряет оптимизацию compressed oops на указателях объектов, и прирост от увеличения heap падает непропорционально. Вторую половину памяти Elasticsearch использует под файловый кеш Lucene-индексов на чтение — отдав всю RAM под heap, вы замедлите поиск, а не ускорите.

Диск. Индексы обычно занимают на диске больше, чем исходные данные — за счёт инвертированных индексов, doc values и реплик. Для логов и текста закладывайте 1.5–3x от объёма сырых данных. SSD обязателен: Elasticsearch активно использует случайный доступ при поиске и merge сегментов.

vm.max_map_count. Lucene, на котором построен Elasticsearch, использует mmap для файлов индекса и упирается в лимит ядра на число memory-mapped областей на процесс. Без правки этого параметра контейнер либо не стартует, либо падает через несколько минут с ошибкой max virtual memory areas vm.max_map_count [65530] is too low.

Настройка хоста перед стартом

Правим параметр ядра на хосте — разово, не в compose-файле:

# разово, до перезагрузки
sudo sysctl -w vm.max_map_count=262144

# навсегда
echo "vm.max_map_count=262144" | sudo tee -a /etc/sysctl.d/99-elasticsearch.conf
sudo sysctl --system

Второй момент — swap. Elasticsearch не любит, когда JVM-процесс уходит в swap: GC-паузы становятся непредсказуемыми, кластер может пометить себя как unhealthy. Это решает флаг bootstrap.memory_lock внутри контейнера, но для его работы хосту нужно снять лимит на locked memory. Если на сервере уже настроен swap для других задач — см. статью про правильный размер swap — для Elasticsearch правильнее исключить его через memory lock, как показано ниже, а не выключать целиком.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Готовый docker-compose.yml

Однопиточный (single-node) конфиг для теста, разработки или небольшого прод-инстанса без отказоустойчивости:

services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.15.0
    container_name: elasticsearch
    restart: unless-stopped
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=true
      - xpack.security.http.ssl.enabled=false
      - ELASTIC_PASSWORD=${ELASTIC_PASSWORD}
      - ES_JAVA_OPTS=-Xms2g -Xmx2g
      - bootstrap.memory_lock=true
    ulimits:
      memlock:
        soft: -1
        hard: -1
      nofile:
        soft: 65536
        hard: 65536
    mem_limit: 4g
    volumes:
      - es_data:/usr/share/elasticsearch/data
    ports:
      - "127.0.0.1:9200:9200"
    healthcheck:
      test: ["CMD-SHELL", "curl -su elastic:${ELASTIC_PASSWORD} -f http://localhost:9200/_cluster/health || exit 1"]
      interval: 20s
      timeout: 10s
      retries: 5
      start_period: 60s
    networks:
      - es-net

volumes:
  es_data:

networks:
  es-net:
    driver: bridge

Версия образа здесь указана 8.15.0 для примера — перед запуском сверьте актуальный тег на странице Docker-образов Elastic и зафиксируйте конкретную версию, не latest: так обновление кластера произойдёт только когда вы сами этого захотите.

Ключевые решения в этом файле: ES_JAVA_OPTS=-Xms2g -Xmx2g — heap ровно половина от mem_limit: 4g, Xms и Xmx равны, чтобы JVM не тратила время на resize heap; discovery.type=single-node — без этого Elasticsearch ждёт кворум других узлов и не выходит из bootstrap; 127.0.0.1:9200:9200 — порт не торчит наружу, доступ снаружи только через реверс-прокси с TLS; ulimits.memlock: -1 вместе с bootstrap.memory_lock=true запрещает хипу уходить в swap.

Файл .env рядом с compose:

ELASTIC_PASSWORD=замените-на-длинный-случайный-пароль

Создать пароль можно так: openssl rand -base64 24.

Подключаем Kibana

Без Kibana Elasticsearch остаётся API без визуального интерфейса — для отладки запросов, построения дашбордов и просмотра индексов Kibana почти всегда нужна рядом. Добавьте сервис в тот же compose-файл:

  kibana:
    image: docker.elastic.co/kibana/kibana:8.15.0
    container_name: kibana
    restart: unless-stopped
    depends_on:
      elasticsearch:
        condition: service_healthy
    environment:
      - ELASTICSEARCH_HOSTS=http://elasticsearch:9200
      - ELASTICSEARCH_USERNAME=kibana_system
      - ELASTICSEARCH_PASSWORD=${KIBANA_PASSWORD}
    mem_limit: 1g
    ports:
      - "127.0.0.1:5601:5601"
    networks:
      - es-net

Версия образа Kibana должна совпадать с версией Elasticsearch — разнобой минорных версий официально не поддерживается и может ломать API-совместимость. Пароль для встроенного системного пользователя kibana_system задаётся отдельно после первого старта:

docker exec -it elasticsearch bin/elasticsearch-reset-password -u kibana_system -i

Введённый пароль впишите в .env как KIBANA_PASSWORD и перезапустите сервис Kibana.

Как и с Elasticsearch, порт 5601 пробрасывается только на localhost — открывать Kibana напрямую в интернет не стоит: у неё нет собственной защиты от брутфорса и DDoS. Правильная схема — Kibana за реверс-прокси; если ещё не настраивали такую связку, в статье про Nginx как реверс-прокси разобраны типичные грабли именно этой конфигурации — таймауты вебсокетов, кеш заголовков и SSL-редиректы, которые Kibana как SPA-приложение подсвечивает особенно наглядно.

Security: пароли, TLS и закрытые порты

Начиная с версии 8.x встроенная security (X-Pack Security) включена по умолчанию, и без пароля кластер вообще не запустится — это хорошо: раньше многие поднимали Elasticsearch без аутентификации и получали открытые на весь интернет базы, регулярно попадающие в подборки утечек.

В конфиге выше xpack.security.http.ssl.enabled=false — TLS между клиентом и Elasticsearch не поднят, это допустимо только потому, что порт 9200 не выходит за пределы 127.0.0.1, а трафик до Kibana идёт внутри изолированной docker-сети es-net, не покидая хост. Если Elasticsearch должен принимать соединения снаружи хоста, TLS обязателен — через встроенный xpack.security.http.ssl либо через реверс-прокси с собственным сертификатом.

Базовые правила: никогда не публикуйте 9200/5601 напрямую в 0.0.0.0, только 127.0.0.1 или внутренняя docker-сеть; пароль elastic храните в .env, а .env — в .gitignore, не в compose-файле; для приложений создавайте отдельного пользователя с ролью, ограниченной конкретными индексами, а не используйте elastic в коде. Если контейнер общий с другими сервисами на хосте, изоляция через docker — не панацея; общие принципы разобраны в статье про изоляцию сервисов через Docker.

Тома, снапшоты и перенос данных

Volume es_data в примере выше — именованный docker-volume, который живёт независимо от контейнера и переживает docker compose down (но не docker compose down -v). Для прод-инстанса этого недостаточно — нужны снапшоты: снапшот Elasticsearch консистентен по кластеру и умеет инкрементальные обновления, а простое копирование файлов тома — нет.

Настройка snapshot-репозитория на файловую систему (проще всего для одного узла) — добавьте в environment сервиса elasticsearch строку path.repo=/usr/share/elasticsearch/snapshots и смонтируйте директорию для снапшотов рядом с томом данных:

    volumes:
      - es_data:/usr/share/elasticsearch/data
      - ./snapshots:/usr/share/elasticsearch/snapshots

После перезапуска регистрируем репозиторий и создаём снапшот:

curl -u elastic:${ELASTIC_PASSWORD} -X PUT "localhost:9200/_snapshot/backup_repo" \
  -H "Content-Type: application/json" -d '{
  "type": "fs",
  "settings": { "location": "/usr/share/elasticsearch/snapshots" }
}'

curl -u elastic:${ELASTIC_PASSWORD} -X PUT "localhost:9200/_snapshot/backup_repo/snapshot_1?wait_for_completion=true"

Папку ./snapshots дальше можно синхронизировать rclone или restic на внешнее хранилище — конфиги для обоих есть в статьях про rclone в Docker Compose и restic в Docker Compose. Если позже понадобится перенести весь стек на другой сервер, общий подход к переносу docker-проекта с volumes описан в статье про перенос docker-проекта на другой сервер — для Elasticsearch важно поставить на новом сервере ту же или более новую версию: откат данных на старую версию не поддерживается.

Ресурсы, лимиты и типичные ошибки при старте

Ориентир по памяти в зависимости от нагрузки (именно ориентир — у вас будет зависеть от структуры документов, количества полей и реплик):

СценарийRAM на контейнерHeap (-Xmx)Диск
Тест / разработка, < 1 млн документов2 ГБ1g10–20 ГБ
Небольшой прод, поиск по сайту/каталогу4 ГБ2gот 50 ГБ
ELK для логов, активная запись8–16 ГБ4–8gот 200 ГБ, растёт
Аналитика на больших объёмах32 ГБ+~16g (не выше ~31g)по объёму данных

Если нагрузка ближе к верхним строчкам — стоит прикинуть требования к серверу целиком, а не только к контейнеру; общий подход разобран в статье про распределение ресурсов Docker между контейнерами.

Частые причины падения контейнера при первом запуске:

  • max virtual memory areas vm.max_map_count [65530] is too low — не выставлен vm.max_map_count=262144 на хосте.
  • Контейнер перезапускается по кругу без явной ошибки в логах — почти всегда mem_limit контейнера меньше, чем нужно JVM с заданным -Xmx плюс overhead процесса; поднимите mem_limit минимум до 2x от -Xmx.
  • AccessDeniedException на файлах в data — некорректные права на директорию тома при биндмаунте вместо named volume: образ работает от непривилегированного пользователя elasticsearch (uid 1000), директория на хосте должна быть доступна этому uid.
  • Кластер уходит в yellow/red после рестарта — на single-node это нормально для индексов с number_of_replicas: 1 по умолчанию (реплике некуда деться при одном узле); выставляйте number_of_replicas: 0 в шаблоне индекса.

Быстрая диагностика:

curl -su elastic:${ELASTIC_PASSWORD} http://localhost:9200/_cluster/health?pretty
docker logs elasticsearch --tail 100
docker stats elasticsearch --no-stream

Если docker stats показывает, что контейнер регулярно упирается в лимит памяти — см. статью что делать при нехватке RAM: для Elasticsearch типовое решение не «добавить swap» (ухудшит задержки поиска), а поднять mem_limit или вынести сервис на отдельный сервер.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Elasticsearch или OpenSearch — что выбрать для нового проекта?

OpenSearch — открытый форк от AWS, лицензия Apache 2.0 без ограничений, API почти идентичен на базовом уровне. Если критична полная open-source-лицензия — смотрите установку OpenSearch на VPS. Если важна экосистема и совместимость с существующими Kibana-дашбордами — Elasticsearch остаётся стандартом.

Можно ли запустить Elasticsearch без Kibana?

Да, все операции доступны через REST API на порту 9200. Kibana нужна только для визуального анализа и дашбордов — если приложение общается с Elasticsearch напрямую, можно не разворачивать её вовсе и сэкономить гигабайт RAM.

Почему heap ограничен ~31 ГБ, а не всей доступной памятью?

Особенность JVM: до порога размера heap около 30–32 ГБ указатели на объекты сжимаются (compressed oops) и занимают 4 байта вместо 8. После порога сжатие отключается, указатели становятся вдвое больше, и увеличение heap выше этой границы часто даёт меньше пользы, чем кажется.

Как обновить Elasticsearch без потери данных?

Через снапшот перед обновлением — обязательный шаг, а не подстраховка: меняете тег образа в compose-файле, docker compose up -d, данные в volume подхватятся автоматически. Откат на старую версию после апгрейда официально не поддерживается, поэтому свежий снапшот — единственный путь назад.

Нужен ли кластер из нескольких узлов для небольшого проекта?

Нет. Single-node конфигурация из этой статьи полностью рабочая для одного сервера — несколько узлов имеют смысл, когда объём данных или требования по аптайму это оправдывают, а не по умолчанию.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →