SSD Nodes Learn 🎉 VPS от $5.50/мес
Руководства Matt ConnorАвтор: Matt Connor

Как самостоятельно развернуть OpenAnalytics на VPS

Узнайте реальные системные требования для установки OpenAnalytics. Вам потребуются 4 GB RAM, 25 GB диска, Docker Compose и четыре DNS-записи для корректной настройки стека.

Требования к ресурсам перед началом работы

Для самостоятельного размещения OpenAnalytics вам потребуется Linux VPS с объемом оперативной памяти около 4 GB, 25 GB свободного места на диске, Docker с плагином Compose и четыре DNS-записи, уже указывающие на ваш сервер. Это честное описание требований, и оно должно быть приведено до выполнения первой команды, а не после неё.

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

Если вы искали решение в виде одного бинарного файла и одного конфигурационного файла, то это не тот случай. GoatCounter — это вариант с одним бинарным файлом в данной категории: один исполняемый файл на Go, по умолчанию используется SQLite, внешняя база данных не требуется вовсе. Более тяжелый стек предоставляет вам воронки продаж, показатели Web Vitals, атрибуцию доходов из вашего собственного аккаунта Stripe и сервер MCP (model context protocol). Выбор между инструментами аналитики для самостоятельного размещения — это статья, в которой взвешиваются данные компромиссы. Данное руководство предполагает, что вы уже сделали свой выбор.

Сначала укажите четыре DNS-записи на сервер

Четыре поддомена должны разрешаться в публичный IP-адрес сервера до начала любых действий. Caddy запрашивает сертификаты Let's Encrypt при первом запуске, и проверка завершится ошибкой, если имя еще не разрешается.

  • app.example.com обслуживает панель управления.
  • api.example.com обслуживает API и OAuth-коллбэки.
  • c.example.com обслуживает сборщик и скрипт трекера.
  • rt.example.com обслуживает поток данных в реальном времени.

Используйте четыре A-записи или одну A-запись и три CNAME-записи, указывающие на неё. Проверьте результат с помощью dig +short app.example.com перед продолжением. Имя, которое вы добавили минуту назад, может всё ещё кэшироваться как NXDOMAIN тем резолвером, который использует Let's Encrypt. Если первая попытка получения сертификата не удалась, стоит подождать и изучить логи Caddy. Повторный запуск установки не ускоряет распространение DNS.

Развертывание OpenAnalytics с помощью Docker Compose

Выберите тегированный релиз. В ветке по умолчанию ведется разработка, а теги релизов соответствуют опубликованным образам. Приведенные ниже команды предполагают, что Docker и плагин Compose уже установлены; процесс описан в руководстве запуск сервисов Docker Compose на VPS.

git clone https://github.com/OpenLabs-so/openanalytics
cd openanalytics
git checkout "$(git tag -l 'v*' --sort=-v:refname | sed '/-/d' | head -1)"
cd infra/selfhost
./generate-secrets.sh --domain example.com --email you@example.com --with-geoip
docker compose pull && docker compose up -d

Флаг sed '/-/d' в команде checkout исключает предварительные релизы, поэтому вы переключитесь на актуальную стабильную версию, а не на релиз-кандидат. --with-geoip загружает базу данных городов DB-IP во время генерации. Если пропустить этот шаг, для всех событий будет установлено значение country: null, и карта географии останется пустой. Вы можете добавить базу позже: выполните infra/selfhost/geoip/fetch-dbip.sh, установите GEOIP_DB_PATH=/geoip/dbip-city-lite.mmdb в файле env/collector.env, а затем пересоздайте коллектор командой docker compose up -d --force-recreate collector. Эта база данных обновляется ежемесячно, поэтому повторяйте загрузку каждый месяц, иначе данные о городах станут неактуальными.

Создайте резервные копии сгенерированных секретов перед продолжением

Генератор создает три элемента. .env содержит доменные имена и ссылки на образы. env/*.env содержит по одному файлу секретов для каждого сервиса. docker-compose.override.yml содержит три пары ключей Ed25519 в виде YAML-блоков, так как многострочный формат PEM не может находиться в файле переменных окружения. Все эти файлы игнорируются git, и ни один из них невозможно восстановить с теми же значениями.

Скопируйте эти файлы с сервера прямо сейчас. Потеря каждого из них влечет за собой конкретные последствия:

  • Потеря паролей хранилища приведет к невозможности доступа к Postgres и ClickHouse; сброс возможен только изнутри контейнеров.
  • Потеря OA_CREDENTIAL_KEYRING делает невозможным восстановление всех сохраненных учетных данных сторонних сервисов, поэтому каждому пользователю, подключившему аккаунт Stripe, придется подключать его заново.
  • Потеря ANONYMOUS_IDENTITY_SECRET приводит к сбросу идентификации посетителей: все вчерашние посетители будут считаться новыми, и этот разрыв будет виден на графиках.
  • Потеря AUTH_SECRET приводит к аннулированию всех сессий, поэтому всем пользователям придется авторизоваться снова.
  • Потеря закрытого ключа подписи требует ротации пары. Данные при этом не теряются.

Два секрета должны быть идентичны побайтово в двух разных файлах каждый. ANONYMOUS_IDENTITY_SECRET присутствует в collector.env и worker.env, так как коллектор вычисляет хеш посетителя, а воркер записывает его. OA_CREDENTIAL_KEYRING присутствует в api.env и worker.env. Все остальные секреты намеренно ограничены областью действия ровно одного сервиса; если сервису передается секрет, который он не должен использовать, сервис завершает работу вместо запуска.

Запуск стека и проверка состояния

grep OA_IMAGE .env
docker compose pull
docker compose up -d
docker compose logs -f migrate
docker compose ps

migrate применяет схемы Postgres и ClickHouse, после чего завершает работу, поэтому остановленное состояние контейнера migrate является корректным. tracker-build компилирует oa.js в том, который обслуживает Caddy, и также завершает работу. Статус всех остальных сервисов должен быть healthy в docker compose ps. Если сервис перезапускается в цикле, это почти всегда означает ошибку проверки окружения; лог выводит все проблемы единым списком, а не по одной на каждую перезагрузку. Две наиболее частые причины: оставленная пустой переменная, которая отклоняется вместо того, чтобы считаться не заданнной, и секрет, помещенный не в тот файл сервиса.

Для архитектуры arm64 или при сборке из ветки готовые образы отсутствуют, поэтому сборка выполняется локально с помощью docker compose up -d --build. На хосте с 4 GB оперативной памяти процесс сборки может прерваться из-за нехватки ресурсов. Сначала добавьте swap, который потребуется только на время сборки:

fallocate -l 4G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab

Сборка занимает около 10 минут. Загрузка образов занимает несколько минут, именно поэтому существуют релизные образы.

Зарегистрируйте первую учетную запись немедленно

Откройте https://app.example.com. В развертывании, в которое еще никто не входил, форма входа не отображается: вместо нее предлагается создать первую учетную запись. Эта учетная запись навсегда становится привилегированной и является единственной, имеющей доступ к экрану настроек развертывания. Как только она создана, маршрут начинает отвечать 409, поэтому никто другой не сможет получить доступ после вас. Выполните это сразу же, как только стек станет работоспособным, а не через неделю.

Установка трекера

Добавьте сайт в панели управления, и вы получите тег. Его формат фиксирован:

<script
  async
  src="https://c.example.com/oa.js"
  data-key="YOUR_TRACKING_KEY"
  data-collector="https://c.example.com"
></script>

Разместите его в заголовке страницы (head). Ключ отслеживания является публичным по своей природе, поэтому его размещение в HTML, где его может прочитать любой, является нормальной практикой. Скрипт устанавливает window.oa, а вызовы вроде oa("track", ...) ставятся в очередь заглушкой и отправляются сразу после загрузки файла, поэтому пользовательское событие, вызванное на раннем этапе, не будет потеряно. Если объект window.oa уже используется другим элементом на странице, трекер установится как window.openanalytics.

Затем проверьте весь путь от начала до конца:

curl -s https://c.example.com/oa.js -o /dev/null -w '%{http_code} %{size_download}\n'
curl -s https://api.example.com/health | head -c 200
docker compose logs --tail=50 worker | grep -i batch

Первая команда должна вывести 200 и несколько килобайт данных. Загрузите страницу вашего сайта, а затем в течение нескольких секунд найдите строку пакета в логе воркера. Коллектор отвечает 202 в момент принятия события, а 202 означает, что событие поставлено в очередь, а не сохранено. Именно воркер переносит события в ClickHouse. Если события принимаются, но не отображаются в панели управления, значит, воркер заблокирован; подтверждением этого служит постоянно растущая глубина очереди Valkey. Частые причины — неверные учетные данные ClickHouse в worker.env или отсутствие прав доступа к таблице, добавленной в ходе недавней миграции.

Публичный доступ к сборщику и защита панели управления паролем

Caddy поставляется внутри файла compose и самостоятельно получает сертификаты для всех четырёх имён, поэтому стандартный путь не требует настройки прокси. Если на сервере уже работает обратный прокси-сервер nginx, используйте вместо него предоставленный infra/selfhost/nginx.conf.example и сохраните настройки обработки заголовков без изменений:

proxy_set_header X-Real-IP $remote_addr;
proxy_set_header CF-Connecting-IP "";
proxy_set_header True-Client-IP "";
proxy_set_header Fly-Client-IP "";

Сборщик вычисляет хеш ежедневных посетителей на основе IP-адреса клиента, поэтому он должен получать этот адрес напрямую из соединения, а не из заголовка. Передача CF-Connecting-IP через недоверенный узел позволяет любому отправителю подделать адрес, что одновременно искажает данные геолокации и завышает количество посещений.

Доступ чётко разделяется по именам хостов. c. и rt. должны быть доступны каждому посетителю любого сайта, который вы отслеживаете, поэтому никогда не устанавливайте базовую аутентификацию или IP-фильтры (allowlist) перед этими двумя адресами. app. и api. должны быть доступны только авторизованным пользователям. Панель управления защищается встроенными средствами аутентификации приложения: вход по паролю включён по умолчанию через AUTH_PASSWORD_SIGNIN=enabled в env/api.env, а кнопки входа через Google или GitHub появляются только при наличии идентификатора клиента (client ID) и секретного ключа (client secret) для соответствующего провайдера. Магические ссылки (magic links) требуют настройки почтового транспорта; без него API лишь записывает отправку в исходящую очередь, поэтому письма не доставляются, но и ошибки не возникают.

Один параметр определяет работоспособность панели управления. AUTH_TRUSTED_ORIGINS в env/api.env должен в точности соответствовать источнику (origin) панели управления. Если он указан неверно или отсутствует, API не отправляет заголовки CORS (cross-origin resource sharing), браузер отклоняет все запросы, и вы получаете панель, которая отрисовывает интерфейс, но не отображает данные, в то время как docker compose ps сообщает, что всё работает исправно.

Пока вы работаете с конфигурацией прокси, позаботьтесь об автоматизированном трафике. Поисковые роботы обращаются к сборщику так же, как и обычные пользователи, и их просмотры страниц попадают в ClickHouse, искажая вашу статистику. Блокировка AI-краулеров на уровне сервера позволяет отсечь часть этого трафика до того, как он повлияет на точность данных и заполнение диска.

Файлы cookie не используются. Идентификатор посетителя представляет собой соленый хеш, соль меняется ежедневно, а исходные IP-адреса никогда не сохраняются. Геолокация определяется локально по файлу DB-IP на вашем диске, поэтому данные о посетителе не покидают хост.

Это дает отсутствие идентификатора, сохраняемого на устройстве посетителя, что является именно тем фактором, который подпадает под действие правил ЕС ePrivacy о согласии на отслеживание. По этой причине конфигурации, работающие только с агрегированными данными, часто используются без баннера о согласии. GDPR по-прежнему регулирует все, что вы сохраняете и как долго, а решение по вашему случаю принимает ваш юрист, а не README.

Цена этого — невозможность отслеживания идентификатора в течение нескольких дней. Ротация соли означает, что человек, посетивший сайт в понедельник и снова в среду, будет засчитан как два разных посетителя; это сделано намеренно и не имеет обходных путей. Ежедневные показатели уникальных посетителей точны. Еженедельные и ежемесячные показатели уникальных посетителей формируются на основе ежедневных и будут завышать охват, поэтому любые долгосрочные показатели «вернувшихся посетителей» не соответствуют своему названию. Сессии и пути перемещения надежны в рамках одного дня. Ротация ANONYMOUS_IDENTITY_SECRET имеет тот же эффект, что и смена суток, поэтому рассматривайте эту ротацию как изменение данных, а не как рутинную очистку.

Коллектор учитывает заголовки Do Not Track и Global Privacy Control — сигналы браузера, которые сообщают сайту о запрете на продажу или передачу персональных данных. Тег скрипта имеет собственные переключатели для тех же целей: data-respect-gpc, data-respect-dnt и data-require-consent, который приостанавливает сбор данных до получения согласия и запоминает ответ в localStorage под ключом oa.consent. Установка data-storage="none" полностью отключает хранилище браузера.

Почему диск заполняется через полгода работы

Это основная причина выхода из строя серверов с self-hosted аналитикой, и события здесь обычно ни при чем.

Начните с образов. Релиз публикует десять из них, что занимает примерно 13 GB на диске. Обновление загружает новое поколение до того, как удалит старое, поэтому некоторое время вы храните две версии. Это составляет большую часть требования в 25 GB еще до того, как поступит первый просмотр страницы.

Затем снимки состояния (snapshots). snapshot.sh останавливает стек, архивирует оба тома данных вместе со всеми секретами и перезапускает систему. Холодные копии — единственный безопасный вариант в данном случае, так как ClickHouse объединяет части данных в фоновом режиме, и копия, сделанная во время слияния, будет несогласованной. upgrade.sh автоматически создает такую копию перед каждым обновлением, поэтому архивы накапливаются на том же диске, пока вы не ограничите их количество.

./snapshot.sh create --label before-something-risky
./snapshot.sh list
./snapshot.sh --keep 3

На хосте, где место заканчивается, удалите предыдущее поколение перед обновлением. Это безопасно во время работы стека, так как образы, на которых работают запущенные контейнеры, по-прежнему используются:

docker image prune -a -f

Теперь о самих событиях. ClickHouse сильно сжимает колоночные данные, поэтому объем необработанных событий растет медленнее, чем ожидается, а таблицы свертки (rollup tables), которые считывает дашборд, малы по сравнению с основной таблицей. Измеряйте, а не гадайте:

docker system df -v
docker compose exec clickhouse df -h /var/lib/clickhouse

Чтобы получить данные по каждой таблице, выполните это с учетными данными ClickHouse, которые генератор записал в infra/selfhost/env/:

SELECT table, formatReadableSize(sum(bytes_on_disk)) AS size, sum(rows) AS row_count
FROM system.parts
WHERE active
GROUP BY table
ORDER BY sum(bytes_on_disk) DESC;

Снимите показания на первой неделе и повторите на четвертой. Две точки дадут вам скорость роста, а скорость роста подскажет, когда нужно увеличивать объем диска. В руководстве по self-hosting по состоянию на август 2026 года нет настроек хранения или времени жизни (time-to-live) для необработанных событий, поэтому рассчитывайте размер диска исходя из измеренной скорости, а не в расчете на то, что старые строки удалятся сами.

Стоит знать об одной ловушке при удалении, прежде чем она сработает. Удаление сайта или аккаунта ставит задачу в очередь для воркера, а этому воркеру требуются установленные CLICKHOUSE_MAINTENANCE_USER и CLICKHOUSE_MAINTENANCE_PASSWORD, а также существующий в ClickHouse соответствующий пользователь oa_maintenance. Без них удаление остается в очереди навсегда. Сайт исчезает из дашборда, а все строки остаются на диске, поэтому создается видимость очистки, но место не освобождается.

Обновления и три вида издержек

git fetch --tags
git checkout "$(git tag -l 'v*' --sort=-v:refname | sed '/-/d' | head -1)"
cd infra/selfhost
./upgrade.sh

upgrade.sh выводит три вида издержек перед выполнением действий. Простой системы — это реальность: события, отправленные во время остановки сборщика, теряются, так как трекер не выполняет повторные попытки. Откат приводит к потере данных, поскольку rollback.sh --to backups/<snapshot> полностью заменяет оба хранилища и отбрасывает все строки, записанные после создания снимка. Дисковое пространство — это третья издержка, связанная с накоплением снимков, описанным выше.

Легко допустить ошибки в двух правилах перезапуска. Запускайте шлюз запросов (query gateway) перед API, так как более новая версия API отправляет поля запроса, которые старый шлюз отклонит. А для ClickHouse требуется пересоздание, а не перезапуск, поскольку docker compose restart повторно использует исходное окружение контейнера и молча игнорирует ваши правки:

docker compose up -d --force-recreate clickhouse

Панель управления (dashboard) содержит аналогичную ловушку. Три источника NEXT_PUBLIC_* в env/web.env компилируются в бандл браузера и подставляются при запуске контейнера, поэтому панель управления, обращающаяся к неверному имени хоста, исправляется с помощью docker compose up -d --force-recreate web, а не restart. В логе веб-контейнера выводятся источники, с которыми он был запущен — это самый быстрый способ убедиться, что исправление применено.

Если ClickHouse отказывается запускаться после изменения конфигурации, прочитайте первую строку его лога. Строка, начинающаяся с oa-entrypoint:, означает, что точка входа (entrypoint) отклоняет установленное вами значение. Любое другое сообщение обычно означает, что файл конфигурации содержит невалидный XML; самая частая причина — двойной дефис внутри XML-комментария, что недопустимо.

Лицензия AGPL-3.0 и наименование

Код распространяется по лицензии AGPL-3.0. Запуск неизмененного кода для собственных сайтов не накладывает никаких обязательств по публикации. Обязательства возникают, если вы модифицируете код и запускаете эту измененную версию как сетевой сервис: лицензия требует предоставить пользователям такого сервиса доступ к измененному исходному коду. Это касается предоставления клиентам доступа к дашбордам на вашем экземпляре, а также включения кода в состав коммерческих продуктов. Публикация изменений в открытом форке полностью удовлетворяет этому требованию без дополнительных процедур.

Бренд отделен от кода. Название "OpenAnalytics" и домен проекта указывают на экземпляр, управляемый авторами, и не являются частью лицензионного соглашения. Ваше развертывание использует программное обеспечение без использования бренда, поэтому дайте сервису собственное имя, прежде чем предоставлять его платным клиентам.

FAQ

Можно ли запустить OpenAnalytics на VPS с 1 ГБ ОЗУ?

Нет. Проекту требуется около 4 ГБ оперативной памяти и 25 ГБ свободного места на диске, так как один развертываемый экземпляр запускает шесть прикладных сервисов вместе с Postgres, ClickHouse и двумя экземплярами Valkey. Один только ClickHouse — это ресурсоемкий процесс. На сервере с 1 ГБ ОЗУ контейнеры запускаются, но затем OOM-killer ядра завершает один из них, обычно ClickHouse. Если 1 ГБ — это жесткое ограничение, используйте инструмент в виде одного бинарного файла, например GoatCounter, который работает на SQLite без внешних баз данных.

Это вопрос к вашему юристу, но технические факты на вашей стороне. Файлы cookie не используются, идентификатор посетителя — это ежедневно меняющийся соленый хеш, а исходные IP-адреса никогда не сохраняются, поэтому для идентификации посетителя не записывается ничего постоянного. GDPR по-прежнему регулирует, что вы храните и как долго. Если вы хотите явно запрашивать согласие на сбор данных, установите data-require-consent в теге скрипта: трекер не будет собирать данные до получения согласия, а ответ будет храниться в localStorage в рамках oa.consent.

Почему события возвращают 202, но не появляются на панели мониторинга?

202 означает, что коллектор принял и поставил событие в очередь, а не то, что он его сохранил. Воркер выгружает эту очередь в ClickHouse, поэтому пустая панель мониторинга при успешных запросах указывает на проблемы с воркером. Прочитайте docker compose logs --tail=50 worker и отслеживайте размер очереди в Valkey. Если очередь постоянно растет, значит, воркер заблокирован; типичные причины — неверные учетные данные ClickHouse в worker.env или отсутствие прав доступа к таблице, созданной в ходе недавней миграции.

Почему панель мониторинга пуста, хотя все контейнеры работают исправно?

Сначала проверьте AUTH_TRUSTED_ORIGINS в env/api.env. Значение должно в точности совпадать с источником (origin) панели мониторинга. Если это не так, API не отправляет заголовки CORS, браузер отклоняет все вызовы, и вы видите работающий интерфейс без данных. Второе, что нужно проверить, — это три значения NEXT_PUBLIC_* в env/web.env, которые подставляются при запуске веб-контейнера. Для их исправления требуется docker compose up -d --force-recreate web, так как обычный перезапуск сохраняет старые значения.

Мешает ли AGPL-3.0 предлагать этот сервис клиентам?

Нет, она накладывает только одно условие. Используйте код без изменений, и вы никому ничего не должны. Если вы изменили код и предоставляете эту версию как сервис для других людей, вы обязаны предоставить этим пользователям доступ к вашему измененному исходному коду (публичный форк удовлетворяет этому требованию). Кроме того, название "OpenAnalytics" не лицензируется вместе с кодом, поэтому для всего, что вы продаете, нужно придумать собственное имя.