SSD Nodes Learn Hosting plans →
Руководства Matt ConnorАвтор: Matt Connor · Обновлено 2026-08-26

Установка OpenAnalytics на свой VPS: пошаговое руководство

Узнайте системные требования для запуска OpenAnalytics: 4 GB RAM, 25 GB диска, Docker и четыре DNS-записи. Разбор стека из ClickHouse, Postgres и Valkey перед установкой.

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

Для самостоятельного размещения 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 во время генерации. Если пропустить этот шаг, для всех событий будет указана пустая страна, и раздел географии останется пустым. Вы можете добавить базу позже: выполните 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. Если тот же сайт доступен также как onion-сервис, исключите этот тег из такой сборки, так как скрипт, загруженный с c.example.com, вернет посетителя Tor Browser в обычную сеть (clearnet) и свяжет два адреса в рамках одной загрузки страницы.

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

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-файла и самостоятельно получает сертификаты для всех четырех имен, поэтому для стандартного пути настройка прокси не требуется. Если на сервере уже работает reverse proxy на базе 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. должны быть доступны каждому посетителю любого сайта, который вы отслеживаете, поэтому не устанавливайте basic auth или IP allowlist перед этими двумя адресами. app. и api. должны быть доступны только авторизованным пользователям. Панель управления защищена встроенной аутентификацией приложения: вход по паролю включен по умолчанию через AUTH_PASSWORD_SIGNIN=enabled в env/api.env, а кнопки Google или GitHub отображаются только при наличии client ID и client secret для соответствующего провайдера. Для работы magic links требуется почтовый транспорт; без него API лишь записывает отправку в исходящую очередь, поэтому письма не доставляются, а ошибки не возникают. Если ваши другие self-hosted приложения уже находятся за единой точкой входа Authentik, заранее решите, будет ли эта панель управления использовать общую авторизацию или собственные учетные записи, так как первая созданная здесь учетная запись навсегда получает права администратора.

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

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

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

Это дает отсутствие идентификатора, сохраняемого на устройстве посетителя, что является ключевым фактором, подпадающим под действие правил EU 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 ещё до того, как поступит первый просмотр страницы.

Затем снимки состояния. 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

Дашборд содержит аналогичную ловушку. Три источника 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 (out-of-memory 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" не лицензируется вместе с кодом, поэтому для любого продукта, который вы продаете, нужно придумать собственное имя.