Every security professional says the same thing: use a password manager. The argument for self-hosting your password manager is less universally agreed upon, but it’s compelling — your vault lives on your hardware, your credentials never touch a third-party server, and you control the backup and recovery process entirely.
Vaultwarden (formerly Bitwarden_RS) is an unofficial, lightweight Bitwarden-compatible server written in Rust. It implements the full Bitwarden API, meaning every Bitwarden client — browser extensions, desktop apps, the mobile apps — works with it out of the box. The official Bitwarden server requires 2GB+ of RAM and a SQL Server instance; Vaultwarden runs in under 100MB of RAM on a Raspberry Pi.
This guide covers a production-quality Vaultwarden deployment: HTTPS with Traefik, automated encrypted backups, hardened admin configuration, and migrating from other password managers.
Architecture Overview
Internet / LAN
│
▼
Traefik (reverse proxy + TLS termination)
│
▼
Vaultwarden (port 80 internally)
│
SQLite / PostgreSQL (vault data)
│
Backup job → encrypted archive → S3 / Backblaze / local NAS
Vaultwarden uses SQLite by default — perfectly adequate for personal and small-team use. For larger deployments or higher availability requirements, it supports PostgreSQL and MySQL/MariaDB.
Deployment with Docker Compose and Traefik
Directory Structure
vaultwarden/
├── docker-compose.yml
├── .env
└── data/ # Vaultwarden data volume (mounted)
├── db.sqlite3
├── attachments/
├── sends/
└── config.json
Environment File
1
2
3
4
5
6
7
8
|
# .env — keep this out of git
DOMAIN=vault.yourdomain.com
ADMIN_TOKEN= # generate with: openssl rand -base64 48
SMTP_HOST=smtp.gmail.com
SMTP_FROM=vault@yourdomain.com
SMTP_PORT=587
SMTP_USERNAME=vault@yourdomain.com
SMTP_PASSWORD=your-app-password
|
Generate a strong admin token:
1
2
3
|
openssl rand -base64 48
# Or argon2 hash (recommended for Vaultwarden 1.28+):
echo -n "your-admin-password" | argon2 $(openssl rand -base64 32) -id -t 3 -m 15 -p 4 -l 32 -e
|
Docker Compose — Vaultwarden + Traefik
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
|
# docker-compose.yml
services:
traefik:
image: traefik:v3.0
container_name: traefik
restart: unless-stopped
command:
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
- "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
- "--certificatesresolvers.letsencrypt.acme.email=admin@yourdomain.com"
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
# Uncomment for staging cert while testing:
# - "--certificatesresolvers.letsencrypt.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory"
ports:
- "80:80"
- "443:443"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "./letsencrypt:/letsencrypt"
networks:
- proxy
vaultwarden:
image: vaultwarden/server:latest
container_name: vaultwarden
restart: unless-stopped
environment:
DOMAIN: "https://${DOMAIN}"
ADMIN_TOKEN: "${ADMIN_TOKEN}"
# Disable open registration — invite only
SIGNUPS_ALLOWED: "false"
INVITATIONS_ALLOWED: "true"
# Email (optional but highly recommended for 2FA recovery)
SMTP_HOST: "${SMTP_HOST}"
SMTP_FROM: "${SMTP_FROM}"
SMTP_PORT: "${SMTP_PORT}"
SMTP_SECURITY: "starttls"
SMTP_USERNAME: "${SMTP_USERNAME}"
SMTP_PASSWORD: "${SMTP_PASSWORD}"
# Security settings
WEBSOCKET_ENABLED: "true"
PASSWORD_HINTS_ALLOWED: "false"
SHOW_PASSWORD_HINT: "false"
# Logging
LOG_LEVEL: "warn"
EXTENDED_LOGGING: "false"
# Data directory
DATA_FOLDER: "/data"
volumes:
- "./data:/data"
networks:
- proxy
labels:
- "traefik.enable=true"
# Main web UI and API
- "traefik.http.routers.vaultwarden.rule=Host(`${DOMAIN}`)"
- "traefik.http.routers.vaultwarden.entrypoints=websecure"
- "traefik.http.routers.vaultwarden.tls.certresolver=letsencrypt"
- "traefik.http.routers.vaultwarden.service=vaultwarden"
- "traefik.http.services.vaultwarden.loadbalancer.server.port=80"
# WebSocket for live sync
- "traefik.http.routers.vaultwarden-ws.rule=Host(`${DOMAIN}`) && Path(`/notifications/hub`)"
- "traefik.http.routers.vaultwarden-ws.entrypoints=websecure"
- "traefik.http.routers.vaultwarden-ws.tls.certresolver=letsencrypt"
- "traefik.http.routers.vaultwarden-ws.service=vaultwarden-ws"
- "traefik.http.services.vaultwarden-ws.loadbalancer.server.port=3012"
# Security headers middleware
- "traefik.http.routers.vaultwarden.middlewares=vaultwarden-headers"
- "traefik.http.middlewares.vaultwarden-headers.headers.stsSeconds=31536000"
- "traefik.http.middlewares.vaultwarden-headers.headers.stsIncludeSubdomains=true"
- "traefik.http.middlewares.vaultwarden-headers.headers.stsPreload=true"
- "traefik.http.middlewares.vaultwarden-headers.headers.forceSTSHeader=true"
- "traefik.http.middlewares.vaultwarden-headers.headers.contentTypeNosniff=true"
- "traefik.http.middlewares.vaultwarden-headers.headers.browserXssFilter=true"
networks:
proxy:
external: false
|
Internal-Only (LAN) Deployment with Self-Signed Cert
If you only want Vaultwarden accessible on your home network, use Traefik with a self-signed certificate and add the CA to your devices:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
# Traefik config for LAN-only with self-signed cert
command:
- "--providers.docker=true"
- "--entrypoints.websecure.address=:443"
# Use a local cert instead of ACME
- "--providers.file.filename=/traefik/tls.yml"
# tls.yml
tls:
certificates:
- certFile: /certs/vault.crt
keyFile: /certs/vault.key
stores:
default:
defaultCertificate:
certFile: /certs/vault.crt
keyFile: /certs/vault.key
|
Or use Caddy instead of Traefik for simpler TLS handling:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
caddy:
image: caddy:alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
networks:
- proxy
# Caddyfile
vault.yourdomain.com {
reverse_proxy vaultwarden:80
reverse_proxy /notifications/hub vaultwarden:3012
encode gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
}
}
|
First-Run Setup
1
2
3
4
|
docker compose up -d
# Check logs
docker compose logs -f vaultwarden
|
Initial steps:
- Navigate to
https://vault.yourdomain.com — you’ll see the Bitwarden web vault
- Click Create Account to register your admin user (registration is open until you add users, then you can disable it)
- After creating your account, go to
https://vault.yourdomain.com/admin and log in with your ADMIN_TOKEN
Admin Panel Hardening
In the admin panel (/admin), configure:
General Settings:
✓ Allow invitations: true (invite users individually)
✗ Allow signups: false (after creating your accounts)
✗ Allow password hints: false
✓ Require email verification for signups: true
Advanced Settings:
✓ Enable websocket notifications: true
✓ Domain URL: https://vault.yourdomain.com (must match exactly)
Restrict the admin panel to your IP (highly recommended) via Traefik middleware:
1
2
3
4
|
# In docker-compose.yml labels for vaultwarden:
- "traefik.http.routers.vaultwarden-admin.rule=Host(`${DOMAIN}`) && PathPrefix(`/admin`)"
- "traefik.http.routers.vaultwarden-admin.middlewares=admin-ipwhitelist"
- "traefik.http.middlewares.admin-ipwhitelist.ipwhitelist.sourcerange=192.168.1.0/24,127.0.0.1/32"
|
Or completely block /admin from the internet using a separate Traefik router that only matches internal IPs.
Automated Backups
Your password vault backup is one of the most critical backups you have. Losing it means losing access to every account.
What to Back Up
Vaultwarden stores everything in /data:
db.sqlite3 — the vault database (all passwords, TOTP secrets, notes)
attachments/ — file attachments stored in vault items
sends/ — Bitwarden Send files
config.json — server configuration
rsa_key.* — RSA keys used for encryption (critical — losing these makes the database unreadable)
Backup Script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
#!/bin/bash
# /opt/vaultwarden/backup.sh
set -euo pipefail
VAULTWARDEN_DATA="/opt/vaultwarden/data"
BACKUP_DIR="/opt/vaultwarden/backups"
BACKUP_RETENTION_DAYS=30
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
BACKUP_FILE="${BACKUP_DIR}/vaultwarden_${TIMESTAMP}.tar.gz.gpg"
GPG_RECIPIENT="your-gpg-key-id" # or use symmetric: GPG_PASSPHRASE
mkdir -p "${BACKUP_DIR}"
# SQLite backup — use the official backup command to avoid corruption
# from a live database
sqlite3 "${VAULTWARDEN_DATA}/db.sqlite3" ".backup '${BACKUP_DIR}/db_${TIMESTAMP}.sqlite3'"
# Create encrypted archive
tar -czf - \
-C "${VAULTWARDEN_DATA}" \
attachments/ \
sends/ \
config.json \
rsa_key.pem \
rsa_key.pub.pem \
"${BACKUP_DIR}/db_${TIMESTAMP}.sqlite3" \
| gpg --batch --yes \
--recipient "${GPG_RECIPIENT}" \
--encrypt \
--output "${BACKUP_FILE}"
# Clean up temp SQLite backup
rm -f "${BACKUP_DIR}/db_${TIMESTAMP}.sqlite3"
# Remove backups older than retention period
find "${BACKUP_DIR}" -name "vaultwarden_*.tar.gz.gpg" -mtime +${BACKUP_RETENTION_DAYS} -delete
echo "Backup complete: ${BACKUP_FILE} ($(du -sh "${BACKUP_FILE}" | cut -f1))"
|
For symmetric encryption (simpler, no GPG key management):
1
2
3
4
5
6
|
# Replace the gpg command with:
| gpg --batch --yes \
--passphrase "${BACKUP_PASSPHRASE}" \
--symmetric \
--cipher-algo AES256 \
--output "${BACKUP_FILE}"
|
Offsite Upload with rclone
1
2
3
4
5
6
7
8
|
# Install rclone and configure a remote (Backblaze B2, S3, Wasabi, etc.)
rclone config # follow interactive setup
# Add to backup script after creating the archive:
rclone copy "${BACKUP_FILE}" "b2:your-bucket/vaultwarden-backups/"
# Verify the upload
rclone ls "b2:your-bucket/vaultwarden-backups/" | tail -5
|
Cron Schedule
1
2
3
4
5
6
|
# /etc/cron.d/vaultwarden-backup
# Run at 2 AM daily
0 2 * * * root /opt/vaultwarden/backup.sh >> /var/log/vaultwarden-backup.log 2>&1
# Or weekly full + daily incremental with restic:
# 0 2 * * * root restic -r b2:your-bucket/vaultwarden backup /opt/vaultwarden/data
|
Backup as a Docker Sidecar
Keep the backup container co-located with Vaultwarden:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
# Add to docker-compose.yml
vaultwarden-backup:
image: bruceforce/vaultwarden-backup:latest
container_name: vaultwarden-backup
restart: unless-stopped
depends_on:
- vaultwarden
environment:
BACKUP_DIR: /backups
BACKUP_FILE_SUFFIX: "_%Y%m%d"
CRON_TIME: "0 2 * * *"
DELETE_AFTER: 30
RCLONE_REMOTE_NAME: "b2"
RCLONE_REMOTE_DIR: "your-bucket/vaultwarden"
TIMESTAMP: "true"
volumes:
- ./data:/data:ro # Vaultwarden data (read-only)
- ./backups:/backups # Local backup staging
- ./rclone.conf:/config/rclone/rclone.conf:ro
|
Testing Restores
A backup you’ve never tested is not a backup:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
#!/bin/bash
# test-restore.sh — run monthly to verify backups are recoverable
BACKUP_FILE="$1"
RESTORE_DIR="/tmp/vaultwarden-restore-test"
mkdir -p "${RESTORE_DIR}"
# Decrypt and extract
gpg --batch --decrypt "${BACKUP_FILE}" | \
tar -xzf - -C "${RESTORE_DIR}"
# Verify SQLite integrity
sqlite3 "${RESTORE_DIR}/db.sqlite3" "PRAGMA integrity_check;"
# Check RSA keys exist
if [[ -f "${RESTORE_DIR}/rsa_key.pem" && -f "${RESTORE_DIR}/rsa_key.pub.pem" ]]; then
echo "✓ RSA keys present"
else
echo "✗ RSA keys MISSING — backup is incomplete!"
exit 1
fi
# Check database has expected tables
TABLES=$(sqlite3 "${RESTORE_DIR}/db.sqlite3" ".tables")
for table in users ciphers folders collections org_policies; do
if echo "$TABLES" | grep -q "$table"; then
echo "✓ Table '$table' present"
else
echo "✗ Table '$table' MISSING"
exit 1
fi
done
# Count records
USER_COUNT=$(sqlite3 "${RESTORE_DIR}/db.sqlite3" "SELECT COUNT(*) FROM users;")
CIPHER_COUNT=$(sqlite3 "${RESTORE_DIR}/db.sqlite3" "SELECT COUNT(*) FROM ciphers;")
echo "✓ Restore verified: ${USER_COUNT} users, ${CIPHER_COUNT} vault items"
rm -rf "${RESTORE_DIR}"
|
PostgreSQL Backend (Optional)
For multi-user deployments or if you want ACID-compliant backups via pg_dump:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
services:
postgres:
image: postgres:16-alpine
container_name: vaultwarden-db
restart: unless-stopped
environment:
POSTGRES_USER: vaultwarden
POSTGRES_PASSWORD: "${DB_PASSWORD}"
POSTGRES_DB: vaultwarden
volumes:
- ./postgres:/var/lib/postgresql/data
networks:
- proxy
vaultwarden:
image: vaultwarden/server:latest
environment:
DATABASE_URL: "postgresql://vaultwarden:${DB_PASSWORD}@postgres/vaultwarden"
# ... other env vars
depends_on:
- postgres
|
With PostgreSQL, backups become:
1
2
3
|
# Dump the database
docker exec vaultwarden-db pg_dump -U vaultwarden vaultwarden \
| gzip | gpg --encrypt --recipient "${GPG_KEY}" > vault_db_$(date +%Y%m%d).sql.gz.gpg
|
Two-Factor Authentication
Always enable 2FA on your Vaultwarden account — your password manager is the skeleton key to everything else.
Vaultwarden supports:
- TOTP (Authenticator apps — Aegis, Bitwarden Authenticator, Authy)
- WebAuthn / FIDO2 (hardware security keys — YubiKey, etc.)
- Email 2FA (requires SMTP configured)
- Duo (requires Duo account)
- YubiKey OTP (requires YubiCloud API key)
To enable in the web vault:
- Profile → Account Settings → Two-step Login
- Choose your preferred method — TOTP is the best balance of security and usability
- Save your recovery code somewhere completely separate from your vault (physical paper, separate encrypted drive)
Enforcing 2FA for an Organization
In the Vaultwarden admin panel or via the Organizations feature:
Organization → Settings → Policies → Require Two-step Login: Enabled
Migrating from Other Password Managers
From LastPass
- In LastPass: Account Options → Advanced → Export → LastPass CSV File
- In Bitwarden web vault (connected to your Vaultwarden): Tools → Import Data
- Select “LastPass (csv)” as format
- Upload the file
After importing:
- Review items marked with
[LastPass] tags
- Delete the CSV export from your computer immediately
- Check for items with empty usernames or passwords (common LastPass export quirk)
- Review form fills — LastPass form fill data doesn’t map cleanly to Bitwarden
From 1Password
- In 1Password: File → Export → All Items → 1PIF format (1Password Interchange Format)
- Or export as CSV for individual vaults
- In Bitwarden web vault: Tools → Import → “1Password (1pif)” or “1Password (csv)”
Note on 1Password export format:
1Password’s 1PIF format preserves more data (custom fields, tags, URLs) than CSV. If you have complex vault items with custom fields, use 1PIF. The CSV export loses custom fields.
1
2
|
# If you have many vaults to export, 1Password CLI makes it easier:
op export --format 1pif > export.1pif
|
From KeePass/KeePassXC
KeePassXC → Database → Export → CSV
Then import as “KeePass 2 (xml)” or “KeePassX (csv)” in Bitwarden.
Better approach — export as KeePass XML which preserves the folder hierarchy:
KeePassXC → File → Export Database → KeePass 2 XML (*.xml)
From Dashlane
- Dashlane → My Account → Export Data → Export to CSV
- Import into Bitwarden as “Dashlane (csv)”
Clean up after any migration:
1
2
3
4
|
# Immediately shred the export file after import
shred -vzu -n 3 ~/Downloads/vault_export.csv
# Or on macOS:
srm -vz ~/Downloads/vault_export.csv
|
Client Setup
Vaultwarden is compatible with all official Bitwarden clients. After installing, point them at your server:
Browser Extension
- Install the Bitwarden extension (Chrome, Firefox, Safari, Edge)
- Click the extension → Log in → Self-hosted
- Server URL:
https://vault.yourdomain.com
- Log in with your credentials
Desktop App
- Download from bitwarden.com
- On the login screen, click the region selector → Self-hosted
- Server URL:
https://vault.yourdomain.com
Mobile App (iOS/Android)
- Download Bitwarden from the App Store / Google Play
- Tap the region selector on the login screen → Self-hosted
- Server URL:
https://vault.yourdomain.com
Bitwarden CLI
1
2
3
4
5
6
7
8
9
10
11
12
13
|
# Install
npm install -g @bitwarden/cli
# Point at your server
bw config server https://vault.yourdomain.com
# Log in
bw login your@email.com
# Use it
bw list items | jq '.[].name'
bw get password "My Bank"
bw generate -uln --length 32 # generate a strong password
|
Organizations and Sharing
Vaultwarden supports Bitwarden Organizations for sharing credentials within a household or team.
Web Vault → New Organisation → "Family" (or "Team")
→ Invite family members by email
→ Create Collections: "Shared Streaming", "Home", "Financial"
→ Assign items to collections
→ Set member permissions (View/Edit/Manage per collection)
Organization vaults are end-to-end encrypted — Vaultwarden never sees the plaintext. The encryption key is shared via asymmetric encryption using each member’s RSA key.
Practical household setup:
- Personal vault: private credentials only you know (work accounts, personal finance)
- Family organisation: shared streaming passwords, smart home credentials, emergency contacts, home Wi-Fi passwords, router admin credentials
Hardening Checklist
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
## Vaultwarden Security Checklist
### Network
- [ ] HTTPS enforced — no HTTP access to vault
- [ ] HSTS header with preload
- [ ] Admin panel restricted to LAN IP range
- [ ] Fail2ban or Traefik rate limiting on login endpoint
- [ ] Port 80 redirects to 443
### Authentication
- [ ] Open registration disabled (SIGNUPS_ALLOWED=false)
- [ ] Admin token is a cryptographically random 48+ char string
- [ ] 2FA enabled on all accounts
- [ ] Emergency access set up (trusted contact for account recovery)
### Data
- [ ] Daily encrypted backups verified working
- [ ] Offsite backup copy (Backblaze B2, S3, etc.)
- [ ] Backup restore tested in the last 90 days
- [ ] RSA keys included in backup
### Monitoring
- [ ] Alerts on backup job failure
- [ ] Alerts on Vaultwarden container restart
- [ ] Log rotation configured
|
Rate Limiting with Traefik
1
2
3
4
5
|
# Add to Traefik middlewares in docker-compose.yml
- "traefik.http.middlewares.vaultwarden-ratelimit.ratelimit.average=10"
- "traefik.http.middlewares.vaultwarden-ratelimit.ratelimit.burst=20"
- "traefik.http.middlewares.vaultwarden-ratelimit.ratelimit.period=1m"
- "traefik.http.routers.vaultwarden.middlewares=vaultwarden-headers,vaultwarden-ratelimit"
|
Fail2ban for Login Brute Force
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
# /etc/fail2ban/filter.d/vaultwarden.conf
[Definition]
failregex = ^.*Username or password is incorrect\. Try again\. IP: <ADDR>.*$
^.*error.*Invalid username or password.*<ADDR>.*$
ignoreregex =
# /etc/fail2ban/jail.d/vaultwarden.conf
[vaultwarden]
enabled = true
port = 80,443
filter = vaultwarden
logpath = /opt/vaultwarden/data/vaultwarden.log
maxretry = 5
bantime = 3600
findtime = 600
|
Updating Vaultwarden
1
2
3
4
5
6
7
8
9
10
11
|
# Pull latest image
docker compose pull vaultwarden
# Check release notes at github.com/dani-garcia/vaultwarden/releases
# Look for any database migration notes
# Restart with new image (SQLite migrations run automatically on startup)
docker compose up -d vaultwarden
# Verify it started cleanly
docker compose logs --tail=20 vaultwarden
|
Before updating: take a manual backup:
1
|
sqlite3 data/db.sqlite3 ".backup 'data/db_pre_update.sqlite3'"
|
Monitoring
1
2
3
4
5
6
7
8
|
# Add a healthcheck to docker-compose.yml
vaultwarden:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:80/alive"]
interval: 30s
timeout: 10s
retries: 3
start_period: 15s
|
Watch for these log patterns:
1
2
3
4
5
6
7
8
|
# Real-time log monitoring
docker compose logs -f vaultwarden | grep -E "ERROR|WARN|failed"
# Count failed login attempts (last hour)
docker compose logs vaultwarden --since 1h | grep -c "Username or password is incorrect"
# Track who's logging in
docker compose logs vaultwarden --since 24h | grep "Logged in user"
|
Disaster Recovery
Document your recovery process before you need it:
1
2
3
4
5
6
7
8
9
|
## Vaultwarden Recovery Runbook
### Scenario: Server dies, need to restore on new host
1. Provision new server with Docker installed
2. Clone deployment repo: `git clone ...`
3. Restore backup:
```bash
gpg --decrypt vaultwarden_YYYYMMDD_HHMMSS.tar.gz.gpg | tar -xzf - -C /opt/vaultwarden/data/
|
- Update DNS to point vault.yourdomain.com at new server IP
docker compose up -d
- Verify:
curl -f https://vault.yourdomain.com/alive
- Log in and confirm vault items are present
Scenario: Corrupted database
- Stop Vaultwarden:
docker compose stop vaultwarden
- Check integrity:
sqlite3 data/db.sqlite3 "PRAGMA integrity_check;"
- If corrupted, restore from last known good backup
- If partially recoverable:
sqlite3 data/db.sqlite3 ".recover" | sqlite3 data/db_recovered.sqlite3
- Restart:
docker compose start vaultwarden
Scenario: Lost admin token
- Stop Vaultwarden
- Remove ADMIN_TOKEN from environment (or set to empty string)
- Restart — admin panel will be disabled entirely
- If you need admin access, set a new token and restart
---
## Why Self-Host vs Cloud Bitwarden?
The honest answer: for most people, the paid cloud Bitwarden ($10/year) is the right choice. It has:
- Redundant infrastructure
- Professional security team
- Emergency access features
- No operational overhead
Self-hosting with Vaultwarden makes sense when:
- You want zero third-party dependency for credentials
- You're already running a homelab and the operational overhead is marginal
- You want to learn about self-hosting in a low-stakes environment
- Your organization has compliance requirements about where credential data lives
- You want features from the premium tier without the subscription
The trade-off is real: you are now responsible for availability, backups, and updates. A vault that goes down when you're trying to log into your bank account at the airport is worse than a cloud service. Take the backup and monitoring sections of this guide seriously.
---
*Related: [Securing the Home Lab](/posts/securing-home-lab/), [Secrets Management](/posts/secrets-management/), [Traefik Complete Guide](/posts/traefik-complete-guide/), [Backup Strategy](/posts/backup-strategy/)*
Comments