Railway

Deploy Betterlytics

Betterlytics is a cookieless analytics platform GDPR-compliant.

Deploy Betterlytics

Just deployed

/var/lib/clickhouse

Just deployed

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.

Deploy on Railway

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_MONITORING instead 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 networkingpostgres.railway.internal:5432 and clickhouse.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.

ServiceRoleRoot DirectoryVolumeMemoryPublic
postgresDashboard databasepostgres/var/lib/postgresql/data1 GB –2 GBNo
clickhouseAnalytics eventsclickhouse/var/lib/clickhouse2 GB – 4 GBNo
betterlyticsDashboard + trackerbetterlyticsNone512 MB floor, 1 GB betterYes
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.

ServiceMount pathWhat is storedStart size
postgres/var/lib/postgresql/dataAccounts, sites, settings, sessions. PGDATA is already this path.1 GB
clickhouse/var/lib/clickhouseAll pageviews/events, tables, SQL users (admin / backend / dashboard).5 GB (20 GB+ as traffic grows)
betterlyticsdo not mount anythingStateless. 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 → SettingsVolumesAdd Volume → paste the mount path exactly.

Recommended on betterlytics:

VariableDefaultDescription
ADMIN_EMAILFirst dashboard login
ADMIN_PASSWORDFirst dashboard password
DEFAULT_LANGUAGEenUI language
ENABLE_UPTIME_MONITORINGfalseSet true for built-in uptime checks
DATA_RETENTION_DAYS365Event 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_BASE with extra DB passwords: CLICKHOUSE_PASSWORD, POSTGRES_PASSWORD, and DATABASE_URL are ignored or overwritten. ClickHouse still stores the first-boot password on the volume. Use SECRET_BASE only, 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. Keep HTTP_SCHEME=http and FORCE_HTTP_SCHEME=https.
  • Do not use Railway’s Postgres plugin: The app derives the database password from SECRET_BASE. The kit’s postgres service is the matching pair.
  • ClickHouse on 512 MB will die: Raise the clickhouse service memory to 2 GB before the first deploy.
  • DOMAIN after the first domain generate: If you generated *.up.railway.app after the first deploy, update DOMAIN and 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

More templates in this category

View Template
Matomo Analytics + MariaDB
Privacy-friendly analytics with MariaDB and persistent volumes.

leodev
1
View Template
Bugsink
Self-hosted Error Tracking. Sentry-SDK compatible

nonzerosum
19
View Template
SubTrackr
Self-hosted Subscription Tracker

amnesia
3