Self-Hosted 2026-08-03

Self-Hosted Session Replay on One VPS: a Realistic Setup

Here’s the number that matters before anything else: a self hosted session replay stack — ingest, storage, query layer, player — runs fine on a single 8GB RAM / 4-core VPS for a small SaaS. Not “fine if you squint.” Fine, as in I’ve run this shape of deployment for over a year with the only recurring incident being disk filling up, which is a scheduling problem, not an architecture problem.

That box costs $40–60/month at Hetzner or DigitalOcean as of mid-2026. Everything below assumes that budget and a team of one to five engineers who have better things to do than operate infrastructure.

What a self hosted session replay stack actually contains

Replay platforms in the Highlight/rrweb lineage all decompose into roughly the same five services, and you should know what each one is before you docker compose up a black box:

Service Job Where the pain lives
Reverse proxy TLS termination, routes /public ingest Cert renewal, body size limits
Backend Auth, session metadata, APIs Mostly boring
Postgres Users, projects, session index Backups
ClickHouse Events, logs, traces RAM appetite
Object storage (MinIO or S3) Compressed replay chunks Disk growth

ClickHouse is the one that earns the 8GB requirement. It will happily idle at 1–2GB and spike during merges and heavy queries. Give the stack 4GB total and you’ll meet the OOM killer within a week — I’ve watched it pick off ClickHouse mid-merge and corrupt nothing, which is a credit to ClickHouse, but you don’t want to test that regularly.

LogReplay ships exactly this layout as a single-node Docker deployment, and its stated baseline is the same 8GB / 4-core figure. That’s not vendor generosity; it’s the honest floor for this architecture.

The Docker layout

One compose file, one named volume per stateful service, everything on an internal network except the proxy:

services:
  proxy:
    image: nginx:stable
    ports: ["80:80", "443:443"]
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - certs:/etc/letsencrypt
  backend:
    image: yourvendor/backend
    depends_on: [postgres, clickhouse, minio]
  postgres:
    image: postgres:16
    volumes: [pgdata:/var/lib/postgresql/data]
  clickhouse:
    image: clickhouse/clickhouse-server:24.8
    volumes: [chdata:/var/lib/clickhouse]
  minio:
    image: minio/minio
    volumes: [miniodata:/data]

Two opinions, both learned the hard way.

First: pin image tags. latest on ClickHouse across a major version is how you discover schema migrations at 2 a.m.

Second: don’t expose Postgres, ClickHouse, or MinIO ports to the host. Only the proxy gets published ports. Every “my self-hosted analytics got ransomed” story starts with a database port open to the internet with default credentials.

TLS with nginx and certbot

The replay SDK posts from your users’ browsers, so you need a real certificate — no self-signed shortcuts. The standard certbot webroot dance works:

certbot certonly --webroot -w /var/www/certbot \
  -d replay.example.com --agree-tos -m ops@example.com

Then the nginx server block. The one line people forget is client_max_body_size — replay payloads are compressed event batches and the nginx default of 1MB will silently 413 your larger sessions:

server {
    listen 443 ssl;
    server_name replay.example.com;
    ssl_certificate     /etc/letsencrypt/live/replay.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/replay.example.com/privkey.pem;

    client_max_body_size 50m;

    location /public {
        proxy_pass http://backend:8082;
        proxy_read_timeout 60s;
    }
}

Serving ingest from your own domain has a side benefit worth naming: ad blockers filter third-party analytics hosts by list, and a first-party /public path on your own subdomain doesn’t match those lists. You’ll capture sessions from the 30–40% of technical audiences running blockers (an estimate, but if your users are developers it’s not a wild one).

Put certbot renew in cron and — this is the part people skip — add a second cron entry that curls your own endpoint weekly and alerts if the cert expires within 14 days. Renewal jobs fail silently; expired certs do not.

Disk growth, with arithmetic

This is the actual operational cost of self-hosting replay, so let’s do it properly.

A compressed rrweb session runs 200KB–2MB depending on page complexity and duration. Call the median 500KB for a typical dashboard-style app. Then:

Add ClickHouse event data — logs, network requests, traces — which in my experience lands at roughly the same order of magnitude as the replay chunks. So a 10k-session/month deployment eats about 10GB/month total before retention kicks in.

With 30-day retention that’s a steady-state ~10GB. With 90 days, ~30GB. A 160GB VPS disk handles either without drama. What it will not survive is no retention policy, which is the default failure mode: everything works for eight months, then the disk hits 95% and ingest starts failing, and you discover that ClickHouse behaves badly at 100% disk in ways that are tedious to recover from.

Set retention on day one. Set a disk alert at 80% on day one. These two lines of config are the entire difference between “self-hosting is easy” and the war stories.

Backups: what actually needs saving

Not everything in that stack deserves a backup, and pretending otherwise makes backups so expensive you’ll stop doing them.

Postgres: yes, always. It’s small (session metadata, not sessions) and it’s the part you cannot regenerate. pg_dump nightly, ship it off-box:

docker exec postgres pg_dump -U postgres logreplay | \
  gzip | aws s3 cp - s3://backups/pg/$(date +%F).sql.gz

Replay chunks in MinIO: your call. They’re the bulky part, and they expire on schedule anyway. My position: if your retention is 30 days, losing the box loses at most 30 days of replays, and replays are diagnostic evidence, not business records. Most small teams should accept that loss window rather than pay to replicate tens of gigabytes of expiring data. If compliance says otherwise, point MinIO’s bucket replication at real S3 and pay the egress.

ClickHouse: same logic. Rebuildable-ish, expiring, bulky. Skip it until someone gives you a reason in writing.

Test the Postgres restore once. An untested backup is a superstition.

Day-two operations: the actual maintenance calendar

The setup takes an afternoon. The question that decides whether self-hosting was a good idea is what the next twelve months cost you, so here is the honest maintenance load, based on running this shape of stack in anger:

Weekly (automated, you just read alerts): disk usage check, cert expiry check, backup success check. If you wired the alerts from the sections above, this is zero minutes unless something fires.

Monthly (~30 minutes, human): pull updated images for the app services, docker compose up -d, click around the dashboard to confirm nothing broke. Read the release notes first — the one month you skip them is the one with a breaking config change. Postgres and ClickHouse I upgrade far less often, only for security fixes or a feature I actually want, because stateful-service upgrades are where the risk concentrates.

Quarterly (~1 hour): restore the Postgres backup to a scratch container and confirm it works; review retention settings against actual disk trend; reboot the box for kernel updates if your distro doesn’t live-patch.

Add it up and you’re at something like 10–15 hours a year of attention, plus the occasional unscheduled hour when an alert fires. Against a hosted replay bill, that trade is defensible for some teams and not others — the point is to price your time into the comparison honestly instead of comparing $50 of VPS against a SaaS invoice and declaring victory.

Two things make that number blow up, and both are avoidable. Unpinned image tags (covered above) turn routine updates into surprise migrations. And skipping the disk alert converts a 5-minute retention tweak into a multi-hour recovery with the team asking why sessions are missing.

When one box stops being enough

Honest answer: later than the scaling blog posts suggest. The single-node ceiling for this stack is somewhere around a few hundred thousand sessions a month, and the first symptom is ClickHouse query latency during merges, not ingest failures. If you’re a 1–20 engineer SaaS, you will probably change jobs before you hit it.

What actually pushes teams off one box is organizational: someone wants HA, or legal wants storage in a specific region, or you get acquired and inherit a platform team with opinions. Those are fine reasons. “We might need to scale” is not — you can migrate volumes to a bigger VPS in an evening.

If you’re still choosing which stack to run, the deployment shape above applies to all of the serious contenders; the differences are in what’s captured and how the pieces are licensed, which I’ve broken down in a comparison of the open-source session replay options.

One last thing: put the VPS provider’s snapshot feature on a weekly schedule too. It’s a blunt instrument, it’s a couple of dollars a month, and it has bailed me out exactly once — which paid for a decade of it.

See the bug the way your user did

LogReplay captures session replays, console output, network requests, and errors in one timeline — so you stop guessing what happened before the ticket arrived.

Try LogReplay free