SSD Nodes Learn 🎉 VPS від $5.50/міс
Посібники Matt ConnorВід Matt Connor

Як розгорнути OpenAnalytics на VPS власноруч

Дізнайтеся про вимоги OpenAnalytics: ClickHouse, Postgres, Valkey, 4 GB RAM, 25 GB вільного місця та чотири DNS-записи перед інсталяцією.

Потрібні ресурси до першого кроку

Щоб розгорнути OpenAnalytics у власній інфраструктурі, потрібен Linux VPS приблизно з 4 GB RAM, 25 GB вільного дискового простору, Docker із Compose plugin і чотири DNS-записи, які вже вказують на цей сервер. Це основна вимога, і її слід зазначити перед першою командою, а не після неї.

Стек складається із шести application services і трьох сховищ даних. Postgres зберігає дані control plane: облікові записи, сайти, API keys і share links. ClickHouse зберігає необроблені події та rollup-дані, які читає dashboard. Valkey запускається двічі: один екземпляр працює як надійна черга подій, а другий — як кеш, втрату якого система може витримати, оскільки для цих двох завдань потрібні протилежні політики eviction. Лише один процес, query gateway, може читати ClickHouse. Перед виконанням кожного запиту він перевіряє Ed25519-підпис у його envelope.

Якщо вам потрібні один binary і один config file, цей варіант вам не підходить. GoatCounter — це варіант із одним binary у цій категорії: один Go executable, SQLite за замовчуванням і взагалі без зовнішньої бази даних. Важчий стек дає funnels, web vitals, атрибуцію доходу з вашого власного Stripe account і MCP (model context protocol) server. Порівняння self-hosted analytics tools — це стаття, у якій розглянуто цей компроміс. У цьому посібнику передбачається, що ви вже зробили вибір.

Спочатку спрямуйте чотири DNS-записи на сервер

Перед початком роботи чотири субдомени мають резолвитися в публічну IP-адресу сервера, оскільки під час першого запуску Caddy запитує сертифікати Let's Encrypt, а перевірка завершується помилкою, якщо ім’я ще не резолвиться.

  • app.example.com обслуговує dashboard.
  • api.example.com обслуговує API та OAuth callbacks.
  • c.example.com обслуговує collector і tracker script.
  • rt.example.com обслуговує realtime stream.

Створіть чотири A-записи або один A-запис і три CNAME-записи, що вказують на нього. Перед продовженням перевірте це за допомогою dig +short app.example.com. Ім’я, додане хвилину тому, ще може кешуватися як NXDOMAIN у resolver, який використає 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 відкидає теги попередніх версій, тому ви переходите до найновішої стабільної версії, а не до release candidate. --with-geoip завантажує базу даних міст DB-IP під час генерації. Якщо пропустити цей крок, кожна подія матиме значення country null, тому подання географії взагалі нічого не покаже. Пізніше базу можна додати, виконавши infra/selfhost/geoip/fetch-dbip.sh, задавши GEOIP_DB_PATH=/geoip/dbip-city-lite.mmdb у env/collector.env, а потім повторно створивши collector за допомогою docker compose up -d --force-recreate collector. Ця база даних оновлюється щомісяця, тому щомісяця повторюйте завантаження. Інакше дані про міста поступово застаріватимуть.

Створіть резервну копію згенерованих секретів, перш ніж продовжити

Генератор записує три об’єкти. .env містить доменні імена та посилання на образи. env/*.env містить по одному файлу секретів для кожного сервісу. docker-compose.override.yml містить три пари ключів Ed25519 як YAML block scalars, оскільки багаторядковий PEM не можна зберігати в env-файлі. Усе це виключено з Git за допомогою .gitignore, і жоден із цих об’єктів не можна повторно згенерувати з тими самими значеннями.

Скопіюйте ці файли з машини зараз. Втрата кожного з них має конкретні наслідки:

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

Два секрети мають бути побайтно ідентичними у двох файлах кожен. ANONYMOUS_IDENTITY_SECRET міститься у collector.env і worker.env, оскільки collector обчислює хеш відвідувача, а worker записує його. 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 у volume, який обслуговує Caddy, і також завершує роботу. Для всіх інших сервісів у docker compose ps має відображатися стан healthy. Якщо сервіс циклічно перезапускається, майже завжди не пройдено перевірку змінних середовища. Журнал виводить усі проблеми одним списком, а не по одній проблемі після кожного перезапуску. Дві типові причини: змінна залишилася порожньою, і її відхилено, а не трактовано як невстановлену; або секрет указано у файлі неправильного сервісу.

На arm64 або під час роботи з гілкою опублікованих images немає, тому їх потрібно зібрати локально за допомогою 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

Збірка триває приблизно ten minutes. Завантаження займає кілька хвилин, тому й існують release images.

Негайно створіть перший обліковий запис

Відкрийте https://app.example.com. Розгортання, у якому ще ніхто не виконав вхід, не показує форму входу: воно пропонує створити перший обліковий запис. Цей обліковий запис назавжди має привілейований статус і єдиний бачить екран налаштувань розгортання. Після його створення маршрут повертає 409, тому ніхто не зможе отримати доступ після вас. Зробіть це одразу, щойно стек буде готовий, а не наступного тижня.

Встановіть трекер

Додайте сайт у dashboard — після цього ви отримаєте тег. Його структура фіксована:

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

Розмістіть його в секції head сторінки. Tracking key навмисно є публічним, тому він має бути в HTML, де його може прочитати будь-хто. Скрипт встановлює window.oa, а виклики на кшталт oa("track", ...) спочатку додаються до черги через stub і виконуються після завантаження файлу, тому custom event, надісланий зарано, не втрачається. Якщо інший код на сторінці вже використовує 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 і кілька кілобайт. Відкрийте сторінку свого сайту, а потім протягом кількох секунд знайдіть у журналі worker рядок про пакет. Collector відповідає 202 одразу після прийняття події, а 202 означає, що подію поставлено в чергу, а не збережено. Саме worker переміщує події в ClickHouse. Якщо події приймаються, але в dashboard нічого не з’являється, worker заблокований. Це підтверджує постійне зростання довжини черги Valkey. Зазвичай причина полягає в неправильних облікових даних ClickHouse у worker.env або у відсутності дозволу на таблицю, яку щойно додала міграція.

Залиште collector загальнодоступним, а dashboard захистіть автентифікацією

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 "";

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

Доступ чітко розділено за іменем хоста. c. і rt. мають бути доступними кожному відвідувачу кожного сайту, який ви вимірюєте, тому не встановлюйте перед ними basic auth або список дозволених IP-адрес. app. і api. мають бути доступними лише користувачам, які ввійшли в систему. Dashboard захищає власна автентифікація застосунку: вхід за паролем увімкнено за замовчуванням через AUTH_PASSWORD_SIGNIN=enabled у env/api.env, а кнопки Google або GitHub відображаються лише тоді, коли для відповідного провайдера задано і client ID, і client secret. Для magic links потрібен поштовий транспорт. Без нього API лише записує повідомлення у outbox, тому воно не доставляється, але й помилка не виникає.

Один параметр визначає, чи працюватиме dashboard взагалі. AUTH_TRUSTED_ORIGINS у env/api.env має точно відповідати origin dashboard. Якщо значення неправильне або відсутнє, API не надсилає заголовки CORS (cross-origin resource sharing), браузер відхиляє кожен виклик, і dashboard відображає макет без даних, тоді як docker compose ps повідомляє, що все працює нормально.

Поки ви редагуєте конфігурацію проксі, налаштуйте обробку автоматизованого трафіку. Crawler-и звертаються до collector так само, як і звичайні користувачі, а їхні перегляди сторінок потрапляють у ClickHouse та вашу статистику. Блокування AI crawler-ів на сервері дає змогу не записувати частину такого трафіку в базу даних, перш ніж він негативно вплине на точність даних і використає дисковий простір.

Що тут означає cookieless і чого це вам коштує

Cookie немає. Ідентичність відвідувача — це hash із сіллю. Сіль змінюється щодня, а необроблені IP-адреси ніколи не зберігаються. Геолокація визначається локально за файлом DB-IP на вашому диску, тому жоден запит про відвідувача не залишає хост.

Перевага полягає у відсутності ідентифікатора, збереженого на пристрої відвідувача. Саме це зазвичай підпадає під вимоги ЄС щодо згоди відповідно до ePrivacy. Тому такі конфігурації лише з агрегованими даними часто працюють без банера згоди. GDPR і далі регулює дані, які ви зберігаєте, та строк їх зберігання. У конкретній ситуації рішення приймає ваш юридичний радник, а не README.

Недолік — відсутність ідентифікації між днями. Через щоденну зміну солі людина, яка відвідала сайт у понеділок, а потім у середу, за задумом буде врахована як два відвідувачі. Обійти це неможливо. Щоденні показники унікальних відвідувачів є коректними. Тижневі та місячні показники унікальних відвідувачів будуються на основі щоденних і тому завищують охоплення. Отже, будь-який показник «повторних відвідувачів» за тривалий період не вимірює те, що вказано в його назві. Сеанси та послідовності дій коректні в межах одного дня. Ротація ANONYMOUS_IDENTITY_SECRET має такий самий ефект, як межа дня, тому сприймайте її як зміну даних, а не як звичайне обслуговування.

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

Чому через шість місяців закінчується місце на диску

Саме це виводить з ладу self-hosted сервер аналітики, а події зазвичай не є причиною.

Почніть з images. Один release публікує десять images, які разом займають приблизно 13 GB на диску. Під час оновлення нове покоління завантажується до видалення старого, тому певний час на диску зберігаються два покоління. Це становить більшу частину необхідних 25 GB ще до надходження першого перегляду сторінки.

Далі — snapshots. snapshot.sh зупиняє stack, архівує обидва томи даних разом із кожним secret і запускає stack знову. Тут безпечними є лише холодні копії, оскільки ClickHouse у фоновому режимі об’єднує parts, а копія, створена під час merge, буде неузгодженою. upgrade.sh автоматично створює snapshot перед кожним оновленням, тому архіви накопичуються на тому самому диску, доки ви не обмежите їхню кількість.

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

Якщо на host майже не залишилося місця, видаліть попереднє покоління перед оновленням. Це безпечно, поки stack працює, оскільки images, на основі яких запущено containers, усе ще мають references:

docker image prune -a -f

Далі — самі події. ClickHouse добре стискає columnar data, тому обсяг raw events зростає повільніше, ніж очікує більшість користувачів, а rollup tables, які читає dashboard, невеликі порівняно з raw table. Вимірюйте, а не вгадуйте:

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

Щоб отримати показник для кожної table, виконайте цю команду з обліковими даними ClickHouse, які generator записав у 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;

Зніміть це значення на першому тижні, а потім повторіть вимірювання на четвертому. Двох точок достатньо, щоб визначити швидкість зростання, а вона покаже, коли потрібно збільшити обсяг тому. Станом на August 2026 self-hosting guide не описує параметр retention або time-to-live для raw events, тому розраховуйте обсяг диска за виміряною швидкістю зростання, а не припускайте, що старі рядки видаляються автоматично.

Перед видаленням варто знати про одну пастку. Видалення site або account ставить завдання в чергу для worker. Для його роботи потрібно встановити CLICKHOUSE_MAINTENANCE_USER і CLICKHOUSE_MAINTENANCE_PASSWORD, а в ClickHouse має існувати відповідний користувач oa_maintenance. Без цього завдання на видалення залишаються в черзі назавжди. Site зникає з dashboard, але всі рядки залишаються на диску, тому ви бачите ознаки очищення, але не звільняєте місце.

Оновлення та три види витрат

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

upgrade.sh перед виконанням операції показує три види витрат. Простій є реальним: події, які намагаються передати під час недоступності збирача, втрачаються, оскільки tracker не повторює спроби. Відкат призводить до втрати даних, оскільки rollback.sh --to backups/<snapshot> повністю замінює обидва сховища та відкидає всі рядки, записані після створення цього snapshot. Третя витрата — дисковий простір, який займає описаний вище стос snapshot.

Два правила перезапуску легко порушити. Спочатку запустіть query gateway, а потім API, оскільки новіший API надсилає поля запиту, які старіший gateway відхиляє. А ClickHouse потрібно пересоздати, а не перезапустити, оскільки docker compose restart повторно використовує початкове середовище контейнера та непомітно ігнорує внесену вами зміну:

docker compose up -d --force-recreate clickhouse

Dashboard має таку саму особливість, яку легко пропустити. Три origins NEXT_PUBLIC_* у env/web.env компілюються в browser bundle і підставляються під час запуску контейнера, тому dashboard, який звертається до неправильного hostname, виправляють за допомогою docker compose up -d --force-recreate web, а не restart. У журналі web container виводяться origins, з якими він запустився. Це найшвидший спосіб підтвердити, що зміну застосовано.

Якщо ClickHouse відмовляється запускатися після зміни конфігурації, прочитайте перший рядок його журналу. Рядок, що починається з oa-entrypoint:, означає, що entrypoint відхилив задане вами значення. В інших випадках зазвичай йдеться про некоректний XML у файлі конфігурації. Найпоширеніша причина — подвійний дефіс усередині XML-коментаря, що є неприпустимим.

AGPL-3.0 і назва

Код ліцензовано за AGPL-3.0. Запуск незміненого коду для власних сайтів не створює жодних зобов’язань щодо публікації. Зобов’язання виникає, коли ви змінюєте код і запускаєте змінену версію як мережевий сервіс: тоді ліцензія вимагає запропонувати користувачам цього сервісу вихідний код ваших змін. Це стосується надання клієнтам доступу до інформаційних панелей у вашому екземплярі, а також включення коду до продукту, який ви продаєте. Якщо зберігати зміни в публічному fork, цієї вимоги буде дотримано без додаткових процедур.

Бренд і код — окремі речі. Назва "OpenAnalytics" і домен, на якому розміщено проект, ідентифікують екземпляр, яким керують його автори, і не входять до наданих ліцензією прав. Ваше розгортання запускає програмне забезпечення без цього бренду, тому перед наданням сервісу платним клієнтам дайте йому власну назву.

FAQ

Чи можна запустити OpenAnalytics на VPS із 1 GB оперативної пам’яті?

Ні. Проєкту потрібно приблизно 4 GB оперативної пам’яті та 25 GB вільного дискового простору, оскільки одне розгортання запускає шість сервісів застосунку разом із Postgres, ClickHouse і двома екземплярами Valkey. Сам ClickHouse не є малим процесом. На сервері з 1 GB контейнери запускаються, після чого kernel out-of-memory killer завершує один із них, зазвичай ClickHouse. Якщо тариф на 1 GB є жорстким обмеженням, використовуйте однобінарний інструмент на кшталт GoatCounter, який працює на SQLite без зовнішньої бази даних.

Це питання до вашого юриста, але технічні факти на вашу користь. Cookie не використовується, ідентифікатор відвідувача є salted hash, який щодня змінюється, а вихідні IP-адреси ніколи не зберігаються. Тому для ідентифікації відвідувача не записуються довговічні дані. GDPR все одно регулює, які дані ви зберігаєте та як довго їх утримуєте. Якщо потрібно явно дозволяти збір даних лише після згоди, задайте data-require-consent у тегу script: tracker не збиратиме нічого, доки згоду не буде надано, а відповідь зберігатиме в localStorage у межах oa.consent.

Чому події повертають 202, але ніколи не з’являються на dashboard?

202 означає, що collector прийняв подію та поставив її в чергу, а не те, що він зберіг її. Worker забирає події з цієї черги та записує їх у ClickHouse. Тому порожній dashboard за успішних запитів вказує на проблему з worker. Прочитайте docker compose logs --tail=50 worker і стежте за глибиною черги Valkey. Черга, яка постійно зростає, означає, що worker заблокований. Найчастіші причини — неправильні облікові дані ClickHouse у worker.env або відсутні права на таблицю, створену під час нещодавньої міграції.

Чому dashboard порожній, якщо всі контейнери працюють без помилок?

Спочатку перевірте AUTH_TRUSTED_ORIGINS у env/api.env. Воно має точно збігатися з origin dashboard. Якщо це не так, API не додає CORS-заголовки, тому браузер відхиляє кожен виклик. У результаті ви бачите робочий інтерфейс без даних. Далі перевірте три значення NEXT_PUBLIC_* у env/web.env. Вони підставляються під час запуску web-контейнера. Щоб виправити їх, потрібно виконати docker compose up -d --force-recreate web, оскільки звичайний restart зберігає старі значення.

Чи забороняє AGPL-3.0 пропонувати цей продукт клієнтам?

Ні, ліцензія встановлює одну умову. Якщо ви запускаєте код без змін, ви нікому нічого не винні. Якщо ви змінюєте код і запускаєте змінену версію як сервіс, яким користуються інші люди, ви повинні надати цим користувачам вихідний код своїх змін. Публічний fork виконує цю вимогу. Окремо, назва "OpenAnalytics" не ліцензується разом із кодом, тому для будь-якого комерційного продукту потрібно використовувати власну назву.