Vaultwarden Raspberry Pi gives you a self-hosted password manager that is fully compatible with all official Bitwarden clients (browser extensions, mobile apps, and desktop) running on hardware you own with no subscription cost. Vaultwarden is a Rust implementation of the Bitwarden server API, lightweight enough to run comfortably on a Pi 4 with 4GB RAM, and stores its entire database in a single SQLite file that backs up in seconds. This guide covers Docker Compose deployment, HTTPS via Caddy, admin token generation, Fail2Ban brute-force protection, automated backups, and secure remote access.
Last tested: Raspberry Pi OS Bookworm Lite 64-bit | April 15, 2026 | Raspberry Pi 4 Model B (4GB) | Vaultwarden 1.32.0 | Docker 27.3 | Caddy 2.9.1
Key Takeaways
- Vaultwarden 1.28 and later require the
ADMIN_TOKENto be an Argon2 hash, not a plain string. Using a plain string triggers a deprecation warning and will stop working in a future release. Generate the hash withvaultwarden hashorargon2as shown in the setup section. - Store credentials in a
.envfile, not hardcoded incompose.yaml. Setchmod 600 .envimmediately after creating it. The compose file can be committed to version control; the.envfile should never be. - Vaultwarden is community-maintained and has not undergone the third-party security audits that Bitwarden’s official server receives annually. It is widely trusted for personal and homelab use. Factor this in before using it to manage credentials for a team or business.
- Without backups, a dead SD card or SSD means total vault loss. The SQLite database in
./data/db.sqlite3is the only thing that matters. Back it up daily to a separate device.
Vaultwarden Raspberry Pi: How It Works
Vaultwarden implements the Bitwarden server API in Rust. All official Bitwarden clients (Chrome, Firefox, Safari, and Edge extensions, plus iOS and Android apps) connect to it as if it were a Bitwarden cloud server. The difference is that the server runs on your Pi and the database lives on your hardware.
Passwords and notes are encrypted on the client device before being sent to the server. The server stores ciphertext only. It never sees plaintext credentials. The master password never leaves the client. This means that even if someone gains access to the db.sqlite3 file, they cannot read the vault contents without the master password.
| Feature | Vaultwarden (self-hosted) | Bitwarden cloud | KeePassXC |
|---|---|---|---|
| Data location | Your hardware | Bitwarden servers | Local file |
| Cost | Free | Free tier + paid | Free |
| Multi-device sync | Built-in via server | Built-in | Manual (Syncthing etc.) |
| Browser integration | Bitwarden extension | Bitwarden extension | Add-on only |
| Third-party audit | No | Yes (annual) | Yes |
| Offline access | Local network | Requires internet | Always |
OS and Hardware Preparation
Pi 4 with 4GB RAM is the minimum practical configuration. The 8GB model and Pi 5 both work. The setup steps are identical. Use a USB SSD for the Vaultwarden data directory rather than microSD. The SQLite database is written on every vault sync from every client. MicroSD card wear under this pattern accelerates failure, and a dead card means total vault loss without a backup. See Booting Raspberry Pi from USB SSD for the setup.
Flash Raspberry Pi OS Bookworm Lite 64-bit using Raspberry Pi Imager. In the advanced settings, set hostname, enable SSH, and configure credentials. After first boot:
sudo apt update && sudo apt full-upgrade -y
Set a static IP on Bookworm with nmcli. The vault must be reachable at a consistent address:
sudo nmcli connection modify "Wired connection 1" \
ipv4.method manual \
ipv4.addresses 192.168.1.100/24 \
ipv4.gateway 192.168.1.1 \
ipv4.dns 1.1.1.1
sudo nmcli connection up "Wired connection 1"
Installing Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo usermod -aG docker $USER
sudo apt install docker-compose-plugin -y
Log out and back in, then verify:
docker compose version
Expected result: Returns Docker Compose version 2.x or higher. All subsequent commands in this article use docker compose (with a space) not docker-compose (with a hyphen), which is the deprecated v1 command.

Deploying Vaultwarden
Create the project directory and a .env file for credentials:
mkdir -p ~/vaultwarden/data
cd ~/vaultwarden
Generate the admin token
Vaultwarden 1.28 and later require an Argon2 hash for the admin token. A plain string triggers a deprecation warning and will stop working in a future release. Generate the hash using the Vaultwarden container itself:
# Pull the image first
docker pull vaultwarden/server:latest
# Generate an Argon2 hash of your chosen admin password
# Replace 'your-admin-password' with a strong unique password
docker run --rm -it vaultwarden/server:latest \
/vaultwarden hash --preset owasp
# When prompted, enter your admin password
# Copy the full $argon2id$... string that is output
Create the .env file with the hash output and a secret key:
# Generate a secret key
openssl rand -base64 48
# Create .env -- paste the argon2 hash and secret key here
cat > .env <<EOF
ADMIN_TOKEN='$argon2id$v=19$m=65540,t=3,p=4$...(paste full hash here)...'
ROCKET_SECRET_KEY=$(openssl rand -base64 32)
EOF
chmod 600 .env
Write compose.yaml
cat > compose.yaml <<'EOF'
services:
vaultwarden:
image: vaultwarden/server:latest
container_name: vaultwarden
restart: unless-stopped
env_file: .env
environment:
WEBSOCKET_ENABLED: "true"
SIGNUPS_ALLOWED: "false"
LOG_FILE: "/data/vaultwarden.log"
LOG_LEVEL: "warn"
volumes:
- ./data:/data
ports:
- "127.0.0.1:8080:80"
EOF
Binding to 127.0.0.1:8080 rather than 0.0.0.0:8080 means port 8080 is only reachable from the Pi itself, not from other devices on the LAN. Caddy, running on the same host, can reach it at localhost:8080 while external devices cannot bypass the proxy.
docker compose up -d
docker compose logs -f vaultwarden
Expected result: The log shows Vaultwarden starting, the SQLite database initialising, and the rocket web server listening. No error about ADMIN_TOKEN format. Navigate to http://localhost:8080 on the Pi to confirm the login page loads.

HTTPS with Caddy
Bitwarden clients require HTTPS for vault sync. Caddy provides automatic TLS via Let’s Encrypt with no certificate management required. Install Caddy from the official repository:
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
| sudo gpg --dearmor -o /etc/apt/keyrings/caddy.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
| sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update && sudo apt install caddy -y
Create /etc/caddy/Caddyfile:
vault.yourdomain.duckdns.org {
reverse_proxy localhost:8080
# WebSocket support for real-time sync
reverse_proxy /notifications/hub localhost:3012
reverse_proxy /notifications/hub/negotiate localhost:8080
}
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
For the full DuckDNS setup and router port forwarding steps, see Caddy Reverse Proxy Raspberry Pi. The vault needs ports 80 and 443 forwarded to the Pi for Let’s Encrypt certificate issuance.
Expected result: Navigating to https://vault.yourdomain.duckdns.org shows the Vaultwarden login page with a valid Let’s Encrypt certificate. No browser security warnings.
Account Setup and Admin Panel
With SIGNUPS_ALLOWED: false, accounts must be created from the admin panel. Navigate to:
https://vault.yourdomain.duckdns.org/admin
Enter the admin password you used when generating the Argon2 hash. From the admin panel, go to Users and invite each account by email. The user receives an invitation link and sets their own master password.
In the admin panel, configure:
- Minimum password length: 16 characters
- 2FA enforcement: enable for all accounts
- Session timeout: 30 minutes or less for shared devices
- Disable organisation creation if this is a single-user setup
Enable two-factor authentication in account Settings > Two-Step Login using an authenticator app. Save the recovery codes offline. Losing the authenticator device without recovery codes locks the account permanently.
Security Hardening
Firewall
sudo apt install ufw -y
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
Fail2Ban brute-force protection
Vaultwarden logs failed login attempts to /data/vaultwarden.log inside the container, which maps to ~/vaultwarden/data/vaultwarden.log on the host. Fail2Ban reads this host path:
sudo apt install fail2ban -y
Create /etc/fail2ban/filter.d/vaultwarden.conf:
[Definition]
failregex = ^.*Username or password is incorrect\. Try again\. IP: <ADDR>.*$
ignoreregex =
Create /etc/fail2ban/jail.d/vaultwarden.conf:
[vaultwarden]
enabled = true
port = 80,443
filter = vaultwarden
logpath = /home/pi/vaultwarden/data/vaultwarden.log
maxretry = 5
bantime = 3600
findtime = 300
sudo systemctl enable --now fail2ban
sudo fail2ban-client status vaultwarden
Expected result: fail2ban-client status vaultwarden shows the jail active with 0 currently banned IPs. After five failed login attempts from the same IP within 5 minutes, that IP is blocked for 1 hour.
Run rootless
Add a user directive to the vaultwarden service in compose.yaml to run the container as a non-root user:
user: "1000:1000"
Fix ownership of the data directory to match:
sudo chown -R 1000:1000 ~/vaultwarden/data
docker compose down && docker compose up -d
Backups
The entire vault lives in ~/vaultwarden/data/db.sqlite3. The rest of the ./data directory holds attachments, configuration, and the RSA key pair used for signing. Back up the entire ./data directory, not just the SQLite file.
Create a backup script at ~/vaultwarden/backup.sh:
#!/bin/bash
TIMESTAMP=$(date +"%Y%m%d-%H%M")
BACKUP_DIR="$HOME/vaultwarden/backups"
DATA_DIR="$HOME/vaultwarden/data"
mkdir -p "$BACKUP_DIR"
tar -czf "$BACKUP_DIR/vaultwarden-$TIMESTAMP.tar.gz" -C "$DATA_DIR" .
# Keep last 14 daily backups
ls -tp "$BACKUP_DIR"/*.tar.gz | tail -n +15 | xargs -I {} rm -- {}
chmod +x ~/vaultwarden/backup.sh
# Schedule daily at 02:00
(crontab -l 2>/dev/null; echo "0 2 * * * /home/pi/vaultwarden/backup.sh") | crontab -
Copy backups to a separate device. Backups on the same Pi as the database are not a backup. They will be lost alongside the original if the Pi fails:
# To a USB drive
rsync -avh ~/vaultwarden/backups/ /mnt/usb/vaultwarden-backups/
# To a remote host
rsync -avz ~/vaultwarden/backups/ user@nas:/backups/vaultwarden/
Test the restore process before you need it:
mkdir -p ~/vaultwarden-test/data
tar -xzf ~/vaultwarden/backups/vaultwarden-YYYYMMDD-HHMM.tar.gz \
-C ~/vaultwarden-test/data
# Spin up a test container pointing at this directory and confirm login works

For automated encrypted backups with deduplication and retention policies, see BorgBackup Raspberry Pi Prune Policies. Borg handles the ./data directory efficiently and adds encryption at rest.
Remote Access
Two approaches work well for accessing Vaultwarden from outside the home network. The first is direct HTTPS via Caddy and DuckDNS as configured above. The vault is publicly reachable at https://vault.yourdomain.duckdns.org with Fail2Ban and firewall hardening. This is the most convenient option for mobile use.
The second is a VPN. With WireGuard connecting your phone or laptop to the home network, you access Vaultwarden at its LAN IP with no public exposure at all. This is the more conservative option. See WireGuard Raspberry Pi Site-to-Site VPN for the setup. The trade-off is that Bitwarden clients only sync when the VPN is active.
Migrating from LastPass, 1Password, or Bitwarden Cloud
Migration takes under 15 minutes for most password managers. In all cases, export first, import to Vaultwarden, verify a sample of entries match, then delete from the old service.
| Source | Export format | Vaultwarden import path |
|---|---|---|
| LastPass | LastPass CSV | Tools > Import Data > LastPass (csv) |
| 1Password | 1PUX or CSV | Tools > Import Data > 1Password (1pux) |
| Bitwarden cloud | JSON (Encrypted export) | Tools > Import Data > Bitwarden (json) |
| KeePassXC | KeePass XML (2.x) | Tools > Import Data > KeePass XML |
The encrypted Bitwarden JSON export preserves folder structure, custom fields, and attachments, and Vaultwarden reads the same format natively.
Maintenance
Keep Vaultwarden updated. Pull the latest image and restart the container. The data volume is untouched by the update:
docker compose pull
docker compose up -d
docker image prune -f
To pin a specific version if an update causes problems, edit compose.yaml to use a version tag:
image: vaultwarden/server:1.32.0
Check the Vaultwarden release notes before each major version update for any migration steps. Monthly maintenance tasks:
- Confirm the backup cron job produced a dated file today
- Run
sudo apt update && sudo apt upgrade -yto keep the host patched - Check
sudo fail2ban-client status vaultwardenfor unusual ban activity - Test-restore a backup to a temporary container
- Rotate the admin password and regenerate the Argon2 hash every 6 months
Troubleshooting
Container does not start
docker compose logs vaultwarden
Common causes: port 8080 already in use (sudo ss -tlnp | grep 8080), data directory owned by wrong user (fix with sudo chown -R 1000:1000 ~/vaultwarden/data), or a YAML syntax error in compose.yaml. If the log shows an ADMIN_TOKEN format error, the token is not a valid Argon2 hash. Regenerate it as shown in the deployment section.
Admin panel returns 401 or blank page
The admin password must match the one used to generate the Argon2 hash in .env. The hash is not the password. Do not paste the hash into the login form. Enter the plain password you chose; Vaultwarden verifies it against the stored hash. If the credentials are correct and the page still fails, try a private browser window to rule out cached state, then restart the container.
Bitwarden clients cannot sync
# Check the WebSocket notification hub is running
curl -I https://vault.yourdomain.duckdns.org/notifications/hub
# Check Caddy is routing WebSocket correctly
sudo journalctl -u caddy -n 50 | grep -i websocket
In each Bitwarden client, confirm the server URL is set to your Vaultwarden domain under Settings > Server URL. The default Bitwarden cloud URL must be replaced with your own. Browser extensions require a browser restart after changing the server URL.
Database locked
SQLite database locks occur when two processes access the file simultaneously, typically a backup running while the container restarts. The lock usually clears within seconds. If it does not, stop the container, check for leftover .db-wal or .db-shm files in ./data, and delete them before restarting. These are SQLite write-ahead log files that are safe to remove when Vaultwarden is not running.
FAQ
Can I use the official Bitwarden browser extension with Vaultwarden?
Yes. All official Bitwarden clients are fully compatible with Vaultwarden. In the browser extension, go to Settings > Server URL and enter your Vaultwarden domain. iOS and Android apps have the same setting under Account > Server. After changing the server URL, log in with your Vaultwarden account credentials.
What happens if my Raspberry Pi dies?
If you have daily backups of the ./data directory, recovery takes about 10 minutes on a replacement Pi: install Docker, restore the backup, and start the container. No backup means starting over with an empty vault. This is why the backup section is mandatory, not optional.
Is Vaultwarden safe for sensitive passwords?
The encryption model is the same as Bitwarden: AES-256-CBC with PBKDF2 or Argon2 key derivation, all performed client-side before anything is sent to the server. The server stores ciphertext only. The distinction from Bitwarden’s official server is that Vaultwarden has not undergone formal third-party security audits. For personal use it is widely trusted in the homelab community. For business use managing sensitive credentials, factor in the audit distinction.
Can I run Vaultwarden without exposing it to the internet?
Yes. With VPN access via WireGuard, your devices connect to the home network remotely and access Vaultwarden at its LAN IP. No ports need to be forwarded. The trade-off is that Bitwarden clients only sync when the VPN is active. For a household where all devices are usually on the home network, LAN-only operation is a practical choice.
How do I update Vaultwarden safely?
Run a backup immediately before updating. Then pull the new image and restart: docker compose pull && docker compose up -d. The data volume is separate from the container image and is not affected by the update. If the new version has problems, pin the previous version tag in compose.yaml and restart. Check the release notes before any major version update for database migration steps.
References
- https://github.com/dani-garcia/vaultwarden/wiki
- https://github.com/dani-garcia/vaultwarden/releases
- https://bitwarden.com/help/getting-started-browserext/
- https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page
About the Author
Chuck Wilson has been programming and building with computers since the Tandy 1000 era. His professional background includes CAD drafting, manufacturing line programming, and custom computer design. He runs PidiyLab in retirement, documenting Raspberry Pi and homelab projects that he actually deploys and maintains on real hardware. Every article on this site reflects hands-on testing on specific hardware and OS versions, not theoretical walkthroughs.
Last tested hardware: Raspberry Pi 4 Model B (4GB), USB 3.0 SSD. Last tested OS: Raspberry Pi OS Bookworm Lite 64-bit. Vaultwarden 1.32.0, Docker 27.3, Caddy 2.9.1.

