Password managers are non-negotiable for modern teams, but hosted solutions often come with per-seat pricing, data residency concerns, and limited control over encryption policies. For a growing agency, a misconfigured SaaS account can leak every client credential at once. For a product team, export restrictions and compliance audits make cloud vaults a legal bottleneck. Vaultwarden is an unofficial, lightweight Rust implementation of the Bitwarden server API that is compatible with the official Bitwarden clients. It lets teams run a private password vault on their own infrastructure, manage unlimited users, and enforce organization-wide policies without recurring SaaS fees.
In this guide, we will deploy Vaultwarden on a single Ubuntu host with Docker Compose, publish it through Caddy for automatic HTTPS, enable the admin panel for user management, configure SMTP for email invites, and set up encrypted backups. The result is a production-ready password manager that your team can access from browsers, mobile apps, and browser extensions.
Architecture and flow overview
The deployment uses two containers: Vaultwarden and Caddy. Vaultwarden exposes an HTTP API on an internal port. Caddy terminates TLS, handles certificate renewal, and proxies requests to Vaultwarden. Persistent data lives in a Docker volume mapped to the host filesystem. A separate backup container or cron job snapshots the data directory to an off-site target. SMTP configuration allows Vaultwarden to send invitation emails, verification codes, and breach alerts.
When a user opens the Bitwarden browser extension, the request flows through Caddy on port 443, is decrypted, and forwarded to the Vaultwarden container on the internal Docker network. Vaultwarden verifies credentials against its local SQLite database, returns the encrypted vault payload, and the client decrypts it locally with the user master password. Attachments and RSA keys are stored in the mapped ./data directory. Caddy also manages ACME challenges and automatically renews the TLS certificate before expiry.
- Caddy handles public HTTPS on port 443 and auto-redirects HTTP to HTTPS.
- Vaultwarden runs the Rust server with SQLite, WebSocket support, and the admin panel.
- Docker volumes persist the SQLite database, attachments, and RSA keys across restarts.
- Host cron stops the container briefly to take a consistent filesystem backup.
Prerequisites
- Ubuntu 22.04 or 24.04 LTS with SSH access and sudo privileges.
- A DNS A record pointing
vault.yourdomain.comto the server public IP. - Docker Engine 24.x+ and Docker Compose plugin installed.
- Ports 80 and 443 open to Caddy for ACME challenges and HTTPS traffic.
- At least 2 GB RAM and 10 GB disk for attachments, logs, and backups.
- An SMTP provider account (SendGrid, Mailgun, AWS SES, or your own relay) for email delivery.
Step-by-step deployment
1. Create the project directory
Create a dedicated directory for Vaultwarden and set ownership so the current user can manage files without root:
sudo mkdir -p /opt/vaultwarden/{data,backups}
sudo chown -R $USER:$USER /opt/vaultwarden
cd /opt/vaultwarden
2. Write the Docker Compose file
The Compose file defines two services on a shared bridge network. Vaultwarden loads environment variables from an external .env file and mounts the local data directory into the container. Caddy mounts the Caddyfile and its persistent certificate storage:
cat > docker-compose.yml << 'EOF'
services:
vaultwarden:
image: vaultwarden/server:latest
container_name: vaultwarden
restart: unless-stopped
env_file: .env
volumes:
- ./data:/data
networks:
- vaultnet
caddy:
image: caddy:2-alpine
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
networks:
- vaultnet
volumes:
caddy_data:
caddy_config:
networks:
vaultnet:
driver: bridge
EOF
3. Configure the Caddyfile
Caddy needs the upstream headers so Vaultwarden can build correct redirect URLs and log real client IPs. The header_up directives pass the original Host, remote address, and protocol scheme:
cat > Caddyfile << 'EOF'
vault.yourdomain.com {
reverse_proxy vaultwarden:80 {
header_up Host {host}
header_up X-Real-IP {remote}
header_up X-Forwarded-For {remote}
header_up X-Forwarded-Proto {scheme}
}
}
EOF
4. Create the environment file
Generate a strong admin token, configure SMTP settings, and disable open registration so only invited users can join:
cat > .env << 'EOF'
WEBSOCKET_ENABLED=true
ADMIN_TOKEN=$(openssl rand -base64 48)
SMTP_HOST=smtp.sendgrid.net
[email protected]
SMTP_PORT=587
SMTP_SECURITY=starttls
SMTP_USERNAME=apikey
SMTP_PASSWORD=SG.xxx
SIGNUPS_ALLOWED=false
SIGNUPS_VERIFY=true
INVITATIONS_ALLOWED=true
EOF
5. Start the services
Pull images and start containers in detached mode. Watch the logs to confirm Vaultwarden initializes the SQLite database and Caddy obtains the TLS certificate:
docker compose up -d
docker compose logs -f vaultwarden
6. Access the admin panel
Navigate to https://vault.yourdomain.com/admin and log in with the ADMIN_TOKEN value from /opt/vaultwarden/.env. From the admin panel you can create organizations, manage users, and view server health.
7. Configure SMTP for email delivery
Update the .env file with your SMTP credentials, then restart Vaultwarden:
docker compose restart vaultwarden
8. Set up automated backups
A daily cron job stops Vaultwarden to ensure a consistent snapshot, archives the data directory, prunes backups older than 30 days, and restarts the container:
cat > /opt/vaultwarden/backup.sh << 'EOF'
#!/bin/bash
set -e
BACKUP_DIR="/opt/vaultwarden/backups"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
mkdir -p "$BACKUP_DIR"
docker compose stop vaultwarden
tar czf "$BACKUP_DIR/vaultwarden_${TIMESTAMP}.tar.gz" -C /opt/vaultwarden data
docker compose start vaultwarden
find "$BACKUP_DIR" -name "*.tar.gz" -mtime +30 -delete
EOF
chmod +x /opt/vaultwarden/backup.sh
Add a cron job to run the backup daily:
(crontab -l 2>/dev/null; echo "0 3 * * * /opt/vaultwarden/backup.sh >> /var/log/vaultwarden-backup.log 2>&1") | crontab -
Configuration and secrets handling
Never commit the .env file to version control. Store it in a secrets manager or encrypted backup. The ADMIN_TOKEN is effectively a root credential for Vaultwarden; rotate it quarterly and restrict admin panel access to your internal IP range if possible. For SMTP, use an API key with send-only permissions rather than your primary mail password. Backup archives should be encrypted at rest if they are copied to object storage.
Consider running a separate Caddy instance or reverse proxy in front of the admin panel to add IP allowlisting. If you expose Vaultwarden to the public internet, keep the admin path protected and monitor access logs for brute-force attempts against the token. You can also mount an additional config.json inside the data directory to enforce organization policies such as minimum password length and mandatory two-factor authentication.
Verification
- Run
curl -I https://vault.yourdomain.com/aliveand confirm HTTP 200. - Create a test account through the web vault and save a login item.
- Install the Bitwarden browser extension, point it to your self-hosted URL, and sync the test item.
- Log into the admin panel and verify the user count and organization list.
- Check
/opt/vaultwarden/backups/for a recent.tar.gzarchive. - Send a test invite to a secondary email address and confirm delivery.
Common issues and fixes
WebSocket errors in browser console: Ensure WEBSOCKET_ENABLED=true is set and Caddy passes the Upgrade header. Add header_up Connection {>Connection} and header_up Upgrade {>Upgrade} to the Caddy reverse proxy block if live sync fails.
Admin panel returns 404: The admin panel is disabled by default. You must set ADMIN_TOKEN before the first container start.
SMTP invites not sent: Verify port 587 is open outbound, SMTP_SECURITY matches your provider, and the SMTP_FROM address is authorized in your mail service.
Attachment uploads fail: Check Docker volume permissions. The Vaultwarden container runs as uid 82 in the official image; ensure /opt/vaultwarden/data is writable.
SQLite database is locked: This usually happens during a backup while the container is running. Always stop the container before archiving the data directory.
Certificate renewal fails behind a firewall: Caddy needs outbound HTTPS to the Let's Encrypt or ZeroSSL ACME servers. Whitelist acme-v02.api.letsencrypt.org and the relevant OCSP responders.
FAQ
Is Vaultwarden compatible with official Bitwarden clients?
Yes. Vaultwarden implements the Bitwarden server API, so the web vault, browser extensions, desktop apps, and mobile apps connect without modification. You only need to change the server URL in client settings.
Can I use PostgreSQL or MySQL instead of SQLite?
Vaultwarden supports PostgreSQL and MySQL through environment variables, but SQLite is the default and performs well for teams under a few hundred users. If you need high availability or replicated backups, PostgreSQL is the better choice.
How do I migrate from Bitwarden cloud to self-hosted?
Export your personal vault as a JSON or CSV file from the Bitwarden web vault, then import it into your Vaultwarden instance through the web UI. Organization collections must be exported separately by an organization admin.
Does Vaultwarden support two-factor authentication?
Yes. Users can enable TOTP, WebAuthn/FIDO2, and Duo MFA on their accounts. Organization policies can also require MFA for all members.
What happens if I lose the ADMIN_TOKEN?
You cannot recover the admin panel without the token. Generate a new one with openssl rand -base64 48, update the .env file, and restart the container. Existing users and vault data remain intact.
How do I update Vaultwarden?
Pull the latest image, recreate the container, and verify the version in the admin panel. Because the data directory is a persistent volume, the update is non-destructive.
docker compose pull vaultwarden
docker compose up -d vaultwarden
Can I restrict signups to invited users only?
Yes. Set SIGNUPS_ALLOWED=false and INVITATIONS_ALLOWED=true. Only organization owners and admins can send invitations through the admin panel or web vault.
Internal links
- Production Guide: Deploy Passbolt with Docker Compose + Caddy + MariaDB + GnuPG on Ubuntu
- Production Guide: Deploy Infisical with Docker Compose + Caddy + PostgreSQL + Redis on Ubuntu
- Production Guide: Deploy Nextcloud with Docker Compose + NGINX + MariaDB + Redis on Ubuntu
Talk to us
If you want this implemented with hardened defaults, observability, and tested recovery playbooks, our team can help.
Header image: Original SysBrix generated header, no watermark.