Self-hosting a secrets vault: choosing a platform
Docker on a VPS, Cloudflare Workers, Vercel or Cloud Run? What changes between platforms for a blind-server vault, and the two decisions you make only once.
Crunu teamPublished 7 min read
On this page
Self-hosting a secrets vault used to mean running a cluster, managing storage backends and keeping unseal keys safe. For a vault whose server never holds a decryption key, the job is much smaller. The server stores ciphertext, public keys, signed documents and metadata. Choosing where it runs is mostly a question of availability, operations and where the metadata lives, not of who can read the secrets.
This post compares the platforms Crunu's server supports, the trade-offs that actually differ between them, and the two decisions that are hard to change later.
First, do you need to self-host?
Crunu is available as a hosted service at app.crunu.com, free during early access (see pricing). Because encryption happens only in browsers and agents, the hosted server holds the same thing a self-hosted one would: ciphertext and metadata. Self-hosting does not make values more private. It changes other things:
- Where metadata lives. Names of projects, environments and machines, email addresses and the audit log stay in your infrastructure.
- Network placement. The server can sit in your cloud account, behind your tunnel, close to your machines.
- Operations. Backups, updates, monitoring and the server's own key are your responsibility.
- Mobility. You can move between platforms on your own schedule.
If none of those matter to you yet, start hosted. If they do, read on.
Two decisions you make once
The server address
The public address of the server, set as PUBLIC_ORIGIN, is permanent. Sign-in signatures, share links, passkeys and every enrolled agent are tied to it. Moving to another platform later is fine, as long as the address stays the same and only DNS changes. Pick a name you will keep, such as vault.company.example, not a platform-generated hostname.
The server key
Each server has a SERVER_KEY, 32 random bytes in its .env file. It signs sessions, share links and invitations and encrypts second-factor secrets. It cannot decrypt any environment. Losing it signs everyone out and breaks share links, though environment data is not lost. A leak of the key together with the database weakens TOTP second factors. Keep a copy of .env in a password manager from day one, and know that the key can be rotated without signing anyone out.
The initial .env is created by one command, which generates the server key, a one-time bootstrap key for the first owner and a secret for scheduled jobs:
node scripts/server-init.mjs --origin https://vault.company.exampleThe platforms
The same server, with the same database schema, runs on every platform below, and each has a deploy script. What differs is the database, how background cleanup runs and the platform's own limits.
Your own server with Docker
docker compose -f deploy/docker/compose.yml up -d --buildThe server runs in Docker next to Caddy, which handles TLS. Caddy serves sites over HTTPS by default and obtains and renews certificates automatically (Caddy docs). The database is SQLite in a Docker volume or Postgres alongside. You need a domain, a DNS record pointing at the server and ports 80 and 443 open.
The container runs with a read-only root file system, no capabilities and an unprivileged user. Background cleanup, which removes expired data and checks state-based alerts, runs every minute.
Variant: Cloudflare Tunnel. If you do not want any inbound ports open, a second compose file runs the server behind cloudflared, which makes outbound-only connections to Cloudflare, so the host needs no publicly routable address or inbound listener (Cloudflare docs).
Choose this when you want full control and the fewest external dependencies.
Cloudflare Workers and D1
sh deploy/cloudflare/deploy.sh --name vault-companyNo server to maintain. The script builds the Worker, creates a D1 database if needed and deploys code and secrets together as one version.
The limit to know about is CPU time. On Workers Free, a request gets 10 ms of CPU; on Workers Paid, the default is 30 seconds (Cloudflare Workers limits). Heavy requests, such as signing in, rotating keys or saving a large environment, can exceed 10 ms and fail, so the free plan is fine for a trial but a team should use Workers Paid. Cleanup runs every five minutes.
Choose this when you want no servers and are comfortable with Cloudflare as the host.
Vercel with Postgres on Neon
sh deploy/vercel/deploy.sh --project vault-company --prodFunctions on Vercel with a Postgres database on Neon. Preview deployments get their own database and server key, so a branch never touches production data.
Cleanup on Vercel runs as a cron job. On the Hobby plan, cron jobs run at most once a day, with timing precision of about an hour; Pro allows once a minute (Vercel docs). Expiry of sessions, links and break-glass access is checked on every request and does not depend on cleanup. Some alerts do, including one network-rule alert that only cleanup can detect, and only if it runs every few minutes. If you use network rules with machine passes, run cleanup every minute on Pro.
Choose this when your team already lives on Vercel.
Google Cloud Run
sh deploy/gcp/deploy.sh --project my-project --region europe-west1 --service vaultPostgres on Cloud SQL or Neon, with the server's secrets in Secret Manager. Each revision is pinned to the secret versions it was deployed with, so rolling back traffic also rolls back secrets.
By default Cloud Run uses request-based billing, where CPU is allocated only while requests are being processed (Cloud Run docs), and the deploy script lets instances scale to zero. The server's cleanup timer therefore runs only when there is traffic. Expiry checks are unaffected, but state-based alerts such as expired machine leases, expiring certificates and undelivered keys appear only on the next cleanup, and expired data is removed later.
Choose this when your infrastructure is in Google Cloud and delayed alerts are acceptable, or when you route enough traffic that the delay is small.
AWS App Runner
A deploy script for App Runner exists, with secrets in Systems Manager Parameter Store. However, AWS has closed App Runner to new customers; existing customers can keep using it, and AWS recommends Amazon ECS Express Mode instead (AWS docs). For a new AWS account, use one of the other platforms. On accounts that already have App Runner, cleanup behaves as on Cloud Run.
Side by side
| Platform | Database | Cleanup | Main caveat |
|---|---|---|---|
| Own server, Docker | SQLite or Postgres | Every minute | You run the host |
| Own server, Cloudflare Tunnel | SQLite or Postgres | Every minute | Depends on Cloudflare for ingress |
| Cloudflare Workers | D1 | Every 5 minutes | Free plan CPU limit; use Paid for a team |
| Vercel | Postgres (Neon) | Cron; daily on Hobby | Frequent cron needs Pro |
| Google Cloud Run | Postgres | Only with traffic | State-based alerts can be delayed |
| AWS App Runner | Postgres | Only with traffic | Closed to new AWS customers |
Network rules need the real client address
If you plan to restrict environments by network, the server must see each client's real IP address. On Cloudflare Workers and Vercel the platform provides it. On your own server, Cloud Run and App Runner, the TRUST_PROXY setting tells the server how many proxies sit in front of it, and the diagnostic tool checks that it matches.
Two configurations quietly break network rules:
- Cloudflare proxying in front of Cloud Run, App Runner or Vercel. The server then sees Cloudflare's addresses instead of clients'. Keep such DNS records unproxied.
- Cloudflare's Pseudo IPv4 in overwrite mode. It replaces IPv6 client addresses with placeholder IPv4 ones, so IPv6 rules never match. Turn it off or use the mode that adds a header.
After deploying
-
Run the diagnostic tool against the new address. It checks health, TLS and certificate expiry, security headers, the server clock, migrations, cleanup and the client address:
Shell node scripts/doctor.mjs https://vault.company.example -
Open the address, choose Create the first owner and enter the bootstrap key from
.env. It stops working after the first owner exists. -
Save the Emergency Kit, create a recovery code and turn on a second factor.
-
Set up notification channels for alerts: a signed webhook for Slack, Teams or PagerDuty, or email through Resend.
-
Point external uptime monitoring at
/v2/health. -
Schedule backups.
Backups and moving
Backups are simpler than for most vaults, because the database contains no plaintext values. The server's export command writes the whole database to an archive you can store anywhere:
node scripts/crunu-server.mjs export --env-file .env --from postgres:env:DATABASE_URL --out backups/crunu-2026-10-27.ndjson.gz
node scripts/crunu-server.mjs verify --in backups/crunu-2026-10-27.ndjson.gzKeep .env separately, in a password manager. There is no built-in backup scheduler; run the export from cron or your CI. Remember that restoring a backup rolls back every change made after it, and agents and browsers then need to reset their memory of the server, because they would otherwise refuse the older state as a rollback.
The same archive is how you move between platforms. Export from the old database, import into an empty one on the new platform, verify, then switch DNS. Because the address stays the same, people, links and agents carry on.
Picking one
- Smallest dependency footprint, full control: your own server with Docker, optionally behind Cloudflare Tunnel.
- No servers, low cost: Cloudflare Workers on the Paid plan.
- Already on Vercel: Vercel with Neon, on Pro if you use network rules.
- Already on Google Cloud: Cloud Run, accepting delayed state-based alerts at low traffic.
- Not ready to operate anything: the hosted service.
Whatever you choose, the choice is reversible. The address and the server key are the parts to get right on day one.
More in self-hosting overview, migrate between platforms, backups and restore and the self-hosting page.
Sources
- Automatic HTTPS, Caddy documentation, checked October 27, 2026.
- Cloudflare Tunnel, Cloudflare docs, checked October 27, 2026.
- Limits, Cloudflare Workers docs, checked October 27, 2026.
- Usage and pricing for cron jobs, Vercel docs, checked October 27, 2026.
- Billing settings for services, Cloud Run docs, checked October 27, 2026.
- AWS App Runner availability change, AWS docs, checked October 27, 2026.