Deployment
Run Vaultbase in production — bare metal, Docker, behind a reverse proxy, with TLS and persistent storage.
Vaultbase is a single binary plus a <dataDir> folder. That’s the whole
deployment artifact. Treat it like SQLite + a small Node service.
What you ship
Section titled “What you ship”- The binary for your target (
vaultbase-linux-x64,vaultbase-macos-arm64, etc.). Linux binaries are also published as.xz(about 50 MB instead of ~127 MB); the one-shot installer andvaultbase update(since 0.40.2) download that whenxzis installed, and verify the decompressed binary against the same.sha256/ signature. - An empty data directory — Vaultbase creates the DB and folders on first run
Verifying the release (cosign + SBOM)
Section titled “Verifying the release (cosign + SBOM)”Every release uploaded to GitHub is signed keyless via Sigstore — no static signing key, the cert chain proves the build came from the project’s GitHub Actions workflow. CycloneDX SBOMs ride alongside.
The installers and vaultbase update accept a
signature only from the repository’s release.yml workflow, run for that
version’s tag. A signature from another workflow, a branch, or an older
release is refused.
The installers stop if the release’s checksum can’t be fetched. For an old
release that publishes none, pass --skip-checksum (-SkipChecksum on
Windows).
The installers check the cosign signature before installing whenever
cosign is on the PATH. Without it they warn and rely on the release’s
checksum alone. A signature that is missing or doesn’t match stops the
install. --verify-sig (-VerifySig on Windows) makes the check required,
so the install fails when cosign isn’t there:
curl -fsSL https://get.vaultbase.dev | sudo sh -s -- --verify-sig& ([scriptblock]::Create((irm https://get.vaultbase.dev/install.ps1))) -VerifySigReleases from before signing have no signature. To install one, pass
--no-verify-sig (-NoVerifySig). A --version / -Version that isn’t a
release tag (vX.Y.Z, with an optional pre-release suffix) is refused.
Manual verify with cosign. Releases from v0.39.1 on carry a Sigstore
bundle (<binary>.sigstore.json), which is what current cosign verifies with:
TAG=v0.39.1BIN=vaultbase-linux-x64
curl -L -o "$BIN" "https://github.com/vaultbase-sh/vaultbase/releases/download/$TAG/$BIN"curl -L -o "$BIN.sigstore.json" "https://github.com/vaultbase-sh/vaultbase/releases/download/$TAG/$BIN.sigstore.json"
cosign verify-blob \ --bundle "$BIN.sigstore.json" \ --certificate-identity "https://github.com/vaultbase-sh/vaultbase/.github/workflows/release.yml@refs/tags/$TAG" \ --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \ "$BIN"Earlier releases have a .sig / .pem pair instead (cosign 3 still accepts
it, with a deprecation warning): pass --signature "$BIN.sig" --certificate "$BIN.pem" in place of --bundle. With cosign 2, add --new-bundle-format
to verify a bundle. The installers and vaultbase update use the bundle when
there is one and the pair otherwise.
Each release also publishes a CycloneDX SBOM (<binary>.cdx.json) for
downstream trivy / grype / dependency-track ingestion, and a
vaultbase-source.cdx.json for the source tree.
Bare metal
Section titled “Bare metal”For a start-to-finish walkthrough, see the recipes: a plain VPS and behind Cloudflare.
# On the serveruseradd --system --home-dir /srv/vaultbase --shell /usr/sbin/nologin vaultbasemkdir -p /srv/vaultbasechown vaultbase:vaultbase /srv/vaultbasecp vaultbase-linux-x64 /usr/local/bin/vaultbasechmod +x /usr/local/bin/vaultbase
# systemd unit at /etc/systemd/system/vaultbase.service[Unit]Description=VaultbaseAfter=network.target
[Service]Type=simpleUser=vaultbaseWorkingDirectory=/srv/vaultbaseEnvironment=VAULTBASE_DATA_DIR=/srv/vaultbase/dataEnvironment=VAULTBASE_PORT=8091ExecStart=/usr/local/bin/vaultbaseRestart=on-failureRestartSec=5
[Install]WantedBy=multi-user.target
# Thensystemctl daemon-reloadsystemctl enable --now vaultbaseLogs go to journald (journalctl -u vaultbase -f) plus the structured JSONL
logs in <dataDir>/logs/.
The install script does all of this for you
and writes a hardened unit like
deploy/systemd/vaultbase.service,
which keeps its data in /var/lib/vaultbase and reads its environment from
/etc/vaultbase/vaultbase.env.
Docker
Section titled “Docker”The image is published to the GitHub container registry for every release, for
linux/amd64 and linux/arm64: ghcr.io/vaultbase-sh/vaultbase, tagged with
the version (0.25.0), the minor version (0.25) and latest. It is the
signed release binary on Debian slim. The image is built only from a release
published by the repository’s release workflow on a tag push, and the build
checks the release’s SHA256SUMS with cosign against that workflow’s
identity before using the binary. The image itself is signed keyless with
cosign, by docker.yml on the default branch (see
Verifying the image). latest moves only for the newest release,
and 0.25 only for the newest patch of that minor, so rebuilding an older
tag doesn’t move them. The data folder is /data: mount a
volume there. The server runs as an unprivileged user; started as root, the
entrypoint first gives /data to that user, so volumes that hosts mount as
root work as they are.
# Once: generate the two keys and keep this fileprintf 'VAULTBASE_JWT_SECRET=%s\nVAULTBASE_ENCRYPTION_KEY=%s\n' \ "$(openssl rand -base64 32)" "$(openssl rand -base64 32)" > vaultbase.envchmod 600 vaultbase.env
docker run -d --name vaultbase \ -p 8091:8091 \ -v vaultbase-data:/data \ --env-file vaultbase.env \ -e VAULTBASE_READ_WORKERS=0 \ ghcr.io/vaultbase-sh/vaultbase:0.27.1docker logs vaultbase | grep "Setup key"Reuse the same vaultbase.env every time you recreate the container (for
example to upgrade): a new JWT secret signs everyone out, and a new
encryption key makes encrypted values unreadable.
Open http://<host>:8091/_/setup and create the first admin with that key.
- Read workers. A container sees the host’s cores, not its own limit, and
each read worker costs 50–100 MB. Set
VAULTBASE_READ_WORKERSto what the machine can spare (cores minus two;0on 1–2 vCPU). - Health checks. The image’s
HEALTHCHECKcalls/_/health(the process answers). Point a load balancer or orchestrator’s readiness probe at/_/ready. - Postgres. The image includes
pg_dump,pg_restoreandpsqlfrom the PostgreSQL 18 client, so backups and restore work withVAULTBASE_DATABASE_URLset (see Databases). - Pin a version in production;
latestmoves with every new release.
Verifying the image
Section titled “Verifying the image”The image is signed keyless with cosign. Only docker.yml run from the
default branch publishes it, so the signature names that workflow on
refs/heads/main. Check an image before running it:
cosign verify ghcr.io/vaultbase-sh/vaultbase:<tag> \ --certificate-identity https://github.com/vaultbase-sh/vaultbase/.github/workflows/docker.yml@refs/heads/main \ --certificate-oidc-issuer https://token.actions.githubusercontent.comDocker Compose with HTTPS
Section titled “Docker Compose with HTTPS”deploy/docker/docker-compose.yml
runs the image with Caddy in front, which gets a certificate for your domain:
git clone https://github.com/vaultbase-sh/vaultbase && cd vaultbasecp deploy/docker/.env.example .env # set DOMAIN, generate both secretsdocker compose -f deploy/docker/docker-compose.yml --env-file .env up -ddocker compose -f deploy/docker/docker-compose.yml logs vaultbase | grep "Setup key"Point the domain’s DNS at the server first; Caddy needs ports 80 and 443 (or
set HTTP_PORT / HTTPS_PORT).
Postgres
Section titled “Postgres”Vaultbase runs on SQLite unless VAULTBASE_DATABASE_URL names a Postgres
database (16 or later). Choose before the first admin exists: the choice
is final. What differs from SQLite is
on Databases.
On the same machine
Section titled “On the same machine”# Ubuntu 24.04 ships Postgres 16; elsewhere use the PostgreSQL apt repositoryapt install postgresqlsudo -u postgres psql <<'SQL'CREATE ROLE vaultbase LOGIN PASSWORD 'choose-a-password' CREATEROLE;CREATE DATABASE vaultbase OWNER vaultbase;SQLOwning the database lets the server create its tables, the search extension
and the schema a restore stages into. CREATEROLE lets it make the
read-only login the SQL
page uses; leave it out and set VAULTBASE_PG_READONLY_URL instead.
Then add the URL to the service’s environment (/etc/vaultbase/vaultbase.env
for the install script) and restart:
The server packages include pg_dump, pg_restore and psql of the same
version, which backups and restore use. Run vaultbase doctor with the same
environment: it says whether they are new enough for restore (a release
from August 2025 or later).
A managed database
Section titled “A managed database”Use the URL your provider gives you, with TLS:
-
Always set
sslmode. Without it the connection isn’t encrypted, even when the server offers TLS (libpq tries TLS first; Bun’s driver doesn’t).requireencrypts without checking the certificate;verify-fullalso checks it against the system’s trusted authorities and the host name. -
A provider’s own certificate authority:
sslrootcert=in the URL is ignored. PointNODE_EXTRA_CA_CERTSat the provider’s CA file instead, withsslmode=verify-full. -
Don’t use
sslmode=preferagainst a server without TLS: the connection waits 30 seconds and fails instead of falling back. -
Connect directly, or through a pooler in session mode. Vaultbase keeps a connection that listens for changes (
LISTEN), takes session advisory locks while migrating, and uses prepared statements. None of those work through a pooler in transaction mode. -
Connections. Each server opens up to 8, plus one that listens, plus up to 4 short-lived ones for read-only SQL: plan for 13 per server.
-
A role that can’t create roles (common on managed databases): set
VAULTBASE_PG_READONLY_URLto a login that can only read, as described under Read-only SQL. -
Backups. Your provider’s backups keep working. The backups page and
vaultbase backupneed a PostgreSQL client of the server’s major version or newer on the server’s machine; the Docker image includes one.
Several servers
Section titled “Several servers”Any number of servers can share one Postgres database behind a load balancer, with no sticky sessions: changes and realtime events reach every server through the database. Each server needs:
- the same
VAULTBASE_JWT_SECRETandVAULTBASE_ENCRYPTION_KEY; - file storage they all reach: S3, or one mounted directory;
VAULTBASE_TRUSTED_PROXIESset to the load balancer’s addresses, so rate limits and the realtime per-address cap see client addresses.
upstream vaultbase { server 10.0.0.11:8091; server 10.0.0.12:8091;}Rate limits count per server, and scheduled jobs, backups, queue jobs and webhook deliveries each run once, on whichever server claims them. See Several servers for what is shared and what isn’t.
One-click hosts
Section titled “One-click hosts”Each host runs the published image with a persistent volume for /data, a
generated setup key, and health checks on /_/ready. Keep one instance: on
SQLite the database lives on that volume.
| Host | How | Notes |
|---|---|---|
| Render | Deploy to Render (from render.yaml) | A disk needs a paid instance (Starter or above). The setup key is in the service’s Environment tab. The encryption key is generated into /data/.encryption-key: back it up with the data. |
| Fly.io | fly launch --config deploy/fly/fly.toml --copy-config --no-deploy, then the commands in deploy/fly/fly.toml | One shared-cpu-1x machine with 512 MB and a 1 GB volume. Machines don’t stop when idle, so realtime and scheduled jobs keep running. |
| Railway | The Vaultbase template (recipe) | The setup key is in the service’s Variables tab. |
All three put their own proxy in front, so the templates set
VAULTBASE_TRUSTED_PROXIES to the private address ranges that proxy connects
from; that’s what lets rate limits and the realtime per-address cap see real
client addresses. Hosts without persistent volumes (such as DigitalOcean App
Platform) don’t suit SQLite; run the install script
on a droplet or VPS instead, or use Postgres.
Behind a reverse proxy
Section titled “Behind a reverse proxy”Vaultbase reads X-Forwarded-For for the client IP — set it on your proxy
and set VAULTBASE_TRUSTED_PROXIES (CIDR list) on the vaultbase process,
otherwise the header is silently ignored as a defensive default and every
request looks like it came from the proxy. See the env-vars table below for
details.
The header is only read when the connection comes from a trusted proxy, and
it is read from the right: the client address is the rightmost entry that
isn’t one of your proxies. Have the proxy set the header to the address
it saw ($remote_addr in nginx, {client_ip} in Caddy) rather than append
to whatever the client sent.
Complete templates live in the repository:
deploy/caddy/Caddyfile and
deploy/nginx/vaultbase.conf (its header
says how to raise nginx’s worker_connections). The
deploy notes cover the host settings that matter under
load.
# Caddyfileapi.example.com { reverse_proxy localhost:8091 { header_up X-Forwarded-For {client_ip} transport http { versions h1 h2 } }}Caddy handles TLS automatically and sets X-Forwarded-Proto itself.
server { listen 443 ssl http2; server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
client_max_body_size 100M;
# WebSocket needs Upgrade headers location /realtime { proxy_pass http://127.0.0.1:8091; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 86400; # keep WS open }
# SSE realtime fallback: don't buffer the stream location /api/v1/realtime { proxy_pass http://127.0.0.1:8091; proxy_http_version 1.1; proxy_buffering off; proxy_read_timeout 86400; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; }
location / { proxy_pass http://127.0.0.1:8091; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; }}X-Forwarded-Proto tells Vaultbase the client came over HTTPS, so auth
cookies get the Secure flag. Like X-Forwarded-For, it’s only read from a
trusted proxy.
Required env vars in production
Section titled “Required env vars in production”| Var | Recommended |
|---|---|
VAULTBASE_JWT_SECRET | 32+ bytes from openssl rand -base64 32. Persist; rotating invalidates every existing JWT. Set but empty counts as unset (the secret then comes from <dataDir>/.secret, written at first start). |
VAULTBASE_ENCRYPTION_KEY | 32 bytes from openssl rand -base64 32. Required for encrypted fields. Don’t lose it — losing it makes encrypted values unreadable. |
VAULTBASE_DATA_DIR | A persistent volume — never /tmp. |
VAULTBASE_PORT | If proxying, you can keep :8091 and not expose it publicly. Must be a whole number from 0 to 65535; anything else stops the start with an error. |
VAULTBASE_TRUSTED_PROXIES | CIDR list (e.g. 127.0.0.1/32,10.0.0.0/8). Required behind any reverse proxy — without it, X-Forwarded-For is silently ignored and rate-limits / audit-log IPs / brute-force lockout all key on the proxy IP instead of the real client. |
VAULTBASE_HOST | 127.0.0.1 when a reverse proxy on the same host is the only way in. Default: every interface, IPv4 and IPv6. |
VAULTBASE_SETUP_KEY | Key that first-admin setup requires. Optional: without it the server generates one at first boot, prints it and keeps it in <dataDir>/.setup-key until an admin exists. |
Auto-generated values land in <dataDir>/.secret and
<dataDir>/.encryption-key if you don’t set them — but
in containers/ephemeral hosts that volume might not survive restarts, so set
both explicitly.
Bootstrapping the first admin
Section titled “Bootstrapping the first admin”Two safe ways to seed the very first superuser. Pick one — the choice is mostly about whether you want a one-shot CLI or to use the web wizard.
Option A — CLI (no public surface)
Section titled “Option A — CLI (no public surface)”The binary doubles as a setup tool. Run it once on the host before binding to a public port:
Refuses to run when an admin already exists, unless you pass --force (which
adds another admin alongside the existing ones). No HTTP, no race.
On an installed service, setup-admin reads /etc/vaultbase/vaultbase.env
(or the file named by VAULTBASE_ENV_FILE) for variables not already set, so
it writes to the service’s database. So do doctor, backup, token and
wipe (since 0.32.3).
Option B — Web wizard with the setup key
Section titled “Option B — Web wizard with the setup key”While no admin exists, creating one needs a setup key. On first boot the server prints it, with a link that fills it in:
No admin exists yet. Create one at: http://localhost:8091/_/setup#key=9UzxnHtX…Setup key: 9UzxnHtX…Open the link (or paste the key into the wizard’s Setup key field), or
send it as an X-Setup-Key header to POST /api/v1/admin/setup. The key
lives in <dataDir>/.setup-key (mode 0600) until the first admin exists, so
it survives restarts. To choose it yourself, set VAULTBASE_SETUP_KEY; the
installer does this and prints it.
Health and readiness
Section titled “Health and readiness”Two probes, both public and outside the rate limiter:
GET /_/health— liveness. Answers200as long as the process is up and serving HTTP; it checks nothing else. Use it to decide whether to restart the process.GET /_/ready— readiness. Checks what the server needs to do its work. Point load balancer health checks and orchestrator readiness probes here.
| Check | What it does | Fails as |
|---|---|---|
database | A round trip to the database within 2 s | down → unready |
storage | A lookup in the configured file storage within 3 s | down → unready |
read_workers | Fewer read workers running than configured (shown only when there are any) | degraded |
queues | The queue scheduler isn’t running, or jobs are more than 5 minutes past their time | degraded |
logs | A log write failed in the last 5 minutes | degraded |
/_/ready answers 503 with status: "unready" when the database or
storage is down. Otherwise it answers 200 with status: "ready", or
"degraded" when something works but not fully (the server still serves;
alert on it rather than taking it out of rotation):
{ "data": { "status": "degraded", "checks": { "database": { "status": "ok", "ms": 1 }, "storage": { "status": "ok", "ms": 0 }, "read_workers": { "status": "degraded", "detail": "3 of 4 running; reads on the main thread meanwhile" }, "queues": { "status": "ok" }, "logs": { "status": "ok" } } }}GET /api/health also answers { "data": { "status": "ok", "version": "…" } }
without checking anything.
Kubernetes:
livenessProbe: httpGet: { path: /_/health, port: 8091 }readinessProbe: httpGet: { path: /_/ready, port: 8091 } periodSeconds: 10 timeoutSeconds: 5Docker Compose (the published image includes curl):
healthcheck: test: ["CMD", "curl", "-fsS", "http://localhost:8091/_/ready"] interval: 30s timeout: 5s retries: 3Settings & data live in the DB
Section titled “Settings & data live in the DB”OAuth credentials, SMTP config, rate-limit rules, email templates, auth
feature flags — everything you’d typically configure via env vars in other
backends — live in the vaultbase_settings table and are edited from the
admin UI. So your deployment config is just env vars + the data directory.
To pre-seed settings (e.g. CI seeding OAuth creds), you have two options:
- Restore a backed-up
data.dbthat contains them. - Set them after the first boot with
PATCH /api/v1/admin/settingsand an admin token.
Migration snapshots carry collections only, not settings.
Apply a snapshot on startup
Section titled “Apply a snapshot on startup”For stateless deploys (immutable images, ephemeral containers, fresh dev environments), pass a schema snapshot directly to the binary:
./vaultbase --apply-snapshot=schema.json --snapshot-mode=additive--snapshot-mode=additive(default) — creates collections that don’t exist; leaves existing ones alone.--snapshot-mode=sync— also updates existing collections so they match.- Both equals (
--flag=value) and space (--flag value) forms work. - Idempotent — re-running with the same file is a no-op.
On success, the binary prints applied snapshot: N created, M updated, K unchanged and continues to listen normally.
On failure (missing file, invalid JSON, unknown mode, malformed snapshot, or
any per-collection error), the message is written to stderr and the process
exits with code 1 — the server never starts, so a broken snapshot
won’t silently boot a half-applied DB.
# Typical container entrypoint./vaultbase --apply-snapshot=/etc/vaultbase/schema.json --snapshot-mode=syncShared commits
Section titled “Shared commits”Record creates, updates and deletes that arrive in the same event-loop turn run in one SQLite transaction and commit together, instead of one commit per write. What stays the same:
- A write returns only after the COMMIT that holds it. If that COMMIT fails, every write in it fails; a statement that fails only undoes itself.
- Realtime events, webhooks and after-hooks run after the commit.
- Batches, cascading deletes and schema changes commit any open group first, then run in their own transaction.
The one difference: another request can read a write up to one event-loop
turn before it commits. It only disappears if that COMMIT itself fails (disk
full, I/O error), and then the write’s own request fails too. Set
VAULTBASE_GROUP_COMMIT=0 to commit every write on its own. Shared commits
are off under vaultbase cluster.
WAL checkpoints (copying the write-ahead log back into data.db) run on a
background thread with their own connection, so no request waits for one. If
that thread stops, SQLite’s own checkpointing takes over again. The admin
Monitoring page shows which one is running.
Read worker threads
Section titled “Read worker threads”Record list and get-one requests are served on worker threads, one per spare
CPU core by default (VAULTBASE_READ_WORKERS; 0 keeps everything on one
thread, a number sizes the pool, at most 8 by default). Each worker holds its
own connection to the database and costs roughly 50–100 MB. Writes, auth,
admin, files, realtime and user code stay on the main thread, so there is
still one writer and one set of rate limits and realtime connections.
What a worker sees: a collection or settings change on the main thread at once; which tables a committed write touched, so its list-totals cache keeps the rest; the first read of a collection after start (or after a change to it) is served on the main thread while it refills its collection cache. A worker that dies is dropped and its in-flight requests fail; a forwarded request with no reply within 30 s fails with 503. The service log says how many workers started, and the admin Monitoring page shows them under “read workers · forwarded” with their latencies folded into the request pipeline.
On Linux (since v0.27.0) the read workers also listen on the server’s port, and
the kernel spreads connections over them and the main thread. A worker serves
list and get-one itself and passes every other request to the main thread over
a loopback listener on a random 127.0.0.1 port, with the client’s address, so
trusted proxies and per-client limits work the same; the rate-limit buckets stay
on the main thread. A slow request on a worker (a search, a filter on an
unindexed field) holds up the other requests on that worker’s connections.
VAULTBASE_WORKER_LISTENERS=0 keeps every connection on the main thread, as
before; macOS and Windows don’t spread connections and always do.
Cluster mode (multi-process) — deprecated
Section titled “Cluster mode (multi-process) — deprecated”Read worker threads replace cluster mode: one process now uses every core, with one set of rate limits and realtime connections and cache invalidation that reaches every thread. Cluster mode still works and prints a deprecation line at start; a later release removes it. To run it anyway:
./vaultbase cluster # auto: one worker per CPU coreVAULTBASE_WORKERS=4 ./vaultbase cluster # explicit countEach worker calls Bun.serve({ reusePort: true }) so the kernel
load-balances incoming TCP connections across them — no in-process scheduler,
no shared memory, just N copies of the same binary listening on the same
port. Workers are respawned 1s after a crash; SIGTERM/SIGINT to the
parent triggers a 30s graceful drain.
The same <dataDir> is shared by every worker. SQLite WAL mode handles the
concurrent writes.
Limits — cluster mode is for stateless REST scaling
Section titled “Limits — cluster mode is for stateless REST scaling”Each worker is a separate process with its own memory. In particular:
- Caches are per worker. A collection, hook, route, cron job or queue worker created or changed through one worker isn’t seen by the others until they restart. Make schema and code changes, then restart the service.
- Realtime is per worker (see below).
- Rate limits and realtime connection caps are per worker, so the effective limit is N times the configured one.
- Online restore is disabled (
409). Stop the service, replacedata.dbwith the backup, start it again. - Cron jobs, daily maintenance, the update check and the orphan-file sweep run on worker 0 only; queue jobs, webhook deliveries and scheduled backups are claimed in the database and run on any worker.
Worker 0 prints a warning with these limits at startup.
Verifying which worker answered
Section titled “Verifying which worker answered”/_/health returns the responding worker’s id and pid:
curl http://localhost:8091/_/health# { "data": { "status": "ok", "worker_id": "3", "pid": 41872, "uptime_s": 287 } }Repeat the call a few times — worker_id should rotate. If it doesn’t,
your reverse proxy is pinning to one upstream (sticky sessions, hash-based
load-balancing) and you’re not actually using the cluster.
Realtime caveat — front with sticky sessions or stay single-process
Section titled “Realtime caveat — front with sticky sessions or stay single-process”There is no inter-worker pub/sub yet. Each worker keeps its own subscription map, so a client connected to worker A will not see writes that landed on worker B. All workers share one port and the kernel picks one per connection, so a reverse proxy can’t route realtime to a particular worker. If you rely on realtime, stay single-process: one Bun event loop sustains thousands of open WebSockets — the cluster is a CPU optimization, not a connection-count one.
Cluster mode is deprecated and won’t get cross-worker fan-out (see the roadmap): read worker threads use every core from one process, and realtime there reaches every subscriber.
Self-update
Section titled “Self-update”Update the binary in place from the latest signed GitHub release:
# Interactive — prompts before swapping the binary./vaultbase update
# Non-interactive (cron-friendly)./vaultbase update --yes
# Check only, don't download. Exits 0 if in sync, 1 if newer available../vaultbase update --check
# Pin to a specific version./vaultbase update --version 0.8.0 --yes
# Backwards move (refuses by default)./vaultbase update --version 0.6.0 --allow-downgrade --yesWhat it does:
- Detects platform (
linux-x64,linux-arm64,linux-x64-musl,macos-x64,macos-arm64,windows-x64) - Queries
api.github.com/repos/vaultbase-sh/vaultbase/releases/latest - Compares
vaultbase --versionto the release tag - Downloads the binary (on Linux its
.xzwhenxzis installed, since 0.40.2; the raw binary if that fails),.sha256, and the signature - Verifies SHA-256 (always — fails closed)
- Verifies cosign signature if
cosignis onPATH(warns if not). Only a signature from the release workflow, run for that version’s tag, is accepted - Atomically renames the new binary into place
- Prints “restart vaultbase to apply”
The running process keeps executing the old inode until you restart — no mid-request swap. With systemd:
./vaultbase update --yes && sudo systemctl restart vaultbaseCosign verification
Section titled “Cosign verification”Install cosign to enable cryptographic provenance on every update:
# Linux x64curl -L https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64 \ -o /usr/local/bin/cosign && chmod +x /usr/local/bin/cosign
# Verify it's installedcosign versionWithout cosign, vaultbase update still verifies SHA-256 from the release
manifest but warns that cryptographic verification is skipped. The
--no-verify flag explicitly skips cosign even when installed (SHA-256
remains enforced). Don’t pass it.
Windows
Section titled “Windows”The running .exe is locked while in use. Stop the daemon, run the update,
restart:
sc stop vaultbase./vaultbase.exe update --yessc start vaultbaseAuto-update via cron
Section titled “Auto-update via cron”Cautious deployments: weekly check + apply with restart. Not recommended for production without staging review of release notes first.
# /etc/cron.d/vaultbase-update — every Sunday at 04:13 UTC13 4 * * 0 root /usr/local/bin/vaultbase update --yes --quiet && systemctl restart vaultbase