SSD Nodes Learn Hosting plans →
Посібники Matt ConnorВід Matt Connor · Оновлено 2026-08-28

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

Потрібні 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 policies. Лише один процес, query gateway, може читати ClickHouse. Перед виконанням кожного запиту він перевіряє Ed25519-підпис у query envelope.

Якщо вам потрібні були один binary і один config file, цей варіант не підходить. GoatCounter — це варіант із single binary у цій категорії: один Go executable, SQLite за замовчуванням і взагалі без зовнішньої бази даних. Важчий стек дає funnels, web vitals, revenue attribution із власного 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 у DNS-resolver, який використає Let's Encrypt. Тому після невдалої першої спроби отримання сертифіката варто зачекати та переглянути журнали Caddy. Повторний запуск інсталяції не пришвидшує поширення DNS-змін.

Як самостійно розгорнути OpenAnalytics за допомогою Docker Compose

Перейдіть на позначений тегом реліз. У гілці за замовчуванням триває розроблення, а опубліковані images фактично відповідають тегу релізу. Наведені нижче команди передбачають, що 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 відкидає pre-release теги, тому ви переходите на найновішу стабільну версію, а не на release candidate. Параметр --with-geoip завантажує базу даних міст DB-IP під час генерації. Якщо пропустити цей параметр, кожна подія матиме значення країни 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-ігнорування, і жоден із цих об’єктів не можна повторно згенерувати з такими самими значеннями.

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

  • Якщо втратити паролі сховищ, ви втратите доступ до 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, а потім також завершує роботу. Усе інше має мати стан 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

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

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

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

Встановіть tracker

Додайте сайт у dashboard, і він надасть вам tag. Його формат фіксований:

<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, tracker натомість встановлює window.openanalytics. Якщо той самий сайт також доступний як onion service, не додавайте tag до тієї збірки, оскільки скрипт, завантажений із 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 і кілька кілобайтів даних. Відкрийте сторінку на своєму сайті, а потім протягом кількох секунд знайдіть у журналі worker рядок про пакет. Collector повертає 202 одразу після прийняття event, а 202 означає, що подію поставлено в чергу, але ще не збережено. Саме worker переміщує події до ClickHouse. Якщо події приймаються, але в dashboard нічого не з’являється, worker заблокований. Це підтверджує зростання довжини черги Valkey. Зазвичай причина полягає в неправильних облікових даних ClickHouse у worker.env або у відсутньому grant для таблиці, яку щойно додала міграція.

Не закривайте 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, тому воно не доставляється, але й помилки не виникає. Якщо інші ваші self-hosted застосунки вже працюють за єдиним входом Authentik, заздалегідь вирішіть, чи долучати до нього цей dashboard, чи залишити для нього окремі облікові записи. Перший обліковий запис, створений тут, назавжди матиме привілейований статус.

Один параметр визначає, чи працюватиме 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 немає. Ідентифікатор відвідувача — це хеш із сіллю. Сіль змінюється щодня, а необроблені IP-адреси ніколи не зберігаються. Геолокація визначається локально за файлом DB-IP на вашому диску, тому жоден запит про відвідувача не залишає хост. Локальне виконання запитів усуває постачальника, але не самі дані. Це те саме обмеження, яке виникає, коли ви запускаєте власний екземпляр SearXNG і 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 сервер аналітики, а події зазвичай не є причиною.

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

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

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

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

docker image prune -a -f

Далі — самі події. ClickHouse ефективно стискає колонкові дані, тому обсяг необроблених подій зростає повільніше, ніж зазвичай очікують, а таблиці з агрегованими даними, які читає dashboard, малі порівняно з таблицею необроблених даних. Не припускайте, а вимірюйте:

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;

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

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

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

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

upgrade.sh виводить три витрати перед виконанням операції. Простій є реальним: події, які надходять, коли collector не працює, втрачаються, оскільки 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 діє така сама схема. Три джерела NEXT_PUBLIC_* у env/web.env вбудовуються в browser bundle і підставляються під час запуску контейнера, тому неправильний hostname у dashboard виправляють за допомогою docker compose up -d --force-recreate web, а не restart. Журнал web container виводить джерела, з якими він запустився. Це найшвидший спосіб перевірити, що зміна застосувалася.

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

AGPL-3.0 і назва

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

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

FAQ

Чи можна запустити OpenAnalytics на VPS із 1 GB?

Ні. Проєкту потрібно близько 4 GB RAM і 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 tag: tracker не збиратиме нічого, доки згоду не буде надано, і збереже відповідь у localStorage під oa.consent.

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

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

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

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

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

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