Deploy Betterlytics
Betterlytics is a cookieless analytics platform GDPR-compliant.
clickhouse
Just deployed
/var/lib/clickhouse
betterlytics
Just deployed
postgres
Just deployed
/var/lib/postgresql/data
Deploy and Host Betterlytics on Railway
Betterlytics is a privacy-first, cookieless Google Analytics alternative. A tracking script under 2KB captures pageviews, funnels, geography, Core Web Vitals, and errors without cookies or consent banners. This template self-hosts the full stack on Railway — Next.js dashboard, ingestion backend, Postgres, and ClickHouse.
About Hosting Betterlytics
Hosting Betterlytics means running three containers on Railway’s private network: a public app (nginx + Next.js dashboard + ingestion backend), Postgres for accounts and site config, and ClickHouse for high-volume events. Railway terminates TLS, so the app speaks HTTP internally (HTTP_SCHEME=http, FORCE_HTTP_SCHEME=https). You attach two volumes — Postgres at /var/lib/postgresql/data and ClickHouse at /var/lib/clickhouse — set one shared SECRET_BASE on all three services plus DOMAIN on the app, and let the entrypoint wait for both databases, run migrations, then start supervisord. First boot can take a few minutes. After that, add a site in the UI and drop /analytics.js on your pages.
Common Use Cases
- Replace Google Analytics on a SaaS, blog, or marketing site with cookieless, GDPR-friendly stats and no consent banner
- Agency / multi-site analytics — one Railway project, many sites in the dashboard, data that never leaves your account
- Product analytics plus uptime in one place — enable
ENABLE_UPTIME_MONITORINGinstead of bolting on a second tool - Own the event log — query ClickHouse over the private network for custom reports, funnels, or warehouse syncs
- EU / privacy-sensitive products — no third-party analytics vendor, no cookies, optional MaxMind geolocation with your own key
Dependencies for Betterlytics Hosting
- Betterlytics image:
ghcr.io/betterlytics/betterlytics-selfhost:latest(dashboard + ingestion backend + nginx + supervisord) - PostgreSQL 17.5 (
postgres:17.5) — users, sites, NextAuth - ClickHouse 25.8 (
clickhouse/clickhouse-server:25.8) — event store, IPv6 listen for Railway private networking - Railway private networking —
postgres.railway.internal:5432andclickhouse.railway.internal:8123 - Two persistent volumes (drives) — see Implementation Details
- OpenSSL in the DB images — derives passwords from
SECRET_BASE
Name the database services postgres and clickhouse. Set each service Root Directory to its folder (betterlytics/, postgres/, clickhouse/). Keep Postgres and ClickHouse private; only the app gets a public domain.
Upstream: Self-hosting guide · betterlytics.io · self-host repo
Implementation Details
Clone the template to see how it works:
betterlytics/ # public web service
Dockerfile # FROM ghcr.io/betterlytics/betterlytics-selfhost:latest
entrypoint.sh # derive secrets, wait, migrate, supervisord
nginx.conf # :8080 → dashboard :3000 + backend :3001
railway.toml # healthcheck /, 300s timeout
postgres/
Dockerfile # postgres:17.5 + openssl + derive wrapper
clickhouse/
Dockerfile # clickhouse-server:25.8 + IPv6 config + init users
config.xml # listen 0.0.0.0 and ::, 60% RAM cap
Topology
Name the services exactly as written. Private DNS is .railway.internal. If you rename them, the app cannot find the databases.
| Service | Role | Root Directory | Volume | Memory | Public |
|---|---|---|---|---|---|
| postgres | Dashboard database | postgres | /var/lib/postgresql/data | 1 GB –2 GB | No |
| clickhouse | Analytics events | clickhouse | /var/lib/clickhouse | 2 GB – 4 GB | No |
| betterlytics | Dashboard + tracker | betterlytics | None | 512 MB floor, 1 GB better | Yes |
Volumes (drives) — what to mount
Attach before the first successful start. Skip this and init lands on ephemeral disk; the next deploy wipes accounts and events.
| Service | Mount path | What is stored | Start size |
|---|---|---|---|
| postgres | /var/lib/postgresql/data | Accounts, sites, settings, sessions. PGDATA is already this path. | 1 GB |
| clickhouse | /var/lib/clickhouse | All pageviews/events, tables, SQL users (admin / backend / dashboard). | 5 GB (20 GB+ as traffic grows) |
| betterlytics | do not mount anything | Stateless. Railway terminates TLS. | — |
Do not mount /etc/letsencrypt, certbot webroot, or /etc/clickhouse-server/users.d. Those are VPS/compose paths. SQL users persist inside /var/lib/clickhouse.
Railway: each DB service → Settings → Volumes → Add Volume → paste the mount path exactly.
Recommended on betterlytics:
| Variable | Default | Description |
|---|---|---|
ADMIN_EMAIL | — | First dashboard login |
ADMIN_PASSWORD | — | First dashboard password |
DEFAULT_LANGUAGE | en | UI language |
ENABLE_UPTIME_MONITORING | false | Set true for built-in uptime checks |
DATA_RETENTION_DAYS | 365 | Event retention in ClickHouse. -1 = keep forever |
Optional email (ENABLE_EMAILS=true), then MailerSend or SMTP: MAILER_SEND_API_TOKEN / SMTP_HOST SMTP_PORT SMTP_USER SMTP_PASSWORD SMTP_FROM.
Optional geolocation: ENABLE_GEOLOCATION=true, GEOLOCATION_MODE=country or full, plus MAXMIND_ACCOUNT_ID and MAXMIND_LICENSE_KEY. New events only; IPs are not stored.
If you rename services: POSTGRES_HOST (default postgres.railway.internal), CLICKHOUSE_HOST (default clickhouse.railway.internal), CLICKHOUSE_PORT (must be 8123, not 9000).
Do not set (derived from SECRET_BASE): POSTGRES_PASSWORD, POSTGRES_SITECONFIG_RO_PASSWORD, CLICKHOUSE_PASSWORD, CLICKHOUSE_BACKEND_PASSWORD, CLICKHOUSE_DASHBOARD_PASSWORD, NEXTAUTH_SECRET, TOTP_SECRET_ENCRYPTION_KEY.
Baked in — leave alone: HTTP_SCHEME=http, FORCE_HTTP_SCHEME=https, PORT=8080, CLICKHOUSE_DB=analytics, POSTGRES_DB=dashboard, POSTGRES_USER=user.
Traps
Five ways this still fails:
- Do not mix
SECRET_BASEwith extra DB passwords:CLICKHOUSE_PASSWORD,POSTGRES_PASSWORD, andDATABASE_URLare ignored or overwritten. ClickHouse still stores the first-boot password on the volume. UseSECRET_BASEonly, then wipe the ClickHouse volume once. - App waits forever for Postgres: Postgres can be healthy while the app never connects. Railway private DNS is IPv6. Redeploy app from the updated kit (
wait-for.cjs). Do not restart Postgres to fix this. - Do not set
HTTP_SCHEME=https: That turns on Let’s Encrypt inside the container. Railway already terminates TLS. KeepHTTP_SCHEME=httpandFORCE_HTTP_SCHEME=https. - Do not use Railway’s Postgres plugin: The app derives the database password from
SECRET_BASE. The kit’spostgresservice is the matching pair. - ClickHouse on 512 MB will die: Raise the
clickhouseservice memory to 2 GB before the first deploy. DOMAINafter the first domain generate: If you generated*.up.railway.appafter the first deploy, updateDOMAINand redeploy so login and tracking URLs match.
Why Deploy Betterlytics on Railway?
Railway is a singular platform to deploy your infrastructure stack. Railway will host your infrastructure so you don't have to deal with configuration, while allowing you to vertically and horizontally scale it.
By deploying Betterlytics on Railway, you are one step closer to supporting a complete full-stack application with minimal burden. Host your servers, databases, AI agents, and more on Railway.
Template Content
clickhouse
AnarchistManifesto/Betterlyticsbetterlytics
AnarchistManifesto/Betterlyticspostgres
AnarchistManifesto/Betterlytics