Skip to content

Self-hosting

Self-Hosted Session Replay: Why and What It Takes

When to self-host session replay, what AnyReplay needs on one host (PostgreSQL, three services, nginx, chunk storage) and how backups and retention work.

Published · Updated · 6 min read

Session recordings are the most sensitive data most products collect about their users: a picture, however masked, of what each person did. For some teams that is reason enough to keep them on infrastructure they control. This post is about running AnyReplay yourself: why you would, what the documented deployment looks like, and what you are signing up to operate.

Why self-host

  • Data residency. Recordings never leave your region, your account or your network. International transfer questions under GDPR mostly disappear; see session replay and GDPR.
  • Fewer processors. There is no third party holding recordings to name in your privacy notice or your processor agreements. Regulated teams — fintech compliance is the common case — often need this.
  • Retention on your terms. The 14-day default and the per-organisation override are yours to set, and the storage that backs them is a directory you can audit.
  • Predictable cost. A single host with a known disk is a bill you can reason about.

And why not: you are now the operator. Backups, upgrades, disk space and TLS are yours. The rest of this post is what that involves, honestly.

The shape of the deployment

AnyReplay ships a documented single-host installation in the deploy/ folder of the repository. One machine runs everything, behind whatever nginx is already there:

                    ┌── anyreplay.example ──► :7100  dashboard (Next.js)
Cloudflare ── nginx ├── api.example ────────► :7101  API      (NestJS)
   (proxy)          ├── in.example ─────────► :7102  ingest   (Fastify)
                    └── cdn.example ────────► :7102  recorder bundle
                                                │
                                      PostgreSQL + /var/lib/anyreplay/storage
The documented topology. Cloudflare terminates TLS at the edge and reaches the origin over an origin certificate.

Three services, one database, one storage directory. The split by service is deliberate: ingest is high-volume, unauthenticated and write-only, the opposite load profile from the dashboard, so when a single host stops being enough the move is to put ingest on its own machine — a move, not a rewrite.

What you need

ComponentRoleNotes
Ubuntu 24.04 hostRuns everythingThe documented target; other Linux distributions will work with adjustments
PostgreSQLOrganisations, users, sessions, chunk index, translation memory, audit logReuse an existing server if one is there; the app gets its own role and database
Node.js 20.19+ (22 recommended) and pnpm 10RuntimeInstalled privately under /opt/node-22 so the system Node is untouched
nginxReverse proxyOne server block per hostname; nothing else on the host is affected
Cloudflare (or another TLS edge)TLS and origin protectionThe origin certificate comes from Cloudflare; the firewall admits only Cloudflare ranges on 443
DiskChunk storageUnder /var/lib/anyreplay/storage; retention keeps it bounded
An object storage bucketOff-host backupsAnything rclone can reach

The Node detail is worth a sentence. The API is compiled to CommonJS and loads ESM-only workspace packages, which require() cannot do on Node 18; on an old Node the install and build succeed and the service crash-loops at startup with ERR_REQUIRE_ESM. The deploy guide installs a private Node 22 under /opt/node-22 so that nothing else on the host changes.

For development or a first look you do not need PostgreSQL at all: with DATABASE_URL empty the API runs PGlite, real PostgreSQL compiled to WebAssembly, in-process. Production uses a real server so that the API and ingest can connect independently.

Services and ports

The three services run under systemd from the unit files in deploy/: anyreplay-api, anyreplay-ingest and anyreplay-web. Each reads an environment file under /etc/anyreplay/. The documented ports are 7100–7102, chosen as a clean block on a host that already serves other projects; check with ss -ltnp and change them if something else is there. A service that cannot bind fails loudly, which is the good case.

The API refuses to start with a JWT secret shorter than 32 characters. A service that boots with a weak secret is worse than one that refuses to boot.

Storage and retention

Recorded sessions are stored as numbered chunks under the storage directory, with the chunk index in PostgreSQL. Retention is enforced by the API on an interval: sessions older than the organisation's retention period (14 days by default) are deleted, and deleting a session deletes its chunks, so the directory does not grow without bound. Deleting a project deletes its recordings too. Disk usage is therefore roughly sessions per day, times average session size, times retention days — a number you can estimate before you provision.

Backups: encrypted, off-host, and tested

Two things on the host cannot be rebuilt from the repository: the database and the chunk store. deploy/backup.sh takes both in one run — a pg_dump and a compressed archive of the storage directory — encrypts the result with GPG on the host before it leaves, and uploads it with rclone to a bucket. The copy sitting in someone else's bucket is worthless to whoever reads it, which matters because a bucket of session recordings is exactly the thing this product promises to look after.

Two systemd timers drive it: a daily backup at 03:20 UTC, and a weekly verification at 04:10 UTC on Sundays that downloads the newest backup, restores it into a throwaway database and counts the rows. An untested backup is a hope. Backups older than BACKUP_KEEP_DAYS (30 by default) are pruned. The passphrase lives in /etc/anyreplay/backup.pass; put a copy somewhere that is not this host, because a backup you cannot decrypt is not a backup.

gpg --batch --decrypt --passphrase-file /etc/anyreplay/backup.pass backup.tar.gpg | tar -xf -
sudo -u postgres pg_restore --no-owner --no-privileges -d anyreplay < db.dump
tar -C /var/lib -xf <(zstd -dc storage.tar.zst)
Restore is deliberately boring: decrypt, restore the dump, unpack the storage archive.

One file per run rather than an incremental format is a choice: deduplication would save money at scale and costs the one thing that matters at three in the morning, a restore with no tool to install and no repository to unlock first.

Security posture you inherit

Self-hosting does not mean starting from scratch on security. The same code runs: Argon2id password hashing at OWASP parameters, refresh-token rotation with reuse detection, tenant isolation tested from a foreign tenant against every organisation-scoped route, an append-only audit trail, input masking applied in the visitor’s browser with the same floor that never records a password or a payment field, the origin allow-list that makes a public project key safe, and a replay iframe sandboxed without allow-scripts. What changes is who holds the keys and the disk. See privacy features for the full list.

If you are still choosing a tool, open-source and self-hosted session replay tools compares the self-hostable options side by side.

What you are taking on

  • Upgrades. Pull, build, restart the three services. The schema is created and migrated on boot.
  • Disk. Watch it. Retention bounds it, but a traffic spike with a long retention period is the failure mode.
  • TLS and DNS. Four hostnames, one origin certificate, a firewall that admits only your edge.
  • Secrets. Two JWT secrets, a database password, a backup passphrase, and the keys for whichever translation provider you configure, if you use session translation.
  • The 3 a.m. restore. Do it once, on purpose, before you need to.

If that list reads as reasonable, self-hosting is a good fit. If it reads as a second job, the hosted service runs the same code with the same defaults, and the Free plan — one project, 500 sessions a month — costs nothing to try; see pricing. Teams comparing self-hostable replay tools may also want AnyReplay vs OpenReplay and the React Native recorder, which works identically against a self-hosted ingest endpoint via the ingestUrl option.

Frequently asked questions

Can AnyReplay be self-hosted?

Yes. The deploy folder documents a single-host installation: PostgreSQL, the API, the ingest service and the dashboard as systemd services behind nginx, with encrypted off-host backups.

What database does AnyReplay use?

PostgreSQL. For development without a server it can run PGlite, PostgreSQL compiled to WebAssembly, in-process; production uses a real PostgreSQL server.

How are backups handled?

A script dumps the database and archives the chunk store, encrypts the result with GPG on the host, and uploads it with rclone. A daily timer takes backups and a weekly timer restores the newest one into a throwaway database to verify it.

Does the React Native SDK work with a self-hosted install?

Yes. Both SDKs accept an ingestUrl option that points them at your own ingest service instead of the hosted one.

Start free — 1 project, 500 sessions a month

Install the snippet, record a session, and watch it back in your own language.