MAATRIX / Блог / Доставка: трекинг курьеров на карте на своём сервере, без абонплаты за точку

Доставка: трекинг курьеров на карте на своём сервере, без абонплаты за точку

MAATRIX

Клиент оформил заказ и первым делом открывает карту — где курьер, сколько ждать. Это давно не бонус, а ожидание по умолчанию: без живой точки на карте у службы доставки начинаются звонки «а где мой заказ» и потерянное время оператора. Показать точку на карте технически несложно — за вечер собирается прототип. Проблема начинается позже, когда вы платите стороннему сервису трекинга за каждую отслеживаемую доставку, и с ростом объёма эта плата растёт быстрее, чем вы успеваете зарабатывать на новых заказах. Разберём, как трекинг курьеров устроен изнутри и почему вынести его на свой сервер часто оказывается дешевле и предсказуемее.

Абонплата за точку: как она устроена и почему давит именно при росте

Большинство облачных сервисов трекинга берут деньги не за подписку компании, а за единицу отслеживания — активную доставку, работающего курьера или «точку» в интерфейсе. Модель понятна с точки зрения продавца: чем больше вы используете сервис, тем больше платите. Проблема в том, что для службы доставки это означает — тариф растёт ровно тогда, когда бизнес растёт. Пять курьеров и пятьдесят заказов в день — одна плата. Пятнадцать курьеров и пятьсот заказов — плата кратно выше, часто с переходом на следующий тарифный план, где условия хуже привычных.

При этом сама задача — принять координаты, сохранить, отдать на карту — не становится сложнее пропорционально числу точек. Нагрузка растёт линейно и довольно скромно: один курьер присылает координаты раз в 10–15 секунд, это несколько десятков байт на запрос. Тысяча курьеров одновременно — тысячи мелких запросов в минуту, что для одного нормально настроенного сервера не проблема. А вот счёт от провайдера трекинга растёт не с вашими издержками, а с вашей выручкой — и это разные вещи.

Вторая сторона боли — вы привязаны к чужому SDK в приложении курьера, к чужому виджету карты на сайте клиента и к чужому API, который может поменять лимиты или цены в любой момент. Свой сервер снимает эту зависимость: вы платите за вычислительные ресурсы, а не за количество отслеживаемых объектов.

Что на самом деле нужно от системы трекинга

Прежде чем проектировать архитектуру, стоит честно разложить задачу на составляющие — потому что «трекинг на карте» на самом деле три отдельные подсистемы:

  • Приём координат от курьера. Мобильное приложение или веб-приложение курьера с доступом к геолокации периодически отправляет свои координаты на сервер.
  • Хранение и раздача текущего положения. Сервер держит последнюю известную позицию каждого курьера и умеет быстро её отдавать.
  • Живое обновление на карте клиента. Клиент, открывший страницу отслеживания заказа, видит, как маркер курьера двигается почти в реальном времени, без постоянного обновления страницы.

Отдельно стоит вопрос истории — куда движение курьера пишется на будущее, для разбора спорных ситуаций и расчёта показателей. Это не то же самое, что живое отображение: живая точка нужна раз в несколько секунд, история может агрегироваться грубее.

Важно не путать это с полноценной геоинформационной системой. Для показа курьера на карте не нужен PostGIS с геопространственными индексами и не нужен собственный сервис построения маршрутов — это отдельная и куда более тяжёлая задача. Здесь речь о показе точки и её перемещении, а не о прокладке оптимального маршрута по городу.

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

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

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

Архитектура на своём сервере: из каких частей она собирается

Рабочая схема для трекинга курьеров укладывается в четыре компонента, и все они без труда разворачиваются на одном VPS среднего размера:

  1. Backend API — принимает координаты от приложения курьера (REST-эндпоинт) и отдаёт текущее положение клиентам.
  2. База данных — хранит курьеров, заказы, привязку курьера к заказу и историю координат. Обычный PostgreSQL справляется без специфических геопространственных расширений, если вам не нужен точный расчёт расстояний и попадание в полигоны зон доставки.
  3. Слой реального времени — WebSocket-соединения с клиентами плюс Redis pub/sub, если бэкенд работает в нескольких процессах и нужно рассылать обновления между ними.
  4. Карта на фронтенде — открытая JS-библиотека карт (например, Leaflet или MapLibre GL) поверх открытых тайлов, без привязки к платному картографическому API с постраничной тарификацией.

Минимальный вариант для потока в несколько сотен курьеров одновременно — один сервер с Docker Compose, где рядом живут backend, PostgreSQL и Redis:

version: "3.9"
services:
  api:
    build: ./api
    restart: unless-stopped
    ports:
      - "127.0.0.1:8000:8000"
    environment:
      DATABASE_URL: postgres://tracking:${DB_PASSWORD}@postgres:5432/tracking
      REDIS_URL: redis://redis:6379/0
    depends_on:
      - postgres
      - redis

  postgres:
    image: postgres:16
    restart: unless-stopped
    environment:
      POSTGRES_DB: tracking
      POSTGRES_USER: tracking
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - pgdata:/var/lib/postgresql/data

  redis:
    image: redis:7
    restart: unless-stopped
    volumes:
      - redisdata:/data

volumes:
  pgdata:
  redisdata:

Redis здесь не обязателен, если backend работает в одном процессе (один worker) — тогда можно рассылать обновления клиентам прямо из памяти процесса, без промежуточного pub/sub. Он становится нужен, когда вы масштабируете API на несколько процессов или несколько серверов за балансировщиком: без общей шины обновление, полученное на одном процессе, не долетит до клиента, подключённого к другому.

Про сам PostgreSQL и его базовую настройку — в отдельном разборе: установка и настройка PostgreSQL на VPS.

Приём координат: API и мобильное приложение курьера

Приложение курьера — это может быть нативное приложение или просто веб-страница (PWA) с доступом к геолокации браузера. Оно с заданным интервалом отправляет POST-запрос с текущими координатами:

POST /api/courier/location
Authorization: Bearer <courier_token>
Content-Type: application/json

{
  "lat": 55.751244,
  "lng": 37.618423,
  "accuracy": 12,
  "timestamp": "2026-08-27T14:32:05Z"
}

Несколько практических моментов, на которые стоит обратить внимание при реализации:

  • Интервал отправки — компромисс между точностью и батареей курьерского телефона. Разумный ориентир — каждые 10–20 секунд в движении и реже (или совсем не присылать) при остановке, но это стоит проверить на своём парке устройств.
  • Точность (accuracy) от GPS-модуля стоит сохранять и учитывать: если она хуже условных 50 метров, разумно либо не обновлять маркер, либо явно показать клиенту, что позиция приблизительная — иначе точка «прыгает» по карте на людном перекрёстке.
  • Авторизация курьера — отдельный токен на смену или на устройство, не общий API-ключ на всё приложение. Так при компрометации одного устройства не приходится перевыпускать доступ всем курьерам.
  • Фильтрация выбросов. GPS иногда даёт разовый скачок координат на сотни метров из-за многолучевого распространения сигнала между высокими домами. Проверка «новая точка не дальше N метров от предыдущей за M секунд, иначе игнорируем» убирает большинство таких артефактов без сложной математики.

Backend на приёме сохраняет последнюю позицию курьера (upsert по courier_id) и публикует событие в канал, привязанный к активному заказу этого курьера, чтобы клиент, отслеживающий именно свою доставку, получил обновление.

Живое обновление у клиента: вебсокеты и nginx

Клиентская страница отслеживания заказа открывает WebSocket-соединение и подписывается на канал конкретного заказа. Сервер при получении новых координат курьера, у которого этот заказ активен, рассылает обновление всем подписчикам:

const ws = new WebSocket(`wss://track.example.com/ws/order/${orderId}`);

ws.onmessage = (event) => {
  const { lat, lng } = JSON.parse(event.data);
  courierMarker.setLatLng([lat, lng]);
};

Если перед backend стоит nginx (а в продакшене он почти всегда стоит — для SSL-терминации и раздачи статики), для WebSocket нужен отдельный проксирующий блок с апгрейдом соединения:

location /ws/ {
    proxy_pass http://127.0.0.1:8000;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_read_timeout 3600s;
}

Обратите внимание на proxy_read_timeout — по умолчанию nginx рвёт долгоживущие соединения гораздо раньше, чем вам нужно для постоянно открытого WebSocket-канала отслеживания. Подробнее про настройку nginx как обратного прокси, включая типичные грабли с таймаутами и заголовками — в отдельной статье: nginx как reverse proxy на VPS.

Если WebSocket по какой-то причине не подходит (например, часть клиентов сидит за прокси, которые режут долгие соединения), рабочая альтернатива — обычный polling раз в несколько секунд через HTTP-запрос к эндпоинту текущей позиции. Он проще в поддержке и почти всегда работает, но у него выше базовая нагрузка на сервер при большом числе одновременных наблюдателей, потому что каждый клиент дёргает сервер сам, а не ждёт push-обновления.

История поездок: хранение, оценка времени и разбор спорных ситуаций

Живая позиция — это одна строка на курьера, которая перезаписывается. История — отдельная таблица, куда можно писать точки с меньшей частотой, чем показываете на карте:

CREATE TABLE courier_location_history (
    id BIGSERIAL PRIMARY KEY,
    courier_id INT NOT NULL REFERENCES couriers(id),
    order_id INT REFERENCES orders(id),
    lat DOUBLE PRECISION NOT NULL,
    lng DOUBLE PRECISION NOT NULL,
    accuracy REAL,
    recorded_at TIMESTAMPTZ NOT NULL
);

CREATE INDEX idx_history_courier_time
    ON courier_location_history (courier_id, recorded_at);

История нужна для трёх практических вещей:

  • Разбор споров с клиентом. «Курьер стоял у подъезда десять минут и не позвонил» проверяется по трекам, а не по словам. Имея историю, вы отвечаете на претензию фактами за минуту, а не поднимаете логи трёх систем.
  • Контроль маршрута. Если курьер систематически заезжает не туда или подолгу стоит в месте, не связанном с доставкой, это видно по накопленной истории, а не по единичному инциденту.
  • Приблизительная оценка времени прибытия. Точный ETA с учётом пробок — задача сервисов маршрутизации с картами дорожного графа, и с нуля на своём сервере вы её не воспроизведёте. Но грубая оценка «осталось столько-то по прямой при текущей скорости» вполне считается на своих данных — с оговоркой, что это ориентир, а не гарантия.

Историю разумно хранить не бесконечно — определите срок (например, несколько месяцев), после которого старые записи архивируются или удаляются. Это и вопрос объёма базы, и вопрос обращения с персональными данными: координаты курьера и привязка к заказу клиента — тоже данные, которые нужно защищать, а не просто копить.

Похожая по духу задача — учёт маршрутов и рейсов, только не курьеров пешком или на самокате, а логистики покрупнее — разобрана в статье учёт рейсов логиста на своём сервере: там больше про планирование и путевые листы, но принцип «данные о перемещениях — свои, не в чужом SaaS» тот же.

Сколько это стоит на практике и как посчитать сервер под свой объём

Ключевая разница в экономике простая: облачный трекинг с оплатой за точку масштабирует счёт вместе с вашим бизнесом, свой сервер — нет. Сервер, которого хватает на сто курьеров, почти наверняка справится и с тремя сотнями без апгрейда, потому что узкое место — не число курьеров само по себе, а суммарная частота запросов и число открытых WebSocket-соединений, а это довольно скромные величины даже при заметном парке.

Грубая прикидка нагрузки, чтобы подбирать сервер осознанно, а не наугад:

ПоказательКак считатьОриентир нагрузки
Запросы приёма координаткурьеров × (1 запрос / интервал отправки)несколько сотен курьеров при интервале 15 сек — единицы запросов в секунду
Открытые WebSocket-соединенияактивные заказы с открытой страницей отслеживаниядержатся почти бесплатно по CPU, ограничение — память на соединение
Размер истории за месяцкурьеров × точек в день × днейдля сотен курьеров — обычно единицы–десятки гигабайт в месяц, зависит от интервала записи

Для старта хватает одного VPS: пара ядер и несколько гигабайт памяти держат и API, и PostgreSQL, и Redis без напряжения при парке в пределах пары сотен курьеров. Дальше рост считается не абстрактно, а по факту — смотрите на реальную загрузку CPU и памяти под своей нагрузкой и апгрейдите тариф сервера, когда видите, что запас заканчивается, а не заранее «на всякий случай». Разница с облачным трекингом именно в этом: апгрейд сервера — это шаг вверх по фиксированной шкале тарифов, а не постоянно ползущий вверх счёт за каждую новую отслеживаемую доставку.

Стоит держать в уме и обратную сторону: на своём сервере обслуживание — ваша зона ответственности. Обновления, бэкапы базы с историей поездок, мониторинг доступности WebSocket-эндпоинта делаете вы или ваш админ, а не поддержка SaaS. Для команды, у которой уже есть сервер под другие задачи магазина или CRM, добавить туда трекинг — небольшой инкремент работы. Для команды без серверной инфраструктуры это отдельная статья расходов на администрирование, которую стоит учитывать в сравнении, а не сравнивать голую стоимость VPS с голой абонплатой SaaS.

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

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

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

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

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

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

Нужен ли отдельный сервер только под трекинг, или можно разместить рядом с остальным бэкендом магазина?

Можно и нужно размещать рядом, если ресурсов хватает — трекинг курьеров по нагрузке лёгкий сервис, отдельная машина под него оправдана только при действительно большом парке курьеров или когда вы сознательно разносите нагрузку разных сервисов для изоляции сбоев.

Что делать, если курьер выключил геолокацию или у него сел телефон?

Backend должен явно отслеживать «время последнего обновления» и после истечения таймаута (например, пары минут без новых координат) помечать курьера как offline, а не молча показывать клиенту застывшую последнюю точку без пояснений — это база доверия к трекингу.

Можно ли обойтись без вебсокетов и просто обновлять страницу раз в 10 секунд через обычный запрос?

Да, это рабочий и более простой в поддержке вариант, особенно на старте. Разница в основном в нагрузке на сервер при большом числе одновременных клиентов и в задержке обновления — для небольшой и средней службы доставки разница на практике почти незаметна.

Как быть с точностью GPS в плотной городской застройке?

Точность GPS в каньонах из высотных зданий объективно хуже, чем на открытой местности, и это ограничение физики сигнала, а не софта. Единственное, что можно сделать программно — не показывать клиенту точку с плохой заявленной точностью как окончательную, а сглаживать резкие скачки или явно обозначать приблизительность позиции.

Стоит ли использовать PostGIS, если задача — просто показать курьера на карте?

Для простого показа точки и расчёта примерного расстояния по прямой — не обязательно, обычные DOUBLE PRECISION-колонки с широтой и долготой справляются. PostGIS оправдан, когда появляются задачи вроде «курьер вошёл в зону доставки» или расчётов по полигонам — тогда его геопространственные индексы дают заметный выигрыш.

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

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

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