Deployment
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.) - 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 one-shot installer takes a --verify-sig flag that runs the cosign
check before installing:
curl -fsSL https://get.vaultbase.dev | sh -s -- --verify-sigManual verify with cosign:
TAG=v0.1.6BIN=vaultbase-linux-x64
curl -L -o "$BIN" "https://github.com/vaultbase-sh/vaultbase/releases/download/$TAG/$BIN"curl -L -o "$BIN.sig" "https://github.com/vaultbase-sh/vaultbase/releases/download/$TAG/$BIN.sig"curl -L -o "$BIN.pem" "https://github.com/vaultbase-sh/vaultbase/releases/download/$TAG/$BIN.pem"
cosign verify-blob \ --signature "$BIN.sig" \ --certificate "$BIN.pem" \ --certificate-identity-regexp '^https://github\.com/vaultbase-sh/vaultbase/' \ --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \ "$BIN"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”# On the servermkdir -p /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/.
Docker
Section titled “Docker”# DockerfileFROM debian:bookworm-slimCOPY releases/vaultbase-linux-x64 /usr/local/bin/vaultbaseRUN chmod +x /usr/local/bin/vaultbase
ENV VAULTBASE_DATA_DIR=/dataENV VAULTBASE_PORT=8091EXPOSE 8091VOLUME ["/data"]
CMD ["vaultbase"]docker build -t vaultbase .docker run -d --name vaultbase \ -p 8091:8091 \ -v vaultbase-data:/data \ -e VAULTBASE_JWT_SECRET="$(openssl rand -base64 32)" \ vaultbasedocker-compose.yml:
services: vaultbase: build: . restart: unless-stopped ports: - "8091:8091" volumes: - vaultbase-data:/data environment: VAULTBASE_DATA_DIR: /data VAULTBASE_JWT_SECRET: ${VAULTBASE_JWT_SECRET} VAULTBASE_ENCRYPTION_KEY: ${VAULTBASE_ENCRYPTION_KEY}
volumes: vaultbase-data:The linux-x64-musl build is the right pick for Alpine-based images.
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.
# Caddyfileapi.example.com { reverse_proxy localhost:8091 { header_up X-Forwarded-For {remote_host} transport http { versions h1 h2 } }}Caddy handles TLS automatically.
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_read_timeout 86400; # keep WS open }
location / { proxy_pass http://127.0.0.1:8091; proxy_set_header X-Forwarded-For $remote_addr; }}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. |
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. |
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_SETUP_KEY | Optional first-boot guard. When set, POST /api/v1/auth/admin/setup requires the same value in an X-Setup-Key header. Use it to close the race where an attacker reaches /setup before you do on a public IP, then unset it after seeding the first admin. |
Auto-generated values land in <dataDir>/.secret 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:
./vaultbase setup-admin --email ops@example.com --password 's3cr3t-please'Refuses to run when an admin already exists, unless you pass --force (which
adds another admin alongside the existing ones). No HTTP, no race.
Option B — Web wizard with a setup key
Section titled “Option B — Web wizard with a setup key”Set VAULTBASE_SETUP_KEY to a high-entropy random string before first boot:
export VAULTBASE_SETUP_KEY="$(openssl rand -hex 32)"./vaultbaseThe browser wizard at /_/setup will refuse to create the admin without an
X-Setup-Key header carrying the same value. Curl the create endpoint
yourself, or paste the key into the wizard prompt.
Once the first admin exists, unset the env var and restart — the gate is
only useful while vaultbase_admin is empty.
Health check
Section titled “Health check”GET /api/v1/health → { "data": { "status": "ok" } }Use this for k8s liveness/readiness or load-balancer health.
Settings & 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 three options:
- Apply a migration snapshot on startup that includes the settings.
- Restore a backed-up
data.dbthat contains them. - Pass a snapshot file at startup via the CLI flag (next section).
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=syncCluster mode (multi-process)
Section titled “Cluster mode (multi-process)”The default ./vaultbase runs single-process — one event loop, ~one CPU
core, fine for most workloads. To use more cores, run the bundled cluster
orchestrator instead:
./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; per-collection prepared-statement caches are per-worker
(no cross-process invalidation needed since DDL goes through the records API
on every worker before the next read).
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. Two options:
- Stay single-process. A single Bun event loop sustains thousands of open WebSockets — the cluster is a CPU optimization, not a connection- count one.
- Pin the WS path to one worker. Configure your reverse proxy to send
/realtime(and the SSEGET /api/v1/realtime) to a single upstream (e.g. nginxip_hashon the realtime location), and let the rest of the API fan out across workers.
Cross-worker realtime fan-out is on the roadmap; treat the current behavior as a known limit, not a bug.
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,
.sha256,.sig,.pem - Verifies SHA-256 (always — fails closed)
- Verifies cosign signature if
cosignis onPATH(warns if not) - 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