NATS в Docker Compose: готовый файл
Как только в проекте появляется больше двух сервисов, которым нужно обмениваться событиями, начинается выбор между RabbitMQ, Kafka и десятком других брокеров — и почти всегда это выбор избыточно тяжёлого инструмента под простую задачу. NATS решает ту же проблему compose-файлом на 15 строк, бинарником размером 15-20 МБ и задержками, которых обычно не замечаешь на фоне сети. Ниже — рабочая конфигурация от простого pub/sub до кластера с персистентностью и мониторингом.
Содержание
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →Что такое NATS и когда он уместен
NATS — это message broker, написанный на Go командой, которая позже создала CNCF-проект Synadia. В базовом режиме (Core NATS) он держит сообщения только в памяти и ничего не гарантирует, кроме доставки «at most once»: подписчик не в сети — сообщение потеряно. Это осознанный компромисс ради скорости и простоты, а не недоработка.
Для случаев, когда нужна персистентность, повторная доставка и очереди с подтверждением, есть JetStream — надстройка над тем же протоколом, встроенная в тот же бинарник с версии 2.2. Не нужно поднимать отдельный кластер Kafka или ZooKeeper — JetStream включается одним флагом и хранит данные на диске рядом.
Где NATS обычно выигрывает у альтернатив:
- микросервисная коммуникация внутри одного датацентра или VPC — request-reply с таймаутами из коробки, без городить HTTP-клиенты с ретраями;
- event bus для десятков-сотен сервисов — subject-based маршрутизация (
orders.created,orders.*,orders.>) заменяет очереди и exchange'и RabbitMQ; - IoT и edge — низкое потребление памяти (десятки МБ на процесс) и способность работать в кластере с нестабильной связью через leaf nodes.
Где не стоит: если нужна сложная маршрутизация с трансформацией сообщений (тут ближе RabbitMQ с его exchange-типами) или террабайтные топики с долгим ретеншеном и consumer group semantics один-в-один как в Kafka — для такой нагрузки Kafka спроектирован лучше.
Минимальный docker-compose.yml для Core NATS
Для старта — pub/sub без персистентности, с открытым HTTP-портом мониторинга:
services:
nats:
image: nats:2.10-alpine
container_name: nats
restart: unless-stopped
command: "-js -m 8222"
ports:
- "4222:4222" # клиентский протокол
- "8222:8222" # HTTP monitoring
volumes:
- nats_data:/data
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8222/healthz"]
interval: 10s
timeout: 3s
retries: 5
volumes:
nats_data:
Флаг -js уже включает JetStream (без него сервер работает в чистом Core-режиме и данные /data не пишет — том можно убрать). -m 8222 открывает HTTP-эндпоинт для мониторинга и health-check.
Проверка, что брокер поднялся и отвечает:
docker compose up -d
curl -s http://localhost:8222/varz | head -c 300
Если в ответе пришёл JSON с полем server_id — сервер живой. Для быстрой проверки pub/sub без написания кода удобно использовать natscli (ставится отдельно, brew install nats-io/nats-tools/nats или бинарник с GitHub):
nats sub "orders.>" &
nats pub orders.created '{"id": 42}'
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверJetStream: персистентность и стримы
Голый -js включает JetStream, но не создаёт стримы — их нужно объявить явно, либо через natscli, либо кодом приложения при старте. Пример: стрим для событий заказов с ретеншеном по времени.
nats stream add ORDERS \
--subjects "orders.>" \
--storage file \
--retention limits \
--max-age 168h \
--replicas 1
Ключевые параметры JetStream, которые стоит выставлять осознанно, а не оставлять по умолчанию:
| Параметр | Что делает | Типичное значение |
|---|---|---|
--storage | file (на диске) или memory | file для продакшена |
--retention | limits (по объёму/времени), interest (пока есть подписчики), workqueue (пока не подтверждено) | зависит от паттерна |
--max-age | сколько хранить сообщения | 168h (неделя) как стартовая точка |
--replicas | сколько копий стрима в кластере | 1 для одной ноды, 3 для кластера |
--max-bytes | жёсткий лимит объёма стрима | ограничивайте всегда, иначе диск переполнится |
Важный нюанс: file-хранилище JetStream пишет данные в том /data, который в docker-compose объявлен выше. Без volume при пересоздании контейнера стрим и все сообщения в нём исчезнут — для persistence это не опция, а обязательное условие.
Consumer (подписчик с подтверждением) создаётся отдельно от стрима — это разделение и даёт JetStream гарантии доставки:
nats consumer add ORDERS worker-1 \
--filter "orders.created" \
--ack explicit \
--pull \
--max-deliver 5
--max-deliver 5 — сообщение, которое воркер пять раз не подтвердил, уходит либо в лог, либо (при настроенном dead-letter subject) в отдельный поток для ручного разбора — без этого лимита зависший обработчик будет получать одно и то же сообщение бесконечно.
Кластер из трёх нод для отказоустойчивости
Одна нода NATS — единая точка отказа. Кластер решает это через gossip-протокол на порту 6222 и требует нечётного числа нод (минимум 3) для кворума при выборе лидера в JetStream RAFT-группах.
services:
nats-1:
image: nats:2.10-alpine
command: >
-js -sd /data
--cluster_name NATS
--cluster nats://0.0.0.0:6222
--routes nats://nats-2:6222,nats://nats-3:6222
-m 8222
volumes: [nats1_data:/data]
networks: [nats-net]
nats-2:
image: nats:2.10-alpine
command: >
-js -sd /data
--cluster_name NATS
--cluster nats://0.0.0.0:6222
--routes nats://nats-1:6222,nats://nats-3:6222
-m 8222
volumes: [nats2_data:/data]
networks: [nats-net]
nats-3:
image: nats:2.10-alpine
command: >
-js -sd /data
--cluster_name NATS
--cluster nats://0.0.0.0:6222
--routes nats://nats-1:6222,nats://nats-2:6222
-m 8222
volumes: [nats3_data:/data]
networks: [nats-net]
networks:
nats-net:
volumes:
nats1_data:
nats2_data:
nats3_data:
Клиент подключается по списку адресов (nats://nats-1:4222,nats://nats-2:4222,nats://nats-3:4222) — клиентские библиотеки NATS сами переключаются на живую ноду при обрыве соединения, дополнительный балансировщик перед клиентским портом не нужен. А вот стрим, созданный на этапе с одной нодой, реплики автоматически не получит — при переходе на кластер стримы нужно пересоздать с --replicas 3, либо смигрировать через nats stream edit.
Три ноды на одном физическом сервере кластер от падения самого сервера не защитят — для боевой отказоустойчивости ноды разносят по разным VPS, в идеале в разных зонах доступности. Сетевую сторону такой топологии стоит продумать заранее — этому посвящена отдельная статья про типы Docker-сетей, а сам компоуз-файл под несколько окружений (dev/staging с одной нодой, prod с тремя) удобно держать через profiles в docker-compose.
Мониторинг: HTTP-эндпоинт и Prometheus
Встроенный HTTP-монитор на порту 8222 отдаёт JSON без авторизации — за пределы приватной сети или VPN его выставлять не стоит. Основные эндпоинты:
curl http://localhost:8222/varz # общая статистика сервера
curl http://localhost:8222/connz # активные соединения
curl http://localhost:8222/jsz # статистика JetStream: стримы, консьюмеры, объём
curl http://localhost:8222/healthz # health-check для оркестратора
Для Prometheus официальный prometheus-nats-exporter конвертирует эти эндпоинты в метрики:
nats-exporter:
image: natsio/prometheus-nats-exporter:0.15.0
container_name: nats-exporter
restart: unless-stopped
command: "-varz -connz -jsz=all http://nats:8222"
ports:
- "7777:7777"
depends_on:
- nats
После этого http://localhost:7777/metrics отдаёт метрики в формате Prometheus: число активных подключений, счётчики in/out msgs и bytes, лаг JetStream-консьюмеров. Если Prometheus и Grafana в инфраструктуре уже есть — добавить job на nats-exporter:7777 займёт пару строк в prometheus.yml; с нуля их разворачивание описано в статье про связку Prometheus и Grafana. Для внешнего HTTPS-доступа к дашборду монитора удобнее не пробрасывать порт 8222 напрямую, а спрятать его за Traefik как reverse proxy с базовой авторизацией.
Метрика, за которой стоит следить в первую очередь в продакшене — jetstream_consumer_num_pending (сколько сообщений накопилось неподтверждёнными): её рост означает, что воркеры не успевают за потоком и либо нужно масштабировать consumer group, либо в коде обработчика есть зависание.
Безопасность: аутентификация и TLS
По умолчанию контейнер из примеров выше принимает соединения без пароля — приемлемо только в закрытой Docker-сети без внешних портов. Для чего-то большего NATS поддерживает три способа авторизации: простой user/password, токены и NKeys/JWT (decentralized auth, подходит для мультитенантных систем). Для типичного проекта хватает first-варианта:
nats:
image: nats:2.10-alpine
command: >
-js
--user appuser
--pass "${NATS_PASSWORD}"
-m 8222
ports:
- "4222:4222"
volumes: [nats_data:/data]
Пароль стоит хранить не в самом compose-файле, а в .env рядом (добавленном в .gitignore) или в секретах CI — ${NATS_PASSWORD} docker compose подставит автоматически. Для более гранулярного контроля (разным сервисам — доступ только к своим subject) используется полноценный конфиг-файл nats-server.conf с секцией authorization и списком permissions на публикацию/подписку по маскам subject — это отдельная тема, но для старта важно хотя бы не оставлять брокер полностью открытым.
TLS между клиентами и сервером включается флагами --tls, --tlscert, --tlskey — обязательно, если клиенты подключаются не из той же приватной сети, а через интернет. Для внутрикластерного трафика на 6222 порту TLS тоже стоит включить отдельно (--cluster_tls в конфиге), иначе трафик между нодами кластера идёт открытым текстом.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Чем NATS принципиально отличается от RabbitMQ?
RabbitMQ реализует AMQP с богатой маршрутизацией через exchange'и (direct, topic, fanout, headers) и гарантированной доставкой из коробки. NATS в Core-режиме — это в первую очередь скорость и простота без гарантий, а гарантии (persistence, ack, redelivery) добавляются отдельным слоем JetStream. Если нужна сложная маршрутизация с трансформацией сообщений на уровне брокера — ближе RabbitMQ.
Нужен ли JetStream, если просто нужен pub/sub между сервисами?
Нет. Если подписчики всегда онлайн и потеря отдельного сообщения при перезапуске сервиса не критична (например, обновление live-метрик или broadcast-уведомления в UI), Core NATS без -js проще и легче. JetStream добавляйте, когда нужна гарантия «сообщение не потеряется, даже если получатель был недоступен».
Сколько памяти и CPU реально нужно NATS в продакшене?
Сам бинарник в простое занимает порядка 15-30 МБ RAM — точные цифры зависят от числа подключений, стримов и объёма JetStream-хранилища на диске, поэтому ориентируйтесь на нагрузочное тестирование под свой профиль трафика, а не на абстрактные цифры. Для старта на небольшой нагрузке достаточно 1 vCPU и 512 МБ-1 ГБ RAM с запасом под рост JetStream-хранилища.
Можно ли обновить NATS без даунтайма, если это кластер?
Да, при кластере из 3+ нод — ноды обновляются по одной (rolling update): останавливаете одну, обновляете образ, поднимаете обратно, ждёте, пока она догонит кластер (nats server list покажет статус), переходите к следующей. Клиенты, подключённые к обновляемой ноде, автоматически переподключатся к оставшимся живым.
Как понять, что стрим JetStream переполняется и данные начнут вытесняться?
Смотрите /jsz или вывод nats stream info ORDERS — там показаны текущее число сообщений/байт против лимитов --max-msgs/--max-bytes/--max-age. При достижении лимита старые сообщения вытесняются автоматически (retention limits) — это ожидаемое поведение, а не сбой, но лимиты стоит выставлять осознанно под реальный профиль нагрузки, а не оставлять безлимитными.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →