Case study · backend and platform engineeringРазбор проекта · бэкенд и платформенная разработка
POS Integrations
A multi-tenant, event-driven ordering platform for restaurants. Guests order in the restaurant’s own Telegram bot; staff run the order from a Telegram group or a web panel.Мультитенантная событийная платформа заказов для ресторанов. Гости заказывают в собственном Telegram-боте ресторана, а персонал ведёт заказ из группы в Telegram или из веб-панели.
I designed and built all of it myself, from scratch: the architecture, eleven backend services, the custom frameworks under them, the infrastructure, the telemetry and the CI/CD.Я сам спроектировал и написал всё это с нуля: архитектуру, одиннадцать бэкенд-сервисов, собственные фреймворки под ними, инфраструктуру, телеметрию и CI/CD.
David Alkamyan · 2024 – presentDavid Alkamyan · с 2024 года
01
What a restaurant getsЧто получает ресторан
Each restaurant is a tenant with its own Telegram bot. Once the bot is connected, the platform carries the whole order: the conversation with the guest, the work of the staff, the payment and every notification in between.Каждый ресторан — отдельный тенант со своим Telegram-ботом. После подключения бота платформа ведёт заказ целиком: диалог с гостем, работу персонала, оплату и все уведомления по пути.
GuestsГости
An ordering botБот для заказов
A menu with photos and modifiers, a cart, delivery or takeout, an address taken from a shared location, discounts, and payment in cash, by terminal or online through Payme. In Russian, Uzbek and English.Меню с фотографиями и модификаторами, корзина, доставка или самовывоз, адрес по отправленной геолокации, скидки, оплата наличными, через терминал или онлайн через Payme. На русском, узбекском и английском.
StaffПерсонал
A Telegram group as the order deskГруппа в Telegram вместо пульта заказов
Every order arrives as a card with action buttons: accept, preparing, ready, complete, cancel. The card redraws itself on every change, no matter who made it or where.Каждый заказ приходит карточкой с кнопками: принять, готовится, готов, выдан, отменить. Карточка перерисовывается при любом изменении, кто бы и откуда его ни сделал.
OwnerВладелец
A web admin panelВеб-панель администратора
Orders, the menu, branches, discounts, clients, the bot connection and payment methods in one place, on a desktop or a phone. Sign-in with a password or straight from the Poster back office.Заказы, меню, филиалы, скидки, клиенты, подключение бота и способы оплаты в одном месте, с компьютера или с телефона. Вход по паролю или прямо из бэк-офиса Poster.
The ordering botБот для заказов
A dish. A photo, a description and the modifiers as buttons.Блюдо. Фотография, описание и модификаторы кнопками.Quantity. One message, edited in place as the guest taps.Количество. Одно сообщение, которое редактируется на месте по нажатиям гостя.Checkout. The order before it is sent: branch, pickup or delivery, time, payment.Оформление. Заказ перед отправкой: филиал, самовывоз или доставка, время, оплата.
The bot in Russian, on demo data. It also speaks Uzbek and English.Бот на русском, данные демонстрационные. Ещё он говорит на узбекском и английском.
The admin panelАдмин-панель
Orders. The live order desk, with the same statuses and actions as the staff group.Заказы. Живой пульт заказов: те же статусы и действия, что и в группе персонала.One order. The delivery, the items, the discount and who changed the status, from where and when.Заказ. Доставка, позиции, скидка и история: кто, откуда и когда менял статус.Menu. Categories, prices per branch and how complete the texts are in each language.Меню. Категории, цены по филиалам и полнота текстов на каждом языке.Dashboard. What still needs setting up, what needs action now and today’s numbers.Дашборд. Что ещё нужно настроить, что требует действия сейчас и цифры за сегодня.
The panel on demo data. Click a screen to open it full size.Панель на демонстрационных данных. Нажмите на экран, чтобы открыть его в полном размере.
Integrations todayИнтеграции сейчас
Telegramordering channel and staff deskканал заказов и рабочее место персонала
Postersign-in from the POS accountвход из аккаунта POS-системы
Paymeonline payments over its Merchant APIонлайн-оплата по Merchant API
Google Mapsaddress lookupопределение адреса
It grows at the edges: a new ordering channel talks to the same orders API and listens to the same events, and a new POS system or payment provider is an SDK behind an existing service.Платформа растёт по краям: новый канал заказов работает с тем же API заказов и слушает те же события, а новая POS-система или платёжный провайдер — это SDK за уже существующим сервисом.
02
ArchitectureАрхитектура
Eleven services, two Kafka topics and three public routes. The map shows what talks to what; the decisions below say why.Одиннадцать сервисов, два топика Kafka и три публичных маршрута. На карте — кто с кем общается, ниже — почему так.
Swipe sideways to see the whole diagram.Листайте вбок, чтобы увидеть схему целиком.
HTTP callHTTP-вызов
Kafka recordзапись Kafka
outside the platformвне платформы
The system map. In service names, customers are the tenants (restaurants) and users are their guests. Bots are services like any other: the same runtime, with Kafka handlers as entry points.Карта системы. В названиях сервисов customers — это тенанты (рестораны), а users — их гости. Боты — такие же сервисы, как остальные: тот же рантайм, только входные точки у них — обработчики Kafka.
The edge only verifies and enqueuesШлюз только проверяет и кладёт в очередь
The Telegram gateway checks a per-restaurant secret and writes the update to Kafka untouched. It calls no other service, so a slow bot or a restart never turns into a failed webhook.Telegram-шлюз проверяет секрет конкретного ресторана и пишет апдейт в Kafka как есть. Других сервисов он не вызывает, поэтому медленный бот или рестарт не превращаются в упавший вебхук.
Multi-tenant from the edge inwardМультитенантность от входа и вглубь
Each restaurant has its own bot, token and settings, and one gateway serves them all. Every request, Kafka record and secret carries the tenant, and every service scopes its data by it.У каждого ресторана свой бот, токен и настройки, а шлюз на всех один. Тенант указан в каждом запросе, записи Kafka и секрете, и каждый сервис ограничивает им свои данные.
A database per serviceУ каждого сервиса своя база
Nine Postgres databases and no shared tables. The gateways own no data at all.Девять баз в Postgres и ни одной общей таблицы. У шлюзов своих данных нет вовсе.
One owner of order stateУ состояния заказа один владелец
Every status change is one HTTP call into orders and one event out of it. The staff group and the admin panel are two channels onto the same transitions, and nothing ever calls a bot directly: the bots redraw their messages from the events.Каждая смена статуса — один HTTP-вызов в сервис заказов и одно событие из него. Группа персонала и админ-панель — два канала к одним и тем же переходам, а ботов никто не вызывает напрямую: они перерисовывают свои сообщения по событиям.
Order per chat, parallel across chatsПорядок внутри чата, параллельность между чатами
Telegram updates are keyed by restaurant and chat, so one conversation is always handled in order while different conversations spread over the partitions.Ключ апдейта Telegram — ресторан и чат, поэтому один диалог всегда обрабатывается по порядку, а разные диалоги расходятся по партициям.
Hard rules for dataЖёсткие правила для данных
Instants are UTC everywhere, and a time zone is a display choice read from the tenant. Money is NUMERIC(14, 2) in the database and a fixed-point string on the wire.Моменты времени везде в UTC, а часовой пояс — способ отображения, который берётся из данных тенанта. Деньги — NUMERIC(14, 2) в базе и строка с фиксированной точкой в API и событиях.
03
The strongest partsСильные стороны
Six things I would show another engineer first: three custom frameworks and three rules the services live by.Шесть вещей, которые я показал бы другому инженеру в первую очередь: три собственных фреймворка и три правила, по которым живут сервисы.
3.1
One custom runtime for every serviceОдин собственный рантайм для всех сервисов
Every service is the same process: a FastAPI app whose lifespan starts, by role, a Kafka consumer and a scheduler. A service declares its routers, Kafka handlers and jobs; an environment variable decides which of them a container runs. Health probes, typed API errors, single-flight jobs and telemetry come from the runtime, so a new service is operable and instrumented by construction.Каждый сервис — один и тот же процесс: приложение FastAPI, в lifespan которого по ролям запускаются Kafka-консьюмер и планировщик. Сервис объявляет свои роутеры, обработчики Kafka и задачи, а переменная окружения решает, что из этого поднимет контейнер. Health-пробы, типизированные ошибки API, защиту задач от параллельного запуска и телеметрию даёт рантайм, поэтому новый сервис с первого дня эксплуатируется и наблюдается так же, как остальные.
The package layout is fixed too (entry points → services → infrastructure, dependencies pointing one way), so adding a service is a scaffold and a checklist.Структура пакета тоже зафиксирована (входные точки → сервисы → инфраструктура, зависимости в одну сторону), поэтому новый сервис — это шаблон и чек-лист.
A whole service, declaredВесь сервис одним объявлением
A custom framework on top of aiokafkaСобственный фреймворк поверх aiokafka
Why. aiokafka is a client, not an application: ordering, retries, dead letters, rebalances, shutdown and health checks are left to whoever writes the consumer. I wrote that part once, as a framework. A bot declares a handler and a retry policy per topic; the rest is the framework’s job.Зачем. aiokafka — это клиент, а не приложение: порядок, повторы, dead-letter-очередь, ребалансы, остановка и health-проверки остаются на том, кто пишет консьюмер. Я написал эту часть один раз, как фреймворк. Бот объявляет обработчик и политику повторов для топика, а остальное делает фреймворк.
How. One worker per partition gives strict order inside a partition and full concurrency across them. A failing handler retries on its policy, then the record goes to a dead-letter topic owned by that consumer group. The DLQ send is awaited before the offset commit, so a record is either in the DLQ or still unconsumed, never lost in between.Как. Один воркер на партицию даёт строгий порядок внутри партиции и полную конкурентность между ними. Упавший обработчик повторяется по своей политике, после чего запись уходит в dead-letter-топик, принадлежащий этой группе потребителей. Отправка в DLQ дожидается подтверждения брокера до коммита оффсета, поэтому запись либо уже в DLQ, либо ещё не вычитана — потеряться между этими состояниями она не может.
A full partition queue pauses only that partition; the poll loop never blocks.Переполненная очередь партиции ставит на паузу только её; цикл опроса не блокируется никогда.
A rebalance lets the record in flight finish, then commits only what was completed.При ребалансе обработка текущей записи доводится до конца, после чего коммитится только завершённое.
Records too old to matter (a tap from an hour ago) are discarded, and every discard shows in logs and metrics.Записи, потерявшие смысл от времени (нажатие часовой давности), отбрасываются, и каждый такой случай виден в логах и метриках.
A dead poll loop or a stuck rebalance fails the liveness probe, and the process restarts.Умерший цикл опроса или зависший ребаланс роняют liveness-пробу, и процесс перезапускается.
3.3
A custom UI library on top of aiogramСобственная UI-библиотека поверх aiogram
Why. Plain aiogram leaves every flow to decide when to send, edit or delete a message and how to match a pressed reply button. aiogram-dialog answers that with its own widget layer and implicit wiring: more than the bots needed, and harder to debug. aiogram_windows keeps aiogram’s own keyboards, media and middlewares and adds only the missing rule.Зачем. Чистый aiogram оставляет каждому сценарию самому решать, когда отправить, отредактировать или удалить сообщение и как сопоставить нажатую reply-кнопку. aiogram-dialog отвечает на это собственным слоем виджетов и неявным связыванием: ботам столько было не нужно, а отлаживать это труднее. aiogram_windows оставляет родные клавиатуры, медиа и middleware aiogram и добавляет только недостающее правило.
How. An FSM state is a window: a getter renders the screen, a handler reacts to the next update, and the engine then renders whatever state the handler left. There is one live screen per chat: inline screens are edited in place, reply-keyboard screens are sent anew, stale ones are cleaned up.Как. Состояние FSM — это окно: getter рисует экран, handler обрабатывает следующий апдейт, после чего движок рисует то состояние, в котором handler оставил диалог. В чате один живой экран: inline-экраны редактируются на месте, экраны с reply-клавиатурой отправляются заново, устаревшие убираются.
Registration is explicit: Window(state, getter, handler), no decorators and no hidden wiring.Регистрация только явная: Window(state, getter, handler), без декораторов и скрытого связывания.
A failed turn persists nothing, and a press that arrives in an expired state is replayed, not lost.Неудачный ход ничего не сохраняет, а нажатие, пришедшее в истёкшем состоянии, воспроизводится, а не теряется.
It ships its own test kit: a recording bot and update factories.В комплекте свой тестовый набор: записывающий бот и фабрики апдейтов.
A flow is a list of windowsСценарий — это список окон
async def language_getter(manager: WindowManager) -> View:
return View(text='Choose your language', reply_markup=language_keyboard())
async def language_handler(manager: WindowManager, button_id: str | None) -> None:
if button_id is not None: # a language button was pressed
manager.data['language'] = button_id
manager.goto(Onboarding.ask_phone)
onboarding = WindowGroup(
Window(Onboarding.ask_language, language_getter, language_handler),
Window(Onboarding.ask_phone, phone_getter, phone_handler),
name='onboarding',
)
3.4
Events that can be trustedСобытия, которым можно доверять
The order event is written to an outbox table in the transaction that changes the order: a rollback leaves no event, and a commit always gets its event out. Events carry a full snapshot, are keyed by order id and are checked against a Schema Registry in BACKWARD mode. Consumers are written to see the same event twice.Событие заказа пишется в outbox-таблицу в той же транзакции, что меняет заказ: откат не оставляет события, а после коммита оно обязательно уйдёт. Событие несёт полный снимок заказа, ключом служит id заказа, а схема проверяется в Schema Registry в режиме BACKWARD. Потребители рассчитаны на то, что одно и то же событие придёт дважды.
3.5
Contracts, not importsКонтракты вместо импортов
No service imports another. OpenAPI schemas are committed next to the code, and every call between services goes through a typed client generated from them. The admin gateway is a transparent proxy: it signs the person in, resolves the tenant and forwards to the service that owns the data. It merges the services’ admin schemas into one document, from which the React panel generates its types, so a new admin endpoint never touches the gateway.Ни один сервис не импортирует другой. OpenAPI-схемы хранятся в репозитории рядом с кодом, и любой вызов между сервисами идёт через сгенерированный по ним типизированный клиент. Админ-шлюз — прозрачный прокси: он аутентифицирует человека, определяет тенанта и пересылает запрос сервису, которому принадлежат данные. Админ-схемы сервисов он склеивает в один документ, из которого React-панель генерирует свои типы, поэтому новый админ-эндпоинт не требует ни строчки в шлюзе.
3.6
Tests that decide what shipsТесты, которые решают, что уедет в прод
Over 2,100 automated tests: about half of the Python code is tests. Service suites run against real Postgres, Kafka and Redis in testcontainers, with shared pytest plugins for faking neighbour services, asserting on Kafka and freezing time. Above them sits a full-flow suite: ten services in one pytest process, driven only through the Telegram webhook, the staff group and the Payme callback. Scenarios assert by translation key, so rewording a message breaks nothing and renaming a key fails loudly.Больше 2100 автотестов: примерно половина кода на Python — тесты. Тесты сервисов работают с настоящими Postgres, Kafka и Redis в testcontainers; общие pytest-плагины подменяют соседние сервисы, проверяют сообщения Kafka и замораживают время. Над ними — сквозной набор: десять сервисов в одном процессе pytest, а управлять ими можно только через вебхук Telegram, группу персонала и колбэк Payme. Сценарии проверяют сообщения по ключу перевода: переформулировка текста ничего не ломает, а переименование ключа сразу роняет тест.
A full-flow scenario: from /start to an accepted orderСквозной сценарий: от /start до принятого заказа
How a request, a Kafka record or a failing container becomes something you can see. Three facts carry the whole design.Как запрос, запись Kafka или упавший контейнер превращаются в то, что можно увидеть. Вся конструкция держится на трёх фактах.
Services do nothing specialСервисы ничего особенного не делают
The runtime configures OpenTelemetry once per process. FastAPI, aiohttp, SQLAlchemy, Redis and aiokafka are instrumented automatically, and logs are JSON on stdout. A service built the normal way is fully instrumented.Рантайм настраивает OpenTelemetry один раз на процесс. FastAPI, aiohttp, SQLAlchemy, Redis и aiokafka инструментируются автоматически, логи пишутся в stdout в JSON. Сервис, собранный обычным способом, инструментирован полностью.
One collector is the only agentОдин коллектор — единственный агент
It receives traces and metrics over OTLP, reads every container’s log file and polls Docker, the host, Postgres, Redis, Kafka and the readiness endpoints. Nothing else scrapes anything.Он принимает трейсы и метрики по OTLP, читает файл логов каждого контейнера и опрашивает Docker, хост, Postgres, Redis, Kafka и readiness-эндпоинты. Больше никто ничего не собирает.
Three stores, joined by idsТри хранилища, связанные идентификаторами
Tempo keeps traces, Loki keeps logs, Prometheus keeps metrics, and they never see each other. Grafana joins them: the trace id in every log line links a log to its trace and back, and the service name and the route link both to the metrics.Tempo хранит трейсы, Loki — логи, Prometheus — метрики, и друг о друге они не знают. Связывает их Grafana: trace id в каждой строке лога ведёт от лога к трейсу и обратно, а имя сервиса и маршрут связывают оба сигнала с метриками.
Swipe sideways to see the whole diagram.Листайте вбок, чтобы увидеть схему целиком.
pushотправка (push)
pull, arrowhead on the side being readопрос (pull), стрелка указывает на то, что читают
The telemetry pipeline, from what emits a signal to the alert in a Telegram chat.Конвейер телеметрии: от источника сигнала до алерта в чате Telegram.
One update, three signalsОдин апдейт — три сигнала
Swipe sideways to see the whole diagram.Листайте вбок, чтобы увидеть схему целиком.
server or consumer spanсерверный спан или спан консьюмера
client spanклиентский спан
log lineстрока лога
One Telegram update as telemetry sees it: a single trace across two transports and four processes. The gateway’s send span writes traceparent into the record headers and the consumer opens its span with that header as the parent, so the trace survives Kafka.Один апдейт Telegram глазами телеметрии: единый трейс через два транспорта и четыре процесса. Спан отправки на шлюзе пишет traceparent в заголовки записи, а консьюмер открывает свой спан с этим заголовком в роли родителя, поэтому трейс переживает Kafka.
Three dashboardsТри дашборда
Fleet health. Readiness, HTTP rate, errors and latency of every service on shared charts, Kafka consumers, scheduled jobs, datastores, containers.Fleet health. Готовность, частота запросов, ошибки и задержки всех сервисов на общих графиках, Kafka-консьюмеры, задачи по расписанию, хранилища, контейнеры.
Resources. The host, every container, Postgres, Redis, Kafka and the collector’s own health.Resources. Хост, каждый контейнер, Postgres, Redis, Kafka и состояние самого коллектора.
Trace investigation. The logs of one trace across all services.Trace investigation. Логи одного трейса по всем сервисам.
Dashboards, alert rules and their routing are files in the repository. Retention is short on purpose: traces for 72 hours, logs for 7 days. Telemetry is for the incident, not for the archive.Дашборды, правила алертов и их маршрутизация — файлы в репозитории. Срок хранения намеренно короткий: трейсы живут 72 часа, логи — 7 дней. Телеметрия нужна для инцидента, а не для архива.
22 alert rules, delivered to Telegram22 правила алертов с доставкой в Telegram
Availability · 4. A failing readiness probe, an unhealthy or crash-restarted container, a scrape target that is down.Доступность · 4. Упавшая readiness-проба, нездоровый или аварийно перезапущенный контейнер, недоступная цель сбора метрик.
Errors · 6. Application errors, an order event in the DLQ, 5xx at the edge, rejected webhooks, consumer lag that does not drain, a failed job.Ошибки · 6. Ошибки приложений, событие заказа в DLQ, 5xx на входе, отклонённые вебхуки, лаг консьюмера, который не убывает, упавшая задача.
Saturation · 8. Host CPU, memory and disk, container memory, Postgres connections and deadlocks, Redis memory and rejected connections.Насыщение · 8. CPU, память и диск хоста, память контейнеров, соединения и дедлоки Postgres, память Redis и отклонённые им соединения.
Telemetry itself · 4. The collector failing to export or refusing data, its queue filling up, Grafana failing to deliver a notification.Сама телеметрия · 4. Коллектор не может экспортировать или отказывается принимать данные, его очередь переполняется, Grafana не может доставить уведомление.
Rules are evaluated every minute. One-shot events (a crash, a dead-lettered record, a failed job) are sent once, with no “resolved” message, and a storm is grouped into one.Правила вычисляются раз в минуту. Одноразовые события (аварийный рестарт, запись в DLQ, упавшая задача) отправляются один раз, без сообщения «resolved», а шторм алертов группируется в одно сообщение.
05
Delivery and operationsДоставка и эксплуатация
How code reaches production, where secrets live and what production actually is.Как код попадает в продакшен, где живут секреты и что такое продакшен на самом деле.
5.1 · CI/CD
Each service ships on its own verdictКаждый сервис выкатывается по собственному вердикту
One GitHub Actions run builds, tests, migrates and deploys any selection of services as matrix jobs. A red suite or a failed migration holds back only its own service, which keeps its previous image while the others ship. Migrations run before any restart and are additive by rule, so the previous image always works on the new schema. The deploy refuses to restart a service whose database is not at its image’s Alembic head, and rollback is one more workflow.Один запуск GitHub Actions собирает, тестирует, мигрирует и деплоит любой набор сервисов матричными джобами. Красные тесты или упавшая миграция задерживают только свой сервис: он остаётся на прежнем образе, пока остальные выкатываются. Миграции выполняются до любого рестарта и по правилу только добавляют, поэтому предыдущий образ всегда работает на новой схеме. Деплой откажется перезапускать сервис, чья база не на Alembic-head его образа, а откат — отдельный workflow.
prepareresolve the selected servicesкакие сервисы выбраны
build & pushan image per serviceобраз на каждый сервис
testa suite per serviceтесты каждого сервиса
full-flowthe whole backendвесь бэкенд целиком
plandecide who shipsкто выкатывается
migratebefore any restartдо любого рестарта
deployservice by serviceсервис за сервисом
The production workflow. The builds, the per-service suites and the full-flow suite run side by side; migrate and deploy are one job per service.Продакшен-workflow. Сборка, тесты сервисов и сквозной набор идут параллельно; миграция и деплой — по одной джобе на сервис.
5.2 · Secrets5.2 · Секреты
Secure storage for dynamic secrets: OpenBaoБезопасное хранилище динамических секретов: OpenBao
A restaurant’s bot token and its payment credentials are not known at deploy time: the owner enters them in the admin panel while the system is running. The service that owns such a secret writes it to OpenBao and reads it from there. It never lands in a database row, an env file or an event.Токен бота ресторана и его платёжные реквизиты неизвестны в момент деплоя: владелец вводит их в админ-панели, когда система уже работает. Сервис, которому принадлежит такой секрет, пишет его в OpenBao и оттуда же читает. В строку базы, env-файл или событие он не попадает никогда.
Each service signs in with its own AppRole and can read only its own class of paths.Каждый сервис входит со своим AppRole и читает только свой класс путей.
Webhook secrets are derived, not stored: an HMAC of the tenant and a generation number under a versioned signing key. One tenant’s webhook can be revoked alone, the whole fleet’s by rotating the key.Секреты вебхуков не хранятся, а выводятся: это HMAC от тенанта и номера поколения на версионируемом ключе подписи. Вебхук одного тенанта можно отозвать отдельно, а всего парка — ротацией ключа.
Admin passwords are hashed with argon2id; sessions are opaque tokens in Redis and can be revoked at once.Пароли администраторов хешируются argon2id, сессии — непрозрачные токены в Redis, которые отзываются мгновенно.
5.3 · Deployment5.3 · Развёртывание
One server, Docker Compose, nothing managedОдин сервер, Docker Compose и ничего управляемого
Production is a single VDS with 4 vCPU and 8 GB of RAM. Everything runs on it under Docker Compose: the eleven services, PostgreSQL, Kafka with its Schema Registry, Redis, MinIO, OpenBao, nginx and the whole telemetry stack.Продакшен — это один VDS с 4 vCPU и 8 ГБ памяти. Под Docker Compose на нём работает всё: одиннадцать сервисов, PostgreSQL, Kafka со Schema Registry, Redis, MinIO, OpenBao, nginx и весь стек телеметрии.
No Kubernetes, no managed database and no managed broker. That is a deliberate choice to cut costs: the load fits one machine, and everything the product needs from an orchestrator at this scale (health checks, restarts, one-service deploys, rollback) is covered by Compose and the pipeline above. The price is a single failure domain, accepted at this scale.Ни Kubernetes, ни управляемой базы, ни управляемого брокера. Это сознательное решение ради экономии: нагрузка помещается на одну машину, а всё, что продукту на таком масштабе нужно от оркестратора (health-проверки, рестарты, выкатка одного сервиса, откат), закрывают Compose и конвейер выше. Цена — единая точка отказа, на таком масштабе она принята осознанно.
Seven Compose fragments behind one entry point; Postgres, Redis, Kafka, the telemetry stack and the edge each sit on their own network.Семь фрагментов Compose за одной точкой входа; Postgres, Redis, Kafka, стек телеметрии и входной nginx живут каждый в своей сети.
Only nginx faces the internet. Grafana is reached through an SSH tunnel.В интернет смотрит только nginx. До Grafana можно добраться только через SSH-туннель.
Images are built in CI, tagged by commit and pulled from the registry. A deploy recreates exactly one container and records every attempt, so a rollback knows where to go back to.Образы собираются в CI, помечаются коммитом и забираются из реестра. Деплой пересоздаёт ровно один контейнер и записывает каждую попытку, поэтому откат знает, куда возвращаться.
The dev stack mirrors production: the same Compose fragments, and a self-hosted tunnel gives it stable public hostnames for Telegram and Payme webhooks.Dev-стенд повторяет продакшен: те же фрагменты Compose, а собственный туннель даёт ему постоянные публичные адреса для вебхуков Telegram и Payme.
Ten runbooks cover the rest: provisioning, TLS, the secret store, alerting, webhooks, smoke tests.Остальное описано в десяти ранбуках: подготовка сервера, TLS, хранилище секретов, алертинг, вебхуки, смоук-тесты.
The door stays open: services take their role from an environment variable and expose startup, liveness and readiness probes, so moving to an orchestrator later changes the deployment, not the code.Дверь остаётся открытой: сервис берёт роль из переменной окружения и отдаёт startup-, liveness- и readiness-пробы, так что переезд на оркестратор изменит развёртывание, а не код.