Cara Self-Host OpenAnalytics di VPS
Ketahui kebutuhan nyata OpenAnalytics: ClickHouse, Postgres, Valkey, RAM 4 GB, ruang kosong 25 GB, dan empat record DNS sebelum instalasi dimulai.
Jejak kebutuhan sebelum langkah pertama
Untuk melakukan self-hosting OpenAnalytics, Anda memerlukan Linux VPS dengan RAM sekitar 4 GB, ruang disk kosong 25 GB, Docker dengan plugin Compose, dan empat record DNS yang sudah mengarah ke server tersebut. Itulah persyaratan utamanya, dan informasi ini harus disampaikan sebelum perintah pertama, bukan setelahnya.
Stack ini terdiri atas enam service aplikasi dan tiga penyimpanan data. Postgres menyimpan control plane: akun, situs, API key, dan tautan berbagi. ClickHouse menyimpan event mentah dan hasil agregasi yang dibaca dashboard. Valkey berjalan dua kali, yaitu sebagai antrean event persisten dan sebagai cache yang datanya dapat dihapus, karena kedua fungsi tersebut memerlukan kebijakan eviction yang berlawanan. Hanya satu proses, yaitu query gateway, yang diizinkan membaca ClickHouse. Proses ini memverifikasi tanda tangan Ed25519 pada setiap query envelope sebelum menjalankannya.
Jika yang Anda inginkan adalah satu binary dan satu file konfigurasi, stack ini bukan pilihan yang tepat. GoatCounter adalah opsi single-binary dalam kategori ini: satu executable Go, SQLite secara default, dan tanpa database eksternal. Stack yang lebih berat ini menyediakan funnel, web vitals, atribusi pendapatan dari akun Stripe Anda sendiri, dan server MCP (model context protocol). Memilih antara tool analytics self-hosted adalah artikel yang membahas pertukaran tersebut. Panduan ini mengasumsikan Anda sudah menentukan pilihan.
Arahkan record DNS ke server terlebih dahulu
Empat subdomain harus mengarah ke IP publik server sebelum Anda memulai apa pun. Caddy meminta sertifikat Let's Encrypt saat pertama kali dijalankan, dan challenge akan gagal jika nama tersebut belum dapat di-resolve.
app.example.commenyajikan dashboard.api.example.commenyajikan API dan callback OAuth.c.example.commenyajikan collector dan skrip tracker.rt.example.commenyajikan stream realtime.
Gunakan empat record A, atau satu record A dan tiga CNAME yang mengarah ke record tersebut. Konfirmasikan dengan dig +short app.example.com sebelum melanjutkan. Nama yang baru Anda tambahkan semenit lalu masih dapat tersimpan dalam cache sebagai NXDOMAIN pada resolver yang kebetulan digunakan Let's Encrypt. Karena itu, jika upaya penerbitan sertifikat pertama gagal, tunggu dan periksa log Caddy. Menjalankan ulang instalasi tidak mempercepat propagasi DNS.
Cara melakukan self-hosting OpenAnalytics dengan Docker Compose
Checkout tag rilis. Branch default adalah tempat pengembangan berlangsung, sedangkan tag rilis sesuai dengan image yang dipublikasikan. Perintah di bawah mengasumsikan Docker dan plugin Compose sudah terpasang. Hal ini dibahas dalam menjalankan layanan Docker Compose pada 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 -dsed '/-/d' pada baris checkout mengecualikan tag prarilis. Dengan demikian, Anda menggunakan versi stabil terbaru, bukan release candidate. --with-geoip mengambil database kota DB-IP saat proses pembuatan berlangsung. Jika langkah ini dilewati, setiap event memiliki nilai negara null sehingga tampilan geografi tidak menampilkan data apa pun. Anda dapat menambahkannya nanti dengan menjalankan infra/selfhost/geoip/fetch-dbip.sh, menetapkan GEOIP_DB_PATH=/geoip/dbip-city-lite.mmdb di env/collector.env, lalu membuat ulang collector dengan docker compose up -d --force-recreate collector. Database tersebut diperbarui setiap bulan. Ulangi pengambilan setiap bulan agar data kota tetap akurat.
Cadangkan secrets yang dihasilkan sebelum melanjutkan
Generator menulis tiga hal. .env berisi nama domain dan referensi image. env/*.env berisi satu file secrets untuk setiap service. docker-compose.override.yml berisi tiga pasangan key Ed25519 sebagai YAML block scalar, karena PEM multi-baris tidak dapat disimpan dalam file env. Semua file tersebut diabaikan oleh git, dan tidak ada yang dapat dibuat ulang dengan nilai yang sama.
Salin file-file tersebut ke luar mesin sekarang. Setiap kehilangan memiliki dampak tertentu:
- Jika password store hilang, Anda tidak dapat mengakses Postgres dan ClickHouse. Password hanya dapat direset dari dalam container.
- Jika
OA_CREDENTIAL_KEYRINGhilang, semua kredensial pihak ketiga yang tersimpan tidak dapat dipulihkan. Akun Stripe yang pernah dihubungkan harus dihubungkan kembali. - Jika
ANONYMOUS_IDENTITY_SECREThilang, identitas pengunjung diatur ulang. Semua pengunjung kemarin dihitung sebagai pengunjung baru, dan perubahan ini terlihat pada chart. - Jika
AUTH_SECREThilang, semua session dibatalkan. Semua pengguna harus login kembali. - Jika private key untuk signing hilang, lakukan rotasi pada pasangan key tersebut. Tidak ada data yang hilang.
Dua secrets harus identik byte demi byte pada masing-masing dari dua file. ANONYMOUS_IDENTITY_SECRET muncul dalam collector.env dan worker.env, karena collector menghitung hash pengunjung dan worker menulisnya. OA_CREDENTIAL_KEYRING muncul dalam api.env dan worker.env. Semua secret lainnya sengaja dibatasi hanya untuk satu service, dan service yang menerima secret yang tidak boleh dimilikinya akan berhenti, bukan start.
Jalankan stack dan periksa hasilnya
grep OA_IMAGE .env
docker compose pull
docker compose up -d
docker compose logs -f migrate
docker compose psmigrate menerapkan skema Postgres dan ClickHouse, lalu berhenti. Karena itu, container migrate yang berhenti merupakan kondisi akhir yang benar. tracker-build mengompilasi oa.js ke dalam volume yang disajikan oleh Caddy, lalu ikut berhenti. Layanan lainnya harus membaca healthy di docker compose ps. Service yang terus-menerus restart hampir selalu gagal dalam validasi environment. Log menampilkan semua masalah dalam satu daftar, bukan satu masalah pada setiap restart. Dua penyebab yang paling umum adalah variabel yang dibiarkan kosong, yang akan ditolak dan bukan dianggap belum disetel, serta secret yang ditempatkan dalam file service yang salah.
Pada arm64 atau dari sebuah branch, tidak ada image yang dipublikasikan. Anda harus melakukan build secara lokal dengan docker compose up -d --build. Host dengan RAM 4 GB akan kehabisan memori di tengah proses build tersebut. Tambahkan swap terlebih dahulu. Swap hanya diperlukan selama proses build:
fallocate -l 4G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstabProses build memerlukan waktu sekitar sepuluh menit. Proses pull hanya memerlukan beberapa menit. Karena itu, image release disediakan.
Klaim akun pertama segera
Buka https://app.example.com. Deployment yang belum pernah digunakan untuk login tidak menampilkan formulir login. Sebagai gantinya, deployment tersebut menawarkan pembuatan akun pertama. Akun tersebut menjadi akun dengan hak istimewa secara permanen. Akun itu juga satu-satunya akun yang dapat melihat layar pengaturan deployment. Setelah akun tersebut dibuat, rute itu merespons dengan 409, sehingga tidak ada orang yang dapat masuk setelah Anda. Lakukan ini segera setelah stack berjalan dengan baik, bukan pada minggu berikutnya.
Instal tracker
Tambahkan situs di dashboard, lalu sistem akan memberikan tag. Bentuknya tetap:
<script
async
src="https://c.example.com/oa.js"
data-key="YOUR_TRACKING_KEY"
data-collector="https://c.example.com"
></script>Letakkan tag tersebut di bagian head halaman. Kunci pelacakan memang bersifat publik, sehingga kunci itu harus berada di HTML dan dapat dibaca siapa saja. Skrip tersebut memasang window.oa. Pemanggilan seperti oa("track", ...) dimasukkan ke antrean oleh stub, lalu dikirim setelah file selesai dimuat. Dengan demikian, event khusus yang dipicu lebih awal tidak hilang. Jika window.oa sudah digunakan oleh komponen lain di halaman, tracker memasang window.openanalytics sebagai gantinya.
Selanjutnya, periksa seluruh alur dari awal hingga akhir:
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 batchPerintah pertama seharusnya menampilkan 200 dan beberapa kilobyte. Muat halaman di situs Anda, lalu cari baris batch di log worker dalam beberapa detik. Collector mengembalikan 202 segera setelah menerima event. 202 berarti event berada dalam antrean, bukan sudah disimpan. Worker memindahkan event ke ClickHouse. Jika event diterima tetapi tidak muncul di dashboard, berarti worker terblokir. Kedalaman antrean Valkey yang terus meningkat mengonfirmasi kondisi tersebut. Penyebab yang umum adalah kredensial ClickHouse yang salah di worker.env atau grant yang belum diberikan pada tabel yang baru ditambahkan oleh migration.
Biarkan collector tetap publik dan lindungi dashboard dengan autentikasi
Caddy disertakan di dalam file compose dan memperoleh sertifikat untuk keempat nama tersebut secara mandiri, sehingga jalur default tidak memerlukan konfigurasi proxy tambahan. Jika server sudah menjalankan reverse proxy nginx, tempatkan stack di belakang infra/selfhost/nginx.conf.example yang disediakan, dan pertahankan penanganan header yang ada:
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 menghitung hash pengunjung harian dari IP klien, sehingga alamat tersebut harus diambil dari koneksi, bukan dari header. Meneruskan CF-Connecting-IP dari hop yang tidak tepercaya memungkinkan siapa pun mengklaim alamat apa pun. Hal ini sekaligus merusak geolokasi dan melebihkan jumlah pengunjung.
Akses terbagi dengan jelas berdasarkan hostname. c. dan rt. harus dapat diakses oleh setiap pengunjung dari setiap situs yang Anda ukur. Karena itu, jangan menempatkan basic auth atau daftar izin IP di depan kedua alamat tersebut. app. dan api. hanya perlu dapat diakses oleh pengguna yang login. Autentikasi bawaan aplikasi melindungi dashboard. Login dengan kata sandi aktif secara default melalui AUTH_PASSWORD_SIGNIN=enabled di env/api.env. Tombol Google atau GitHub hanya muncul jika client ID dan client secret untuk penyedia tersebut tersedia. Magic link memerlukan mail transport. Tanpa mail transport, API hanya menulis pesan pengiriman ke outbox. Tidak ada pesan yang terkirim dan tidak ada error.
Satu pengaturan menentukan apakah dashboard dapat berfungsi. AUTH_TRUSTED_ORIGINS di env/api.env harus sama persis dengan origin dashboard. Jika nilainya salah atau tidak ada, API tidak mengirim header CORS (cross-origin resource sharing). Browser menolak setiap panggilan. Akibatnya, dashboard hanya menampilkan tata letak tanpa data, sementara docker compose ps melaporkan bahwa semuanya sehat.
Saat mengedit konfigurasi proxy, tangani juga trafik otomatis. Crawler mengakses collector seperti pengunjung lain, dan page view mereka masuk ke ClickHouse serta ke dalam angka Anda. Memblokir crawler AI di server mencegah sebagian trafik tersebut masuk ke database sebelum mengurangi akurasi dan menghabiskan ruang disk.
Makna cookieless dalam konteks ini dan konsekuensinya
Tidak ada cookie. Identitas pengunjung berupa hash dengan salt, salt tersebut dirotasi setiap hari, dan alamat IP mentah tidak pernah disimpan. Geolokasi ditentukan secara lokal menggunakan file DB-IP pada disk Anda sendiri, sehingga tidak ada permintaan pencarian tentang pengunjung yang meninggalkan host.
Manfaatnya adalah tidak adanya identifier yang disimpan pada perangkat pengunjung. Hal inilah yang secara khusus membuat tracker tunduk pada aturan persetujuan ePrivacy di EU. Karena itu, konfigurasi yang hanya menghasilkan data agregat seperti ini biasanya dijalankan tanpa banner persetujuan. GDPR tetap berlaku untuk data yang Anda simpan dan jangka waktu penyimpanannya. Penentuan untuk kasus Anda tetap berada pada penasihat hukum Anda, bukan README.
Konsekuensinya adalah tidak ada identitas lintas hari. Rotasi salt berarti seseorang yang berkunjung pada hari Senin lalu kembali pada hari Rabu dihitung sebagai dua pengunjung. Ini memang dirancang demikian dan tidak memiliki workaround. Jumlah pengunjung unik harian tetap akurat. Jumlah pengunjung unik mingguan dan bulanan dibentuk dari data harian dan akan melebihkan jangkauan. Karena itu, angka "pengunjung kembali" untuk periode panjang tidak mengukur hal yang dinyatakan oleh labelnya. Sesi dan perjalanan pengguna dapat diandalkan dalam satu hari. Rotasi ANONYMOUS_IDENTITY_SECRET memiliki efek yang sama dengan batas pergantian hari. Perlakukan rotasi tersebut sebagai perubahan data, bukan pemeliharaan rutin.
Collector mematuhi Do Not Track dan Global Privacy Control, yaitu sinyal browser yang memberi tahu situs agar tidak menjual atau membagikan data pribadi. Tag script memiliki switch tersendiri untuk tujuan yang sama: data-respect-gpc, data-respect-dnt, dan data-require-consent, yang menahan seluruh pengumpulan hingga persetujuan diberikan dan mengingat jawaban tersebut dalam localStorage menggunakan key oa.consent. Pengaturan data-storage="none" menonaktifkan penyimpanan browser sepenuhnya.
Mengapa disk penuh setelah enam bulan
Inilah yang biasanya membuat server analytics self-hosted gagal, dan penyebabnya biasanya bukan event.
Mulai dari image. Satu release menerbitkan sepuluh image dengan total sekitar 13 GB di disk. Saat upgrade, image generasi baru di-pull sebelum generasi lama dihapus, sehingga untuk sementara Anda menyimpan dua generasi. Kondisi ini sudah memenuhi sebagian besar kebutuhan 25 GB, bahkan sebelum satu page view masuk.
Berikutnya, snapshot. snapshot.sh menghentikan stack, mengarsipkan kedua volume data beserta semua secret, lalu menjalankannya kembali. Salinan dalam kondisi berhenti adalah satu-satunya jenis yang aman di sini, karena ClickHouse menggabungkan part secara background dan salinan yang dibuat saat proses merge berlangsung tidak konsisten. upgrade.sh membuat snapshot secara otomatis sebelum setiap upgrade, sehingga arsip menumpuk pada disk yang sama sampai jumlahnya dibatasi.
./snapshot.sh create --label before-something-risky
./snapshot.sh list
./snapshot.sh --keep 3Pada host yang kapasitasnya hampir penuh, hapus generasi sebelumnya sebelum melakukan upgrade. Cara ini aman selama stack berjalan karena image yang digunakan container yang sedang berjalan masih direferensikan:
docker image prune -a -fBerikutnya, event itu sendiri. ClickHouse mengompresi data kolumnar dengan sangat baik, sehingga volume event mentah bertambah lebih lambat daripada perkiraan kebanyakan orang. Tabel rollup yang dibaca dashboard juga berukuran kecil dibandingkan tabel mentah. Lakukan pengukuran, bukan perkiraan:
docker system df -v
docker compose exec clickhouse df -h /var/lib/clickhouseUntuk melihat ukuran setiap tabel, jalankan perintah ini menggunakan kredensial ClickHouse yang ditulis oleh generator di bawah 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;Ambil hasil pengukuran pada minggu pertama, lalu ulangi pada minggu keempat. Dua titik data memberikan laju pertumbuhan. Laju pertumbuhan tersebut menunjukkan kapan volume perlu diperbesar. Hingga Agustus 2026, panduan self-hosting tidak mendokumentasikan pengaturan retention atau time-to-live untuk event mentah. Karena itu, tentukan ukuran disk berdasarkan laju yang Anda ukur. Jangan berasumsi bahwa baris lama akan kedaluwarsa sendiri.
Ada satu jebakan penghapusan yang perlu diketahui sebelum menimbulkan masalah. Penghapusan site atau account memasukkan pekerjaan ke antrean worker. Worker tersebut memerlukan CLICKHOUSE_MAINTENANCE_USER dan CLICKHOUSE_MAINTENANCE_PASSWORD yang telah diatur, serta user oa_maintenance yang sesuai di ClickHouse. Tanpa pengaturan tersebut, pekerjaan penghapusan akan terus berada dalam antrean. Site menghilang dari dashboard, tetapi semua baris tetap berada di disk. Hasilnya hanya tampak seperti pembersihan, tanpa ruang disk yang benar-benar kembali.
Pemutakhiran dan tiga biayanya
git fetch --tags
git checkout "$(git tag -l 'v*' --sort=-v:refname | sed '/-/d' | head -1)"
cd infra/selfhost
./upgrade.shupgrade.sh mencetak tiga biaya sebelum bertindak. Downtime adalah dampak nyata: event yang dicoba saat collector tidak aktif akan hilang karena tracker tidak mencobanya lagi. Rollback menyebabkan kehilangan data karena rollback.sh --to backups/<snapshot> mengganti kedua penyimpanan secara menyeluruh dan membuang setiap baris yang ditulis setelah snapshot tersebut dibuat. Disk adalah biaya ketiga, yaitu tumpukan snapshot yang dijelaskan di atas.
Dua aturan restart sering keliru diterapkan. Jalankan query gateway sebelum API karena API yang lebih baru mengirimkan field query yang ditolak oleh gateway versi lama. ClickHouse juga memerlukan recreate, bukan restart, karena docker compose restart menggunakan kembali environment asli container dan mengabaikan perubahan Anda secara diam-diam:
docker compose up -d --force-recreate clickhouseDashboard memiliki jebakan yang sama. Tiga origin NEXT_PUBLIC_* dalam env/web.env dikompilasi ke dalam browser bundle dan diganti saat container dimulai. Karena itu, dashboard yang memanggil hostname yang salah harus diperbaiki dengan docker compose up -d --force-recreate web, bukan dengan restart. Log web container mencetak origin yang digunakan saat container dimulai. Ini adalah cara tercepat untuk memastikan perbaikan sudah diterapkan.
Jika ClickHouse menolak untuk start setelah konfigurasi diedit, baca baris pertama log-nya. Baris yang diawali oa-entrypoint: menunjukkan bahwa entrypoint menolak nilai yang Anda tetapkan. Hal lain biasanya berarti file konfigurasi berisi XML yang tidak valid. Penyebab yang paling umum adalah tanda hubung ganda di dalam komentar XML, yang tidak diizinkan di sana.
AGPL-3.0, dan nama
Kode ini dilisensikan berdasarkan AGPL-3.0. Menjalankannya tanpa modifikasi untuk situs Anda sendiri sama sekali tidak menimbulkan kewajiban publikasi. Kewajiban mulai berlaku ketika Anda memodifikasi kode tersebut dan menjalankan versi yang telah dimodifikasi sebagai layanan jaringan. Lisensi kemudian mewajibkan Anda menawarkan kode sumber yang telah dimodifikasi kepada pengguna layanan tersebut. Hal ini mencakup penyediaan dashboard kepada klien pada instance Anda dan penggabungan kode tersebut ke dalam produk yang Anda jual. Menyimpan perubahan Anda dalam fork publik sudah memenuhi kewajiban tersebut tanpa proses tambahan.
Merek terpisah dari kode. Nama "OpenAnalytics" dan domain yang di-hosting oleh proyek mengidentifikasi instance yang dioperasikan oleh pembuatnya. Keduanya tidak termasuk dalam pemberian lisensi. Deployment Anda menjalankan perangkat lunak tersebut tanpa menggunakan mereknya. Karena itu, beri layanan tersebut nama sendiri sebelum menawarkannya kepada pelanggan berbayar.
FAQ
Apakah OpenAnalytics dapat dijalankan pada VPS 1 GB?
Tidak. Proyek ini memerlukan sekitar 4 GB RAM dan 25 GB ruang disk kosong karena satu deployment menjalankan enam service aplikasi bersama Postgres, ClickHouse, dan dua instance Valkey. ClickHouse sendiri bukan proses yang kecil. Pada server 1 GB, container akan berjalan, lalu kernel out-of-memory killer menghentikan salah satunya, biasanya ClickHouse. Jika paket 1 GB merupakan batasan mutlak, gunakan tool single-binary seperti GoatCounter, yang berjalan dengan SQLite tanpa database eksternal.
Apakah saya memerlukan banner cookie dengan OpenAnalytics?
Hal itu perlu ditanyakan kepada pengacara Anda, tetapi fakta teknisnya mendukung Anda. Tidak ada cookie, identitas pengunjung berupa hash dengan salt yang berganti setiap hari, dan alamat IP mentah tidak pernah disimpan. Jadi, tidak ada data persisten yang ditulis untuk mengidentifikasi pengunjung. GDPR tetap mengatur data yang Anda simpan dan berapa lama Anda menyimpannya. Jika ingin pengumpulan data dikendalikan secara eksplisit, tetapkan data-require-consent pada tag script. Tracker kemudian tidak mengumpulkan apa pun sampai persetujuan diberikan dan menyimpan jawaban tersebut di localStorage pada oa.consent.
Mengapa event mengembalikan 202 tetapi tidak pernah muncul di dashboard?
202 berarti collector menerima dan memasukkan event ke antrean, bukan menyimpannya. Worker menguras antrean tersebut ke ClickHouse. Jadi, dashboard yang kosong dengan request yang berhasil menunjukkan masalah pada worker. Baca docker compose logs --tail=50 worker dan pantau kedalaman antrean Valkey. Antrean yang terus bertambah berarti worker terblokir. Penyebab yang umum adalah kredensial ClickHouse yang salah di worker.env atau grant yang tidak ada pada tabel yang dibuat oleh migration terbaru.
Mengapa dashboard kosong ketika semua container sehat?
Periksa AUTH_TRUSTED_ORIGINS di env/api.env terlebih dahulu. Nilainya harus sama persis dengan origin dashboard. Jika tidak sama, API tidak mengirim header CORS. Akibatnya, browser menolak setiap panggilan dan Anda melihat layout yang berfungsi tanpa data. Hal kedua yang perlu diperiksa adalah tiga nilai NEXT_PUBLIC_* di env/web.env. Nilai tersebut disubstitusi saat web container dijalankan. Untuk memperbaikinya, diperlukan docker compose up -d --force-recreate web karena restart biasa tetap menggunakan nilai lama.
Apakah AGPL-3.0 melarang saya menawarkan layanan ini kepada klien?
Tidak, tetapi lisensi tersebut menetapkan satu syarat. Jika Anda menjalankan kode tanpa modifikasi, Anda tidak memiliki kewajiban kepada pihak lain. Jika Anda memodifikasinya dan menjalankan versi yang dimodifikasi tersebut sebagai service yang digunakan orang lain, Anda harus menawarkan source code yang telah dimodifikasi kepada pengguna tersebut. Public fork memenuhi kewajiban itu. Selain itu, nama "OpenAnalytics" tidak dilisensikan bersama kodenya. Jadi, apa pun yang Anda jual memerlukan nama tersendiri.