Host requirements
This page lists what a host needs to run a Varlatch installation with the
canonical Docker Compose deployment, the one varlatch setup installs from
a release bundle. Getting started
walks you through the installation itself, and
Configuration lists every setting.
| Minimum | |
|---|---|
| Operating system | Linux, on amd64 or arm64 |
| Containers | Docker with the Docker Compose plugin (docker compose), Compose 2.24 or newer |
| On the host | Node.js 22, 24 or 26 with its current security fixes, for the operator CLI |
| Memory | 2 GiB |
| Processor | 2 CPU cores |
| Disk | 20 GB free for Docker’s data and the installation’s directory |
| Network | depends on how people reach Varlatch: Network |
The memory, processor, and disk figures are recommendations with headroom. Sizing shows what an installation actually used, and how it was measured.
Operating system and architecture
Section titled “Operating system and architecture”Run Varlatch on Linux, on amd64 or arm64. From 0.10.0 on, the release images are published for linux/amd64 and linux/arm64, and the Convex backend image the release pins exists for those two platforms only. No other architecture can run an installation.
Run the images for the host’s own architecture, not under emulation. When the release checks run the arm64 images on an amd64 machine, they still run PostgreSQL and the Convex backend natively, because Convex’s function time limits do not hold under emulation on a small host.
Software
Section titled “Software”- Docker with the Docker Compose plugin, Compose 2.24 or newer. Every
host command (
varlatch setup,doctor,upgrade, andadmin backup) drivesdocker compose. The Tailscale overlay needs 2.24, because it uses!resetto drop varlatchd’s own port mapping, and the same minimum holds for every ingress.varlatch setupstops before it changes anything when Compose is older, andvarlatch doctorreports the version. Releases are tested with Docker 29 and Compose 5. - Node.js 22, 24 or 26, with its current security fixes. The operator CLI is a single file that needs only Node.js: Get the CLI. On a Node.js release without those fixes it prints a warning: Node.js version.
- systemd, only if you use the reference backup timer in
infra/host-backup.
You do not install PostgreSQL, Convex, Caddy, or Tailscale yourself: each runs in a container the Compose files define.
Apart from the Compose version, varlatch setup does not check free
ports, memory, or disk before it starts. When something is missing, the Docker or Compose command
it runs fails, and setup stops with that error. Fix the cause and run
setup again: it continues where it stopped.
Access on the host
Section titled “Access on the host”Run varlatch setup and the other host commands as a user who can use
Docker. Access to Docker is root-equivalent on the host, and anyone who can
open a shell in the installation’s containers is an Infrastructure Operator
for it: the compose guide’s
break-glass section
says what that means.
File permissions matter, because the containers run as users other than yours:
- Unpack the release bundle with the usual umask (022). The containers must
read
postgres-init/andconvex-supervisor.cjs. A blanketumask 077while you prepare the directory makes every container fail, as the backup runbook describes. - Secret files must be readable by the containers’ users. Setup writes the
ones containers read with mode 0644, inside a
secrets/directory with mode 0700. varlatchd runs as a user of its own and cannot read a 0600 file that you own. Keys and passwords has the details. - On a host where other people have a shell, mount
/procwithhidepid=2. The Convex backend image passes its instance secret to the backend process as a command-line argument, which every user on the host can otherwise read in/proc/<pid>/cmdline.
Network
Section titled “Network”The address
Section titled “The address”Choose how people reach Varlatch before you run setup:
| Ingress | Address | Inbound to the host | You also need |
|---|---|---|---|
| public | https://vault.example.com |
TCP 80 and 443 from the internet | a DNS name that resolves to this host in public DNS |
| tailnet | https://<machine>.<tailnet>.ts.net |
none | a Tailscale auth key, MagicDNS and HTTPS certificates enabled in the Tailscale admin console, and Compose 2.24 or newer |
| external | whatever your reverse proxy serves | whatever your proxy needs | a proxy that forwards to 127.0.0.1:8787, WebSocket upgrades for /convex/ included |
Whichever you choose, the address must be:
- An origin of its own:
https://and a host name, with no path. Varlatch cannot be served under a path such ashttps://example.com/vault. - HTTPS. Browsers only allow passkeys on HTTPS.
http://localhostworks for local testing only. - Final. Passkeys are registered for this address. Changing it later means everyone registers their passkey again, so setup refuses a different address on a rerun.
For the public ingress, the address must be a DNS name, not an IP address
or a ts.net name, and carry no port. Let’s Encrypt validates the
certificate over ports 80 and 443. A private domain validated over DNS is
not built in: use the external ingress with your proxy’s own certificate
automation.
For the tailnet ingress, the certificate covers only the machine’s full name and is published in Certificate Transparency logs, so keep the machine name non-sensitive. The Tailscale sidecar runs in userspace mode: the host needs no TUN device.
Host ports
Section titled “Host ports”| Host port | Service | When |
|---|---|---|
127.0.0.1:8787 |
varlatch-web: the dashboard, which also serves /v1 and /convex |
always; varlatch setup --port changes it |
| 80/tcp, 443/tcp, and 443/udp on all interfaces | caddy |
public ingress |
| a free loopback port Docker picks | varlatchd and convex-backend |
installations set up by varlatch setup |
127.0.0.1:8686 and 127.0.0.1:3210 |
varlatchd and convex-backend |
installations configured by hand, with the defaults |
Each fixed port must be free on the host, or the stack does not start.
PostgreSQL publishes no port at all. varlatchd’s tailnet listener (port
8687) is reachable only over Tailscale, never through a host port, and so is
its optional tailnet browser endpoint (HTTPS on port 8688 of the node,
varlatch setup --tailnet-endpoint): devices need a tailnet access rule for
that port.
Do not route a public domain at varlatchd’s or Convex’s port: passkey sign-in works only at the public URL, which the dashboard serves. Host ports lists the settings.
Outbound connections
Section titled “Outbound connections”The host connects out to:
- ghcr.io and Docker Hub, to pull images when you install and upgrade. Varlatch’s images and the Convex backend come from ghcr.io; PostgreSQL, Caddy, and Tailscale come from Docker Hub.
- GitHub, when
varlatch upgradeorvarlatch self-updatereads a release, and whenvarlatch upgrade --checklooks for a newer one (the reference backup timer runs it after each backup).varlatch upgrade --release-dir <dir>reads a release from a local directory instead. varlatchd and the dashboard never contact GitHub to look for releases: the dashboard shows what the host’s check recorded. - Let’s Encrypt, with the public ingress, to obtain and renew the certificate.
- Tailscale, with the tailnet ingress or the Tailscale overlay, so the node can join your tailnet.
- Your backup destination, when you configure an S3-compatible one.
- The platforms of Sync Targets (GitHub Actions, Coolify, Convex), when
someone configures one.
VARLATCH_SYNC=offturns delivery off: Sync Targets.
The Compose file turns off the Convex backend’s usage beacon.
Sizing
Section titled “Sizing”How these figures were measured
Section titled “How these figures were measured”Memory and processor use were read from the containers’ control groups on 2026-10-06, on a development installation that had been running for six days: 2 organizations, 3 projects, 4 environments, and 18 identities. For the five minutes around the readings, the dashboard received no requests except its own health checks. The host was an Intel Core i9-9900T (linux/amd64) with Docker 29.7.2. Only the four long-running services were running: the Caddy and Tailscale containers of the ingress overlays are not included.
Memory
Section titled “Memory”| Service | In use, without page cache | Total, with page cache | Peak, with page cache |
|---|---|---|---|
postgres |
188 MiB (52 MiB plus 136 MiB of shared buffers) | 445 MiB | 3,860 MiB |
varlatchd |
60 MiB | 115 MiB | 166 MiB |
convex-backend |
72 MiB | 92 MiB | 181 MiB |
varlatch-web |
2 MiB | 6 MiB | 25 MiB |
| All four | 322 MiB | 658 MiB |
PostgreSQL’s peak is almost all page cache: this installation had grown a 3.3 GiB Application Plane database under a release before 0.14.3 (Disk explains why). The kernel reclaims page cache when other processes need the memory.
A reading from a production installation on 2026-10-05, less than two hours after its Convex backend restarted, was similar: 72 MiB for the Convex backend, and 24 MiB plus 140 MiB of shared buffers for PostgreSQL, without page cache.
The one-shot jobs were measured on a fresh development installation on 2026-10-06, sampling each container’s memory ten times a second:
| Job | Peak |
|---|---|
convex-deploy, deploying the Application Plane functions (every setup and upgrade) |
173 MiB |
varlatch admin backup create: the host CLI |
125 MiB |
the same backup: varlatchd, and the PostgreSQL dump |
83 MiB and 79 MiB, no more than they use idle |
That backup took 1.9 seconds for a small Secret Plane database; a large audit history takes longer and more memory, as below.
Recommended: at least 2 GiB. That leaves room above the measured 658 MiB for the operating system, Docker, page cache, the ingress container, and the one-shot jobs above, which run one at a time.
For a large audit history, the capture measurements in the backup runbook ran PostgreSQL with 2 CPUs and 4 GB and varlatchd with 1 CPU and 1 GB, up to ten million audit events, with no failed requests.
Processor
Section titled “Processor”Over two idle minutes, the four services together used about a tenth of
one core: convex-backend 0.044, postgres 0.031, varlatchd 0.028, and
varlatch-web 0.004 of a core.
Recommended: 2 cores. The idle load is small, but a backup capture runs the database dump while the installation keeps serving, and that costs CPU and I/O. The capture measurements gave PostgreSQL 2 CPUs.
The images of one release, as Docker 29.7.2 reports them for linux/amd64 with its containerd image store, which keeps both the download and the unpacked image. Varlatch’s own three images were built from the current source:
| Image | Used by | On disk | Download |
|---|---|---|---|
postgres:17.6 |
postgres |
636 MB | 161 MB |
varlatchd |
varlatchd, varlatch-migrate |
474 MB | 108 MB |
varlatch-web |
varlatch-web |
95 MB | 27 MB |
| Convex backend (pinned) | convex-backend |
800 MB | 207 MB |
varlatch-convex-deploy |
convex-deploy |
734 MB | 160 MB |
caddy:2.10.2 |
public ingress | 85 MB | 23 MB |
tailscale/tailscale:v1.102.3 |
tailnet ingress | 180 MB | 61 MB |
Without an ingress overlay that is about 2.7 GB on disk and 0.7 GB to
download; the overlays add 0.1 to 0.2 GB. Layers the images share are stored
once, so the real total is somewhat lower. Sizes change a little between
releases. varlatch upgrade does not remove the previous release’s images,
so plan for two sets during an upgrade, until you remove the old ones.
The data grows over time:
- The Secret Plane database held 9.6 MiB on the measured installation. It grows with the audit history: about 0.9 GiB per million audit events in the backup runbook’s measurements.
- The Application Plane database holds the dashboard’s read models (Mirrors), which are small, and grows only when they change. Convex keeps each replaced version of a document for at least 14 days. Before 0.14.3, varlatchd stored every Mirror again once a minute even when nothing had changed, and the measured installation reached 3.3 GiB that way: an installation upgraded from an earlier release may hold several GiB of such versions.
- Backup archives hold only the Secret Plane database: about 76 MiB per
million audit events.
backup createwrites them tobackups/in the installation’s directory, and the reference timer keeps the newest 7 there. - Backup scratch space, in the system’s temporary directory unless you
pass
--scratch-dir, holds the dump and the decrypted copy that verification reads while a backup runs.
Recommended: 20 GB free. That covers two release sets, the databases, and local backups, with room for the audit history to grow. Watch the free space of Docker’s data directory. Where the data lives:
| Volume or directory | Holds |
|---|---|
postgres-data |
both databases: varlatch (the Secret Plane) and convex_self_hosted (the Application Plane) |
convex-data |
Convex’s module bundles and file storage |
varlatch-state |
backup and restore state shared by the services, and the backup history |
caddy-data, caddy-config |
the certificate and the ACME account (public ingress) |
tailscale-state |
the node’s identity and certificate (tailnet ingress) |
secrets/, recovery/, backups/ |
key files, the Root KEK escrow file, and local archives, in the installation’s directory |
Keep every volume through restarts and upgrades. Docker names them after
the Compose project, varlatch: for example varlatch_postgres-data.
Deleting an organization
Section titled “Deleting an organization”Deleting an organization runs as one database transaction when its waiting
period ends: it erases every value version, ends every membership and
revokes everything the organization holds. Measured 2026-10-30 with
scripts/measure-org-deletion.mjs, on the host above (local NVMe disk):
- PostgreSQL 17.6 limited to 2 CPUs and 4 GB.
- The deleted organization: 20 projects of 5 environments, every item with 5 versions, 25 people, 10 machine identities, 10 integrations and 2 webhooks.
- Meanwhile, one client wrote a value in another organization, each write audited, pausing 250 ms after each one.
- Lock waits were sampled from PostgreSQL every 20 ms during the deletion; a shorter wait can go unseen.
| Value versions | Database | Deletion | Other writes p50 / p95 / max, before | … during the deletion | Failed writes | Lock waits sampled |
|---|---|---|---|---|---|---|
| 100,000 | 73 MB | 2.2 s | 15 / 16 / 17 ms | 16 / 28 / 28 ms | 0 | 0 |
| 1,000,000 | 636 MB | 34.1 s | 13 / 14 / 17 ms | 27 / 108 / 1,508 ms | 0 | 0 |
What this means:
- Deletion time grows with the organization’s value versions: about 35 seconds per million on this budget. Three runs at a million took 31 to 39 seconds. Organizations of up to about a million value versions are supported; a larger one is deleted too, but its one transaction takes longer and writes more to PostgreSQL’s write-ahead log.
- Other organizations keep working. No write in another organization failed, and no lock wait was seen in sampling: the deletion records its audit events, which take the installation-wide audit order, only at its end. A diagnostic run that sampled every wait saw the other writes wait on the disk instead, on write-ahead log flushes while the erasure fills the log, and while a checkpoint writes out changed pages. So a few writes took longer: the slowest in each of three runs at a million took 0.8 to 3.8 seconds.
- Plan for the write-ahead log: the erasure rewrites every version, and PostgreSQL logs each change. Replicas, and backups that keep write-ahead log segments, hold that log for a while.
To measure your own hardware:
MEASURE_VERSIONS=100000,1000000 node scripts/measure-org-deletion.mjs
after pnpm build. It runs varlatchd’s code against a throwaway PostgreSQL
container, which it removes at the end. See the script’s header for the
shape and the load it applies.

