Rejourney
★ 283A self-hosted alternative to Hotjar
sspl-1-0Source-Available — Not OSS15 Containers — 15 containers — heavy hosting footprintDocker · Compose · K8s +1Needs Postgres2+ active committers (12mo)
About
Rejourney is a session replay and observability platform aimed at mobile and web applications. It provides SDKs and related backend/dashboard components to capture user sessions with high fidelity,...
Community Ratings
No ratings yetNew this week
Read recap →Weekly Recap — Oct 9, 2026 – Oct 16, 2026Oct 16, 2026
Features
- pixel-perfect capture
- real-time incident stream
- crash detection
- ANR detection
- journey mapping
- interaction heat maps
- global stability monitoring
- growth analytics
- team alerts
- role-based access
Details
- Last Updated
- Oct 19, 2026
- Created
- Jan 23, 2026
- Install Methods
- dockerdocker-composekubernetessource
- Requirements
- Docker ComposeKubernetes (local-k8s / K3s)Node/npmlocal secrets for development
- Authentication
- local
- API
- REST
- Backup & Export
- database-dumpfile-backup
- Privacy & Independence
- Cloud: Optional
- User Model
- Multi-User
- Deployment
Deployment: Docker Compose ✓15 Containers — heavy hosting footprintRuns as 15 containers
Run Rejourney with Docker Compose
Compose file from the Rejourney project repository.
15 services · database: postgres · infra: traefik, postgres, redis, minio · 1 unclassified (mc)
no resource limits declared
needs persistent storage (5 volumes)
docker-compose.selfhosted.yml · @ 0cfe415 · scanned Jun 6, 2026
name: rejourney
x-app-env: &app-env
DOCKER_ENV: "true"
NODE_ENV: production
LOG_LEVEL: ${LOG_LEVEL:-info}
DATABASE_URL: ${DATABASE_URL}
REDIS_URL: ${REDIS_URL}
S3_ENDPOINT: ${S3_ENDPOINT}
S3_PUBLIC_ENDPOINT: ${S3_PUBLIC_ENDPOINT:-}
S3_REGION: ${S3_REGION:-us-east-1}
S3_BUCKET: ${S3_BUCKET}
S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID}
S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY}
JWT_SECRET: ${JWT_SECRET}
JWT_SIGNING_KEY: ${JWT_SIGNING_KEY}
INGEST_HMAC_SECRET: ${INGEST_HMAC_SECRET}
STORAGE_ENCRYPTION_KEY: ${STORAGE_ENCRYPTION_KEY}
PUBLIC_DASHBOARD_URL: ${PUBLIC_DASHBOARD_URL}
PUBLIC_API_URL: ${PUBLIC_API_URL}
PUBLIC_INGEST_URL: ${PUBLIC_INGEST_URL}
DASHBOARD_ORIGIN: ${DASHBOARD_ORIGIN}
TURNSTILE_SITE_KEY: ${TURNSTILE_SITE_KEY:-}
TURNSTILE_SECRET_KEY: ${TURNSTILE_SECRET_KEY:-}
GITHUB_CLIENT_ID: ${GITHUB_CLIENT_ID:-}
GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET:-}
STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY:-}
STRIPE_WEBHOOK_SECRET: ${STRIPE_WEBHOOK_SECRET:-}
SMTP_HOST: ${SMTP_HOST:-}
SMTP_PORT: ${SMTP_PORT:-}
SMTP_USER: ${SMTP_USER:-}
SMTP_PASS: ${SMTP_PASS:-}
SMTP_FROM: ${SMTP_FROM:-}
SMTP_SECURE: ${SMTP_SECURE:-true}
GEOIP_PREFER_MMDB: ${GEOIP_PREFER_MMDB:-true}
GEOIP_MMDB_AUTO_DOWNLOAD: ${GEOIP_MMDB_AUTO_DOWNLOAD:-true}
GEOIP_MMDB_URL: ${GEOIP_MMDB_URL:-https://cdn.jsdelivr.net/npm/geolite2-city/GeoLite2-City.mmdb.gz}
GEOIP_MMDB_PATH: ${GEOIP_MMDB_PATH:-/tmp/rejourney/GeoLite2-City.mmdb}
INGEST_MAX_OBJECT_BYTES: ${INGEST_MAX_OBJECT_BYTES:-26214400}
INGEST_DEVICE_BYTES_PER_MINUTE: ${INGEST_DEVICE_BYTES_PER_MINUTE:-52428800}
INGEST_DEVICE_BYTES_PER_DAY: ${INGEST_DEVICE_BYTES_PER_DAY:-2147483648}
INGEST_PROJECT_BYTES_PER_MINUTE: ${INGEST_PROJECT_BYTES_PER_MINUTE:-536870912}
INGEST_PROJECT_BYTES_PER_DAY: ${INGEST_PROJECT_BYTES_PER_DAY:-107374182400}
INGEST_IP_BYTES_PER_MINUTE: ${INGEST_IP_BYTES_PER_MINUTE:-104857600}
INGEST_IP_BYTES_PER_DAY: ${INGEST_IP_BYTES_PER_DAY:-5368709120}
x-api-image: &api-image ghcr.io/rejourneyco/rejourney/api:${IMAGE_TAG:-latest}
x-web-image: &web-image ghcr.io/rejourneyco/rejourney/web:${IMAGE_TAG:-latest}
x-migration-image: &migration-image ghcr.io/rejourneyco/rejourney/migration:${IMAGE_TAG:-latest}
services:
traefik:
image: traefik:v3.2
restart: unless-stopped
security_opt:
- no-new-privileges:true
command:
- --api.dashboard=false
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --providers.docker.network=rejourney-network
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --entrypoints.web.forwardedHeaders.insecure=true
- --entrypoints.websecure.forwardedHeaders.insecure=true
- --entrypoints.web.http.redirections.entrypoint.to=websecure
- --entrypoints.web.http.redirections.entrypoint.scheme=https
- --certificatesresolvers.letsencrypt.acme.email=${LETSENCRYPT_EMAIL}
- --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
- --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- traefik-certs:/letsencrypt
labels:
- traefik.enable=true
- traefik.http.middlewares.rejourney-security.headers.stsSeconds=31536000
- traefik.http.middlewares.rejourney-security.headers.stsIncludeSubdomains=true
- traefik.http.middlewares.rejourney-security.headers.stsPreload=true
- traefik.http.middlewares.rejourney-security.headers.forceSTSHeader=true
- traefik.http.middlewares.rejourney-security.headers.frameDeny=false
- traefik.http.middlewares.rejourney-security.headers.customFrameOptionsValue=SAMEORIGIN
- traefik.http.middlewares.rejourney-security.headers.contentTypeNosniff=true
- traefik.http.middlewares.rejourney-security.headers.browserXssFilter=true
- traefik.http.middlewares.rejourney-security.headers.referrerPolicy=strict-origin-when-cross-origin
- "traefik.http.middlewares.rejourney-security.headers.contentSecurityPolicy=default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://js.stripe.com https://m.stripe.network https://static.cloudflareinsights.com https://www.googletagmanager.com https://www.googleadservices.com https://googleads.g.doubleclick.net https://pagead2.googlesyndication.com https://www.google.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https: blob:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https://${API_DOMAIN} wss://${API_DOMAIN} https://${INGEST_DOMAIN} https://api.stripe.com https://m.stripe.network https://cloudflareinsights.com https://api.mapbox.com https://events.mapbox.com https://*.tiles.mapbox.com https://www.googletagmanager.com https://pagead2.googlesyndication.com https://www.googleadservices.com https://googleads.g.doubleclick.net https://ad.doubleclick.net https://www.google.com https://google.com; media-src 'self' https: blob:; worker-src 'self' blob:; frame-src 'self' https://js.stripe.com https://hooks.stripe.com https://www.googletagmanager.com; frame-ancestors 'self'; base-uri 'self'; form-action 'self';"
- traefik.http.middlewares.rejourney-security.headers.permissionsPolicy=geolocation=(), microphone=(), camera=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()
- traefik.http.middlewares.rejourney-security.headers.customResponseHeaders.X-Powered-By=
- traefik.http.middlewares.rejourney-security.headers.customResponseHeaders.Server=
- traefik.http.middlewares.rejourney-security.headers.customResponseHeaders.X-Content-Type-Options=nosniff
- traefik.http.middlewares.rejourney-security.headers.customResponseHeaders.X-Frame-Options=SAMEORIGIN
- traefik.http.middlewares.rejourney-security.headers.customResponseHeaders.X-XSS-Protection=1; mode=block
- traefik.http.middlewares.rejourney-compress.compress=true
- "traefik.http.middlewares.rejourney-www-redirect.redirectregex.regex=^https?://www\\.${BASE_DOMAIN}(.*)"
- "traefik.http.middlewares.rejourney-www-redirect.redirectregex.replacement=https://${DASHBOARD_DOMAIN}$${1}"
- traefik.http.middlewares.rejourney-www-redirect.redirectregex.permanent=true
networks:
- rejourney-network
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB:-rejourney}
POSTGRES_USER: ${POSTGRES_USER:-rejourney}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
PGDATA: /var/lib/postgresql/data/pgdata
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-rejourney} -d ${POSTGRES_DB:-rejourney}"]
interval: 10s
timeout: 5s
retries: 10
networks:
- rejourney-network
redis:
image: redis:8.4-alpine
restart: unless-stopped
command: >
redis-server
--save 900 1
--loglevel warning
--requirepass ${REDIS_PASSWORD}
volumes:
- redisdata:/data
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 10s
timeout: 5s
retries: 10
networks:
- rejourney-network
minio:
image: minio/minio:RELEASE.2024-09-22T00-33-43Z
restart: unless-stopped
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
MINIO_API_CORS_ALLOW_ORIGIN: "*"
volumes:
- miniodata:/data
profiles:
- minio
networks:
- rejourney-network
minio-setup:
image: minio/mc:latest
restart: "no"
# Image default ENTRYPOINT is `mc`; override so we run a shell script (see local-k8s/minio.yaml).
entrypoint: ["/bin/sh", "-c"]
command:
- |
set -e
until mc alias set local http://minio:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD"; do
echo "Waiting for MinIO..."
sleep 2
done
mc mb -p "local/${S3_BUCKET}" || true
mc anonymous set download "local/${S3_BUCKET}" || true
depends_on:
- minio
profiles:
- minio
networks:
- rejourney-network
bootstrap:
image: *migration-image
build:
context: .
dockerfile: backend/Dockerfile.migration
restart: "no"
environment:
<<: *app-env
STORAGE_SYNC_LABEL: selfhosted-bootstrap
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
command:
- /bin/sh
- -lc
- |
node --import tsx scripts/applyDatabaseSchema.ts && \
node --import tsx scripts/seedIfDatabaseEmpty.ts && \
node --import tsx scripts/bootstrapSystemData.ts && \
node --import tsx scripts/syncStorageEndpoint.ts
networks:
- rejourney-network
api:
image: *api-image
restart: unless-stopped
environment:
<<: *app-env
PORT: "3000"
COOKIE_SECURE: "true"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
bootstrap:
condition: service_completed_successfully
labels:
- traefik.enable=true
- traefik.http.routers.rejourney-api.rule=Host(`${API_DOMAIN}`)
- traefik.http.routers.rejourney-api.priority=1
- traefik.http.routers.rejourney-api.entrypoints=websecure
- traefik.http.routers.rejourney-api.tls.certresolver=letsencrypt
- traefik.http.routers.rejourney-api.middlewares=rejourney-security,rejourney-compress
- traefik.http.routers.rejourney-api-ingest.rule=Host(`${INGEST_DOMAIN}`)
- traefik.http.routers.rejourney-api-ingest.priority=1
- traefik.http.routers.rejourney-api-ingest.entrypoints=websecure
- traefik.http.routers.rejourney-api-ingest.tls.certresolver=letsencrypt
- traefik.http.routers.rejourney-api-ingest.middlewares=rejourney-security,rejourney-compress
- traefik.http.services.rejourney-api.loadbalancer.server.port=3000
healthcheck:
test: ["CMD", "node", "-e", "require('http').get('http://127.0.0.1:3000/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))"]
interval: 30s
timeout: 5s
retries: 5
networks:
- rejourney-network
ingest-upload:
image: *api-image
restart: unless-stopped
command: ["node", "dist/uploadServer.js"]
environment:
<<: *app-env
PORT: "3001"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
bootstrap:
condition: service_completed_successfully
labels:
- traefik.enable=true
- traefik.http.routers.rejourney-ingest.rule=Host(`${INGEST_DOMAIN}`) && PathPrefix(`/upload`)
- traefik.http.routers.rejourney-ingest.priority=100
- traefik.http.routers.rejourney-ingest.entrypoints=websecure
- traefik.http.routers.rejourney-ingest.tls.certresolver=letsencrypt
- traefik.http.routers.rejourney-ingest.middlewares=rejourney-security,rejourney-compress
- traefik.http.services.rejourney-ingest.loadbalancer.server.port=3001
healthcheck:
test: ["CMD", "node", "-e", "require('http').get('http://127.0.0.1:3001/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))"]
interval: 30s
timeout: 5s
retries: 5
networks:
- rejourney-network
web:
image: *web-image
restart: unless-stopped
environment:
NODE_ENV: production
API_URL: http://api:3000
VITE_TURNSTILE_SITE_KEY: ${TURNSTILE_SITE_KEY:-}
depends_on:
api:
condition: service_healthy
bootstrap:
condition: service_completed_successfully
labels:
- traefik.enable=true
- traefik.http.routers.rejourney-web.rule=Host(`${DASHBOARD_DOMAIN}`)
- traefik.http.routers.rejourney-web.entrypoints=websecure
- traefik.http.routers.rejourney-web.tls.certresolver=letsencrypt
- traefik.http.routers.rejourney-web.middlewares=rejourney-security,rejourney-compress
- traefik.http.routers.rejourney-web-www.rule=Host(`${WWW_DOMAIN}`)
- traefik.http.routers.rejourney-web-www.entrypoints=websecure
- traefik.http.routers.rejourney-web-www.tls.certresolver=letsencrypt
- traefik.http.routers.rejourney-web-www.middlewares=rejourney-www-redirect
- traefik.http.services.rejourney-web.loadbalancer.server.port=8080
healthcheck:
test: ["CMD", "node", "-e", "require('http').get('http://127.0.0.1:8080', (r) => process.exit(r.statusCode < 500 ? 0 : 1)).on('error', () => process.exit(1))"]
interval: 30s
timeout: 5s
retries: 5
networks:
- rejourney-network
ingest-worker:
image: *api-image
restart: unless-stopped
command: ["node", "dist/worker/ingestArtifactWorker.js"]
healthcheck:
disable: true
environment:
<<: *app-env
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
bootstrap:
condition: service_completed_successfully
networks:
- rejourney-network
replay-worker:
image: *api-image
restart: unless-stopped
command: ["node", "dist/worker/replayArtifactWorker.js"]
healthcheck:
disable: true
environment:
<<: *app-env
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
bootstrap:
condition: service_completed_successfully
networks:
- rejourney-network
session-lifecycle-worker:
image: *api-image
restart: unless-stopped
command: ["node", "dist/worker/sessionLifecycleWorker.js"]
healthcheck:
disable: true
environment:
<<: *app-env
depends_on:
postgres:
condition: service_healthy
bootstrap:
condition: service_completed_successfully
networks:
- rejourney-network
# Matches k8s/workers.yaml retention-worker CronJob (*/15 * * * *): single-shot runs with backlog drain.
retention-worker:
image: *api-image
restart: unless-stopped
healthcheck:
disable: true
command:
- /bin/sh
- -c
- |
set -eu
while true; do
node dist/worker/retentionWorker.js --once --drain-backlog --trigger=scheduled
sleep 900
done
environment:
<<: *app-env
depends_on:
postgres:
condition: service_healthy
bootstrap:
condition: service_completed_successfully
networks:
- rejourney-network
alert-worker:
image: *api-image
restart: unless-stopped
command: ["node", "dist/worker/alertWorker.js"]
healthcheck:
disable: true
environment:
<<: *app-env
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
bootstrap:
condition: service_completed_successfully
networks:
- rejourney-network
revenue-sync-worker:
image: *api-image
restart: unless-stopped
command: ["node", "dist/worker/revenueSyncWorker.js"]
healthcheck:
disable: true
environment:
<<: *app-env
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
bootstrap:
condition: service_completed_successfully
networks:
- rejourney-network
networks:
rejourney-network:
name: rejourney-network
driver: bridge
volumes:
pgdata:
redisdata:
miniodata:
traefik-certs:
This file may be out of date. Check the Rejourney documentation for current setup steps.
Track your self-hosted stack
Bookmark software to try, rate tools you've used, and keep your collection in one place.
Metadata extracted from README on Jun 4, 2026
