On this page
Introduction
Self‑hosting a password manager used to be a hobby project for the paranoid. In 2026 the landscape has shifted – privacy regulations, rising cloud costs, and the ever‑present threat of credential stuffing make a self‑hosted solution attractive again. Vaultwarden (the lightweight Rust fork of Bitwarden) promises Bitwarden‑compatible features with a fraction of the resources.
In this review I spin up a $3/mo VPS, install Vaultwarden, push it through a realistic workload, lock it down, and finally connect it to Cloudflare Access. The goal is to give sysadmins and small hosting businesses a practical checklist they can copy‑paste.
Why Vaultwarden in 2026?
| Feature | Vaultwarden | Bitwarden Cloud (Free) | Passbolt (Self‑hosted) |
|---|---|---|---|
| Compatibility with official clients | ✅ Full API support | ✅ | ✅ |
| Docker‑first design | ✅ | ❌ | ✅ |
| Resource footprint (RAM) | ~150 MiB (SQLite) | 300 MiB (hosted) | ~300 MiB (MySQL) |
| End‑to‑end encryption | ✅ | ✅ | ✅ |
| Enterprise features (SAML, SCIM) | ❌ (requires plugins) | ✅ | ✅ |
| Cost on a low‑end VPS | <$5/mo | $0 (free tier) | <$10/mo |
Vaultwarden still lacks some enterprise‑grade integrations, but for a team of up to 50 users it’s more than enough. The biggest win is price + control – you own the data, the TLS cert, and the backup schedule.
Picking a Cheap VPS
For the test I chose a 1 vCPU, 1 GiB RAM, 25 GB SSD instance from Vultr ($3/mo). Any provider with a similar “micro” tier works (DigitalOcean, Linode, Hetzner Cloud). The key is to verify:
- IPv6 support – Cloudflare Access prefers it.
- Ability to run Docker (or at least install it).
- Root access for firewall tweaks.
Tip: If you already have a server for other services, adding Vaultwarden as a Docker container costs virtually nothing.
Installation – Docker Compose Method
1. Prepare the host
# Update and install Docker + Docker Compose
sudo apt-get update && sudo apt-get upgrade -y
sudo apt-get install -y ca-certificates curl gnupg lsb-release
# Docker’s official GPG key
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
# Add the repository
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
# Verify
sudo docker run hello-world
2. Create a dedicated user (optional but recommended)
sudo useradd -r -m -d /opt/vaultwarden -s /usr/sbin/nologin vaultwarden
sudo usermod -aG docker vaultwarden
3. Docker‑Compose file
Create /opt/vaultwarden/docker-compose.yml:
version: "3.8"
services:
vaultwarden:
image: vaultwarden/server:latest
container_name: vaultwarden
restart: unless-stopped
environment:
- ROCKET_ENV=production
- WEBSOCKET_ENABLED=true
- SIGNUPS_ALLOWED=false # disable public sign‑ups
- ADMIN_TOKEN=${ADMIN_TOKEN} # generate a strong random token
volumes:
- ./vw-data:/data
ports:
- "127.0.0.1:8080:80" # bind locally, reverse‑proxy will expose TLS
Security note: The
ADMIN_TOKENis stored only in the environment file, not in the image. Create a.envnext to the compose file with a 64‑character base64 string (openssl rand -base64 48).
4. Start the stack
cd /opt/vaultwarden
sudo -u vaultwarden docker compose up -d
You now have a running Vaultwarden instance on port 8080, accessible only from localhost.
Adding TLS – Nginx as a Reverse Proxy
Instead of exposing the container directly, we terminate TLS with Nginx (or Caddy). Below is an Nginx snippet using a Let’s Encrypt certificate via Certbot.
sudo apt-get install -y nginx certbot python3-certbot-nginx
Create /etc/nginx/sites‑available/vaultwarden.conf:
server {
listen 80;
server_name vault.example.com;
# Redirect all HTTP → HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name vault.example.com;
ssl_certificate /etc/letsencrypt/live/vault.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/vault.example.com/privkey.pem;
include /etc/nginx/snippets/ssl‑params.conf;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X‑Real‑IP $remote_addr;
proxy_set_header X‑Forwarded‑For $proxy_add_x_forwarded_for;
proxy_set_header X‑Forwarded‑Proto $scheme;
}
}
Enable and test:
sudo ln -s /etc/nginx/sites-available/vaultwarden.conf /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d vault.example.com
Now the UI is reachable at https://vault.example.com.
Resource Consumption – Real‑World Numbers
After loading the UI and adding ~200 passwords (including a few attachments), the metrics on the VPS looked like this (averaged over 10 minutes):
| Metric | Value |
|---|---|
| RAM (process) | 138 MiB |
| CPU (Docker) | 0.12 cores (average) |
| Disk I/O | 2 MiB/s read, 1 MiB/s write |
| SQLite DB size | 12 MiB |
Even with a modest 1 vCPU the server stayed well below 30 % utilization. Adding Cloudflare Access introduces a tiny extra load (the JWT verification), but the impact is negligible.
Security Hardening Checklist
TL;DR – copy the list, adjust values, and run the commands.
1. Network level
- UFW – allow only 22 (SSH) and 443 (HTTPS).
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw allow 443/tcp
sudo ufw enable
- Fail2Ban – protect SSH and Nginx.
sudo apt-get install -y fail2ban
cat <<'EOF' | sudo tee /etc/fail2ban/jail.local
[sshd]
enabled = true
maxretry = 5
[nginx-http-auth]
enabled = true
filter = nginx-http-auth
logpath = /var/log/nginx/error.log
maxretry = 3
EOF
sudo systemctl restart fail2ban
2. Application level
- Disable public sign‑ups – already set
SIGNUPS_ALLOWED=false. - Enforce strong admin token – rotate every 90 days.
- Use SQLite’s WAL mode (default) for crash safety.
- Set
ROCKET_TLStotrueif you ever run the container without a reverse proxy.
3. Container hygiene
- Run the container as a non‑root user (
PUID/PGID). Vaultwarden’s image respects the host UID/GID when you setPUID=1000. - Keep the image up‑to‑date (
docker compose pull && docker compose up -d).
4. Monitoring
- Prometheus exporter – add the
vaultwarden-exportercontainer to scrape metrics. - Health‑check – Docker already provides a basic HTTP health‑check; you can add a cron that curls
https://vault.example.com/aliveand alerts on failures.
Backup Strategy – Automated, Off‑site, Immutable
Vaultwarden stores everything in the /data directory (SQLite DB, attachments, icons). A simple rsync‑based script combined with a remote S3 bucket (or Wasabi) works well.
1. Install rclone
curl https://rclone.org/install.sh | sudo bash
# Configure a remote called "s3backup"
rclone config
2. Backup script (/opt/vaultwarden/backup.sh)
#!/usr/bin/env bash
set -euo pipefail
BACKUP_DIR="/opt/vaultwarden/vw-data"
TIMESTAMP=$(date -u +"%Y%m%dT%H%M%SZ")
ARCHIVE="/tmp/vaultwarden-${TIMESTAMP}.tar.gz"
# Freeze the DB by stopping the container (quick <2s downtime)
sudo docker stop vaultwarden
# Create a compressed archive
tar -czf "$ARCHIVE" -C "$BACKUP_DIR" .
# Restart the service immediately
sudo docker start vaultwarden
# Upload to remote storage (S3/Wasabi)
rclone copy "$ARCHIVE" s3backup:vaultwarden-backups/ --s3-chunk-size 64M
# Keep only last 30 backups locally
find /tmp -name "vaultwarden-*.tar.gz" -mtime +30 -delete
# Optional: verify upload and send a Slack webhook on success/failure
Make it executable and schedule it:
chmod +x /opt/vaultwarden/backup.sh
# Daily at 02:30 UTC
(crontab -u vaultwarden -l 2>/dev/null; echo "30 2 * * * /opt/vaultwarden/backup.sh") | crontab -u vaultwarden -
3. Disaster recovery test
- Stop the container.
- Delete
/opt/vaultwarden/vw-data. - Pull the latest archive from S3 (
rclone copy s3backup:vaultwarden-backups/<file> /tmp). - Extract and start the container.
- Verify all logins work.
Running the restore took ≈12 seconds, proving the backup method is both fast and reliable.
Cloudflare Access – Zero‑Trust Remote Login
Cloudflare Access (part of Zero Trust) lets you protect the Vaultwarden UI with identity‑aware policies, eliminating the need for a VPN.
1. Set up a Cloudflare Tunnel (formerly Argo Tunnel)
# Install cloudflared
wget -q https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
sudo dpkg -i cloudflared-linux-amd64.deb
# Authenticate with your Cloudflare account
cloudflared tunnel login
# Create a tunnel named "vaultwarden"
cloudflared tunnel create vaultwarden
# Get the tunnel UUID and write a config file
cat <<'EOF' > /etc/cloudflared/config.yml
tunnel: <UUID‑FROM‑CREATE>
credentials-file: /root/.cloudflared/<UUID>.json
ingress:
- hostname: vault.example.com
service: http://127.0.0.1:8080
- service: http_status:404
EOF
# Run as a systemd service
cloudflared service install
sudo systemctl start cloudflared
Now Cloudflare terminates TLS, presents the Access login page, and forwards traffic securely to the local Docker container.
2. Configure an Access Policy
- In the Cloudflare dashboard → Zero Trust → Access → Applications, add a new application with the URL
https://vault.example.com. - Choose an identity provider (Google Workspace, GitHub, or Azure AD).
- Set Rule:
Email ends with @mycompany.com(or any rule you need). - Enable Session Duration (e.g., 8 hours) and Device Posture checks if desired.
Once saved, any user will be prompted to authenticate via the IdP before reaching Vaultwarden. The original password manager credentials remain unchanged; Cloudflare only protects the HTTP layer.
Real‑World Performance Notes
- Latency: With Cloudflare’s edge caching the TLS handshake is ~30 ms from Europe, ~70 ms from North America.
- Concurrent users: I simulated 30 simultaneous logins with
hey. The 95th‑percentile response time stayed under 200 ms, well within the acceptable range for a password manager. - Attachment uploads: The biggest bottleneck is the VPS’s outbound bandwidth (≈100 Mbps). Uploading a 5 MiB file took ~0.4 s – acceptable for internal teams.
- CPU spikes: The only noticeable spike occurs during a DB vacuum (
VACUUM;) which I schedule weekly during off‑peak hours.
Overall, the cheap VPS handled a realistic load with plenty of headroom for growth up to ~200 users.
FAQ
1. Can I use PostgreSQL instead of SQLite?
Vaultwarden supports PostgreSQL, but the SQLite build is the default and consumes far less RAM. If you already run Postgres for other services, you can switch by setting DATABASE_URL=postgres://user:pass@host/db in the environment.
2. Do I still need a separate firewall if I use Cloudflare Access?
Yes. Cloudflare only protects the HTTP layer; a firewall still blocks brute‑force attempts on SSH and other ports. Keep ufw or iptables rules tight.
3. How often should I rotate the admin token?
Treat it like a root password – rotate every 60‑90 days and store it in a password manager with audit logging.
4. Is there a way to enable two‑factor authentication (2FA) for users?
Vaultwarden supports TOTP (Google Authenticator, Authy). Users can enable it from the web UI under Two‑step login.
5. What happens to my data if the VPS provider goes down?
That’s why the off‑site backup to S3/Wasabi is critical. With daily snapshots you can restore to a new VPS within minutes.
Conclusion
Vaultwarden continues to prove that a robust, Bitwarden‑compatible password manager can run on the cheapest cloud instances without sacrificing security. By following the installation steps, applying the hardening checklist, automating backups, and optionally wrapping the UI with Cloudflare Access, you get a production‑grade solution for under $5 a month.
If you’re looking for a concrete, battle‑tested guide, the scripts and configuration snippets above are ready to drop into your environment. Give it a try, tweak the policies to your team’s needs, and enjoy full control over your credentials.
— Written by the team at mahbuburriad.com