Self-hosted Vaultwarden on a Mac Mini (ARM), exposed at vault.solenhamn.com through a Cloudflare Tunnel, with continuous SQLite replication to Cloudflare R2 via Litestream.
| File | Purpose |
|---|---|
Dockerfile |
Bundles cloudflared into the official vaultwarden/server image. |
start.sh |
Container entrypoint — launches cloudflared, then vaultwarden. |
docker-compose.yml |
Two services: the bundled vaultwarden+tunnel container, and a Litestream sidecar. |
litestream.yml |
Replicates /data/db.sqlite3 to R2 every 10 s with daily snapshots, 30-day retention. |
.env / .env.example |
Secrets (tunnel token, admin token, R2 creds). |
scripts/backup.sh |
Checkpoints the WAL and lists current R2 snapshots. |
scripts/restore.sh |
Pulls the DB back from R2 (fresh install or DR). |
scripts/verify.sh |
Restores into a scratch dir and runs PRAGMA integrity_check. |
-
Cloudflare Tunnel — create a tunnel in Zero Trust → Networks → Tunnels. Add a public hostname
vault.solenhamn.com→ servicehttp://vaultwarden:80. Copy the connector token intoCLOUDFLARE_TUNNEL_TOKEN.Note on the service address: cloudflared and vaultwarden run in the same container, so
http://localhost:80also works. We usehttp://vaultwarden:80(the Docker service name) because it remains correct if you ever split cloudflared into its own container. -
R2 — create a bucket (e.g.
vaultwarden-backup) and an API token with Object Read & Write scoped to that bucket. Note the account-level S3 endpointhttps://<account-id>.r2.cloudflarestorage.com. -
Env file
cp .env.example .env # fill in ADMIN_TOKEN, CLOUDFLARE_TUNNEL_TOKEN, R2_* -
Build & run
docker compose build docker compose up -d
-
Open https://vault.solenhamn.com, set
SIGNUPS_ALLOWED=truebriefly to create your account, then flip back tofalseanddocker compose up -dto apply.
Run weekly:
scripts/verify.shIt restores the latest R2 snapshot into a scratch directory, runs
PRAGMA integrity_check, and prints row counts for users, ciphers,
organizations. Exits non-zero if anything is off.
On the same machine (e.g. accidental DB corruption):
scripts/restore.sh # restore latest
scripts/restore.sh -timestamp 2026-04-28T12:00:00Z # point-in-time
docker compose up -dThe script stops the stack, moves the existing
data/db.sqlite3 aside as db.sqlite3.pre-restore.<epoch>, removes the
-wal / -shm sidecar files, and pulls the chosen snapshot from R2.
On a fresh machine:
git clone <this repo>
cd vaultwarden
cp .env.example .env # fill in
mkdir -p data
scripts/restore.sh
docker compose up -dDNS keeps pointing through the same Cloudflare Tunnel, so as soon as the
container is up the vault is reachable again at
vault.solenhamn.com.
- The custom image runs two processes in one container; Docker's restart policy bounces the container if either dies.
- Cloudflare Tunnel terminates TLS at Cloudflare's edge; vaultwarden
speaks plain HTTP on
:80inside the container, never exposed to the host network. - Litestream's
sync-interval: 5mmeans recovery point objective is ~5 minutes. Snapshots are kept for 30 days (retention: 720h). data/is gitignored. Never commit.env.