Self-Hosting Vaultwarden: Ditching 1Password for Total Password Ownership
I used 1Password for years. It worked well — until the price hikes. Each increase was a reminder that I was renting access to my own credentials. My passwords lived on someone else's infrastructure, behind someone else's pricing decisions. When 1Password raised prices again, I decided to stop paying for what I could own outright.
Today I run Vaultwarden — a lightweight, Rust-based server compatible with the official Bitwarden ecosystem. It costs me nothing beyond the electricity my homelab already draws. My passwords are stored locally, backed up nightly to a NAS, and accessible from every device through the same Bitwarden apps I'd use with a cloud subscription.
Why Self-Host Your Password Manager?
The argument for self-hosting passwords comes down to three things:
- Data sovereignty — Your credentials never leave infrastructure you control. No third-party breach can expose your vault.
- Zero recurring cost — 1Password charges ~$36–60/year per user. Vaultwarden is free, runs on minimal hardware, and supports unlimited users.
- No vendor lock-in — Your data sits in a local SQLite database. You can export it, back it up, or migrate it anytime without depending on a vendor's export tool continuing to exist.
What Made the Switch Easy
Two things eliminated any friction in moving away from 1Password:
1. Painless migration. 1Password exports to .1pux format, and Bitwarden's import tool handles it natively. I exported my entire vault — logins, secure notes, credit cards, identities — and imported it into Vaultwarden in under two minutes. Nothing was lost, nothing needed manual re-entry.
2. The Bitwarden app ecosystem. Because Vaultwarden is API-compatible with Bitwarden, I use the official Bitwarden apps on every platform — iOS, Android, macOS, Windows, Linux, and browser extensions for Chrome, Firefox, and Safari. The UX is identical to what you'd get with a Bitwarden cloud subscription. Autofill works, biometric unlock works, TOTP codes work. My family didn't even notice the backend changed.
This is the key insight: you're not giving up convenience by self-hosting. The client experience is the same. The only thing that changes is where the data lives — and now it lives with you.
Architecture Overview
My setup runs on a Dell Optiplex micro PC (the same host that runs the rest of my homelab). The architecture is simple:
┌─────────────────┐ Cloudflare Tunnel ┌───────────────────┐
│ Bitwarden Apps │ ◄──────────────────────── │ Optiplex Host │
│ (all devices) │ vault.semesmieh.com │ Vaultwarden:80 │
└─────────────────┘ └───────────────────┘
│
NFS mount + rsync
Nightly 3AM
│
▼
┌───────────────────┐
│ Synology NAS │
│ /volume1/docker/ │
│ (active standby) │
└───────────────────┘
- Primary — Vaultwarden runs in Docker on the Optiplex, exposed on port 8081.
- External access — A Cloudflare Tunnel exposes the service as
vault.semesmieh.comwith zero open ports on my router. - NFS mount — The NAS's
/volume1/dockeris mounted locally at/mnt/nas_dockervia NFS, giving the backup script direct filesystem access. - Backup — A nightly cron job creates a safe SQLite snapshot and rsyncs it (plus attachments) to the NAS over the NFS mount.
- Failover — The NAS holds a ready-to-run standby compose. Data is already in place — just start the container and re-point the tunnel.
Implementation Guide
Docker Compose
The primary instance is a single container. Here's the compose file (secrets injected via environment variables):
services:
vaultwarden:
image: vaultwarden/server:latest
container_name: vaultwarden
restart: unless-stopped
user: "1000:1000"
ports:
- "8081:80"
volumes:
- ./data:/data
environment:
DOMAIN: "https://vault.semesmieh.com"
PUSH_ENABLED: "true"
PUSH_INSTALLATION_ID: "${PUSH_INSTALLATION_ID}"
PUSH_INSTALLATION_KEY: "${PUSH_INSTALLATION_KEY}"
SMTP_HOST: smtp.gmail.com
SMTP_FROM: "${SMTP_FROM}"
SMTP_PORT: "587"
SMTP_SECURITY: starttls
SMTP_USERNAME: "${SMTP_USERNAME}"
SMTP_PASSWORD: "${SMTP_APP_PASSWORD}"
Key decisions:
user: "1000:1000"— Run as a non-root user. The data directory must be owned by this UID.DOMAIN— Must match the URL clients connect to. Vaultwarden uses this for WebSocket and push notification registration.- Secrets are stored in a
.envfile (not committed to Git — see my secrets management post).
Push Notifications
Vaultwarden supports Bitwarden's push notification relay so vault changes sync instantly across devices (no manual refresh needed). To enable it:
- Register at bitwarden.com/host to get an installation ID and key.
- Set
PUSH_ENABLED=truealong with yourPUSH_INSTALLATION_IDandPUSH_INSTALLATION_KEYin the compose environment. - Ensure the
DOMAINvariable matches the external URL (the push relay validates this).
Reference: Vaultwarden Wiki — Enabling Push Notifications
SMTP (Email Notifications)
Email is used for account verification, 2FA recovery, and emergency access invitations. I use a Gmail App Password:
SMTP_HOST: smtp.gmail.comSMTP_PORT: 587withSMTP_SECURITY: starttls- Generate an App Password at myaccount.google.com/apppasswords (requires 2FA on the account).
Reference: Vaultwarden Wiki — SMTP Configuration
Cloudflare Tunnel (External Access)
Rather than opening ports or managing dynamic DNS, I use a Cloudflare Tunnel to expose Vaultwarden externally. The tunnel runs as a lightweight cloudflared daemon that creates an outbound-only connection to Cloudflare's edge — no inbound firewall rules needed.
The tunnel routes vault.semesmieh.com → http://localhost:8081. Cloudflare handles TLS termination, DDoS protection, and access policies.
Setup guide: Cloudflare Tunnel — Get Started
Backup & Disaster Recovery
SQLite databases require special care during backup — you can't just cp a live database file. Vaultwarden includes a built-in backup command that uses SQLite's Online Backup API to produce a consistent snapshot without stopping the service.
NFS Mount to NAS
The Optiplex mounts the Synology NAS's Docker volume over NFS. This gives the backup script direct filesystem access to the NAS — no SSH keys or remote rsync daemons needed:
# /etc/fstab entry
10.0.0.10:/volume1/docker /mnt/nas_docker nfs defaults,nofail,_netdev,x-systemd.automount 0 0
Key flags:
_netdev— Waits for network before mounting (prevents boot hangs).nofail— Boot continues even if NAS is unreachable.x-systemd.automount— Mounts on first access (lazy mount), so the system doesn't block at boot waiting for the NAS.
Reference: Arch Wiki — NFS Mount via fstab
The Backup Script
This runs every night at 3 AM via a cron job in /etc/cron.d/vaultwarden-backup:
#!/bin/bash
set -euo pipefail
BACKUP_DIR="/home/anas/vaultwarden/data"
NAS_DEST="/mnt/nas_docker/vaultwarden/data"
# Step 1: Safe SQLite backup via Vaultwarden's built-in command
docker exec vaultwarden /vaultwarden backup
# Step 2: Remove stale WAL/SHM on NAS
rm -f "$NAS_DEST/db.sqlite3-wal" "$NAS_DEST/db.sqlite3-shm"
# Step 3: Copy clean backup as the main DB on NAS
BACKUP_FILE=$(ls -t "$BACKUP_DIR"/db_*.sqlite3 | head -1)
cp "$BACKUP_FILE" "$NAS_DEST/db.sqlite3"
# Step 4: Rsync attachments, config, keys (exclude DB files and icon cache)
rsync -az --no-perms --no-owner --no-group \
--exclude='db.sqlite3' \
--exclude='db.sqlite3-wal' \
--exclude='db.sqlite3-shm' \
--exclude='db_*.sqlite3' \
--exclude='icon_cache' \
"$BACKUP_DIR/" "$NAS_DEST/"
# Step 5: Cleanup local backup file
rm -f "$BACKUP_FILE"
The cron entry (in /etc/cron.d/vaultwarden-backup):
# /etc/cron.d/vaultwarden-backup
0 3 * * * root /home/anas/vaultwarden/backup-to-nas.sh >> /home/anas/vaultwarden/backup.log 2>&1
Because the NAS volume is an NFS mount, the rsync and copy operations write directly to the NAS filesystem — no network transfer overhead beyond standard NFS. The entire backup completes in under a second for a typical vault.
NAS Active Standby
The NAS holds a complete, always-current copy of the Vaultwarden data — synced nightly over the NFS mount via rsync. Because the data is already sitting on the NAS filesystem, failover is nearly instantaneous:
- SSH into the NAS and start the standby container:
docker compose -f docker-compose.nas-standby.yml up -d - Update the Cloudflare Tunnel to point
vault.semesmieh.comat the NAS IP (10.0.0.10). - Vaultwarden comes up immediately with data at most 24 hours old — no restore process, no downloading backups.
The key enabler here is that the rsync writes directly to the path the standby container will mount (/volume1/docker/vaultwarden/data). There's no extraction step. The data is always ready to serve.
The standby compose uses the NAS volume path and identical environment variables. Full config is in my homelab repo.
Migrating from 1Password
The migration took less than five minutes:
- Export from 1Password — In the 1Password desktop app: File → Export → choose
.1puxformat. This includes logins, secure notes, credit cards, and identities. - Import into Vaultwarden — Log into your Vaultwarden web vault → Tools → Import Data → select "1Password (1pux)" as the format → upload the file.
- Verify — Spot-check a few entries. TOTP seeds, custom fields, and attachments all came across intact for me.
- Install Bitwarden apps — On each device, install the Bitwarden app and set the server URL to your self-hosted domain (
https://vault.semesmieh.com) before logging in. - Decommission 1Password — Once you've verified everything, cancel the subscription and delete the 1Password account.
References
- Vaultwarden GitHub Repository
- Vaultwarden Wiki — comprehensive setup, configuration, and admin guide
- Vaultwarden — Push Notifications Setup
- Vaultwarden — SMTP Configuration
- Vaultwarden — Backing Up Your Vault
- SQLite Online Backup API — how Vaultwarden safely snapshots the live database
- Bitwarden — Host Your Own (Push Relay Registration)
- Cloudflare Tunnel — Get Started
- Bitwarden — Import from 1Password
- Arch Wiki — NFS — client mount options and fstab configuration
- rsync man page — flags used in the backup script
Closing Thought
Passwords are the keys to your digital life. Trusting them to a vendor means trusting their pricing, their security practices, and their continued existence. Self-hosting with Vaultwarden gives you total ownership — your data lives on your hardware, backed up on your terms, accessible through battle-tested Bitwarden clients that work everywhere.
The best part? Moving away from a vendor-managed solution wasn't painful. The migration was trivial, the apps are the same, and the ongoing maintenance is a single Docker container and a cron job. Sometimes the self-hosted alternative really is just better.