Self-host Vaultwarden (Bitwarden-compatible) on a VPS and pay in Bitcoin or Monero

· 7 min read

Vaultwarden on an Ubuntu 24.04 VPS with Docker and Caddy: Argon2 admin token, Let’s Encrypt, fail2ban, SQLite backups off-box, and which small plan fits.

Why run your own password server, and why here

Vaultwarden is a small Rust reimplementation of the Bitwarden server API. Every official Bitwarden client (browser extensions, desktop, iOS, Android, CLI) talks to it unchanged, so you keep the client experience and lose the dependency on a third-party vault host. The vault itself is end-to-end encrypted by the clients; the server only ever stores ciphertext. What the server does hold in the clear is the metadata: which email addresses have accounts, when they sync, and from which IP addresses.

That metadata is the argument for putting it on a VPS that is not tied to your identity. A server bought without an account, paid in Monero or Bitcoin, and reached through your own domain means the vault host is not a line in someone’s customer database next to your name. It does not change the cryptography; it changes who can be asked about you. The server also needs to be up when you need a password, which is the other thing a VPS gives you that a box under the desk does not.

This guide uses Docker for Vaultwarden and Caddy on the host as the TLS terminator, because Caddy handles Let’s Encrypt issuance and renewal with two lines of config. nginx with certbot works equally well; the Vaultwarden wiki has proxy examples for both.

Sizing: the smallest plan is more than enough

Vaultwarden idles at well under 100 MB of RAM and its SQLite database for a household or small team is measured in megabytes. CPU is only touched during login (Argon2 or PBKDF2 key stretching happens client-side, but the server verifies a hash) and during the occasional attachment upload. Disk grows with attachments and Send files, which you can cap in the admin panel.

  • BVCX23 (2 vCPU, 4 GB RAM, 40 GB NVMe, from $11/mo in Germany or Finland) is the comfortable default. The extra RAM leaves room for Caddy, fail2ban and the occasional backup job without swap.
  • BVX11 (2 vCPU, 2 GB RAM, 40 GB NVMe, from $10/mo in Europe, $19 in Singapore, $32 in the US) is the pick if you need a US or Singapore address for latency. 2 GB is fine for this stack.
  • BVA11 (Arm, 2 vCPU, 4 GB, 40 GB, $12/mo, EU only) also works; the vaultwarden/server image is published for arm64.

Prepare the server

Everything below assumes a fresh Ubuntu 24.04 image and a root shell (prefix sudo if you created a user). Patch first, then lock down SSH and the firewall before anything else is listening.

apt update && apt full-upgrade -y
apt install -y ufw unattended-upgrades curl gnupg

# SSH: keys only. Put your public key in /root/.ssh/authorized_keys first and
# confirm you can log in with it before disabling passwords.
cat > /etc/ssh/sshd_config.d/00-keys-only.conf <<'EOF'
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin prohibit-password
EOF
sshd -t && systemctl restart ssh

# Firewall: deny everything inbound except SSH for now.
ufw default deny incoming
ufw default allow outgoing
ufw allow OpenSSH
ufw --force enable

Install Docker and Caddy

Point a DNS A record (and AAAA, the server has IPv6) for vault.example.com at the server before you continue; Caddy needs it resolvable to obtain a certificate.

# Docker Engine + Compose plugin from Docker's own repository (not the Ubuntu snap).
apt install -y ca-certificates curl
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
  > /etc/apt/sources.list.d/docker.list
apt update
apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
docker version && docker compose version
# Caddy from the project's apt repository. It obtains and renews Let's Encrypt
# certificates on its own, so there is no certbot to schedule.
apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
apt update && apt install -y caddy

ufw allow 80/tcp
ufw allow 443/tcp

Admin token, compose file, first start

The /admin page lets you manage users, invitations and server settings. It is protected by ADMIN_TOKEN, which should be stored as an Argon2id hash rather than plaintext. Vaultwarden ships a hashing command; run it once in a throwaway container and keep the password you typed in your (old) password manager for now.

docker run --rm -it vaultwarden/server:1.37.4 /vaultwarden hash --preset owasp
# Asks for a password twice, then prints a line like:
# ADMIN_TOKEN='$argon2id$v=19$m=19456,t=2,p=1$....$....'
# Copy everything inside the quotes.

Compose treats a single dollar sign as variable interpolation, so every $ in the hash must be doubled when it goes into the compose file. The log file is enabled now because fail2ban reads it later.

# /opt/vaultwarden/compose.yaml
services:
  vaultwarden:
    image: vaultwarden/server:1.37.4   # check the releases page; pin, do not use :latest
    container_name: vaultwarden
    restart: unless-stopped
    ports:
      - "127.0.0.1:8080:80"
    volumes:
      - ./data:/data
    environment:
      DOMAIN: "https://vault.example.com"
      SIGNUPS_ALLOWED: "true"           # set to "false" after you create your account
      ADMIN_TOKEN: "$$argon2id$$v=19$$m=19456,t=2,p=1$$PASTE$$THE_REST"
      LOG_FILE: "/data/vaultwarden.log"
      LOG_LEVEL: "warn"
      SHOW_PASSWORD_HINT: "false"
      SENDS_ALLOWED: "true"
      ATTACHMENT_LIMIT: "100000"         # KB per user, optional cap
mkdir -p /opt/vaultwarden/data && cd /opt/vaultwarden
docker compose up -d
docker compose logs -f   # wait for "Rocket has launched", then Ctrl-C

Now the Caddyfile. The X-Real-IP header is what makes Vaultwarden log the visitor’s address instead of 127.0.0.1, which fail2ban depends on. WebSocket notifications run over the same port since Vaultwarden 1.29, so no separate upgrade block is needed.

# /etc/caddy/Caddyfile
vault.example.com {
    encode zstd gzip
    header {
        Strict-Transport-Security "max-age=31536000;"
        X-Content-Type-Options "nosniff"
        Referrer-Policy "same-origin"
        -Server
    }
    reverse_proxy 127.0.0.1:8080 {
        header_up X-Real-IP {remote_host}
    }
}
caddy validate --config /etc/caddy/Caddyfile
systemctl reload caddy
journalctl -u caddy --since "2 min ago" | grep -i -E 'certificate|error'

Open https://vault.example.com, create your account, then set SIGNUPS_ALLOWED to "false" in the compose file and run docker compose up -d again. From now on add people by invitation from /admin. Log into /admin with the password you typed into the hash command, not the hash.

fail2ban for the web vault and the admin page

Bitwarden clients retry on bad passwords and attackers do too. Vaultwarden logs failed logins with the client IP in a fixed format, so two small fail2ban filters cover the vault and the admin page. The ban goes into the host firewall, which is correct here because traffic reaches Vaultwarden through Caddy on the host, not through a published Docker port.

apt install -y fail2ban

cat > /etc/fail2ban/filter.d/vaultwarden.local <<'EOF'
[INCLUDES]
before = common.conf
[Definition]
failregex = ^.*?Username or password is incorrect\. Try again\. IP: <ADDR>\. Username:.*$
ignoreregex =
EOF

cat > /etc/fail2ban/filter.d/vaultwarden-admin.local <<'EOF'
[INCLUDES]
before = common.conf
[Definition]
failregex = ^.*?Invalid admin token\. IP: <ADDR>.*$
ignoreregex =
EOF

cat > /etc/fail2ban/jail.d/vaultwarden.local <<'EOF'
[vaultwarden]
enabled  = true
port     = http,https
filter   = vaultwarden
logpath  = /opt/vaultwarden/data/vaultwarden.log
maxretry = 5
bantime  = 14400
findtime = 14400

[vaultwarden-admin]
enabled  = true
port     = http,https
filter   = vaultwarden-admin
logpath  = /opt/vaultwarden/data/vaultwarden.log
maxretry = 3
bantime  = 14400
findtime = 14400
EOF

systemctl enable --now fail2ban
systemctl restart fail2ban
fail2ban-client status vaultwarden

Test it: fail a login on purpose from your phone’s mobile connection, then run fail2ban-regex /opt/vaultwarden/data/vaultwarden.log /etc/fail2ban/filter.d/vaultwarden.local and confirm it matched. If the log shows 127.0.0.1 as the IP, the X-Real-IP header is not arriving; check the Caddyfile. Also enable 2FA (TOTP or a hardware key via WebAuthn) on every account; DOMAIN must be the exact HTTPS URL for WebAuthn to work.

Backups: the database, the keys and the attachments

Everything Vaultwarden owns lives under /opt/vaultwarden/data. The pieces that matter are db.sqlite3 (users, ciphers, organisations), rsa_key.pem (signs login tokens; losing it just logs everyone out), the attachments and sends directories, and config.json if you changed settings in /admin. Copying db.sqlite3 while it is open can produce a corrupt file, so use SQLite’s own online backup instead of cp.

apt install -y sqlite3
cat > /usr/local/bin/vw-backup <<'EOF'
#!/bin/sh
set -eu
SRC=/opt/vaultwarden/data
DST=/var/backups/vaultwarden
STAMP=$(date +%F-%H%M)
mkdir -p "$DST"
sqlite3 "$SRC/db.sqlite3" ".backup '$DST/db-$STAMP.sqlite3'"
tar czf "$DST/files-$STAMP.tgz" -C "$SRC" attachments sends rsa_key.pem rsa_key.pub.pem config.json 2>/dev/null || true
# keep 14 days locally
find "$DST" -type f -mtime +14 -delete
EOF
chmod +x /usr/local/bin/vw-backup
vw-backup && ls -lh /var/backups/vaultwarden

# Run it nightly.
echo '17 3 * * * root /usr/local/bin/vw-backup' > /etc/cron.d/vaultwarden-backup

A backup on the same disk only protects against your own mistakes. Ship /var/backups/vaultwarden somewhere else: restic or rclone to object storage, a cron rsync to a machine at home, or an encrypted tarball to any host you control. Encrypt before upload (restic does this by default; for rclone use the crypt backend) since the SQLite file contains account emails and the encrypted vault blobs. The automated backups add-on at order time gives you whole-disk restore points as a second layer, but it is not a substitute for a copy that leaves the server.

Restore is the reverse: stop the container, replace db.sqlite3 and the directories, fix ownership, start it. Practise it once on a fresh one-day server; a backup you have not restored is a hope, not a backup.

Updates, monitoring and firewall checklist

  • Vaultwarden: watch the GitHub releases page (1.37.4 as of October 2026 contains security fixes, so do not run anything older). To upgrade, change the tag in compose.yaml, then docker compose pull && docker compose up -d. The database migrates itself on start; take a backup first.
  • Clients: Bitwarden clients occasionally require a minimum server version for new features. If a client starts failing to sync after an app update, upgrading the server usually fixes it.
  • Caddy and the OS: unattended-upgrades covers both once the Caddy repository is added. Certificate renewal is automatic; journalctl -u caddy shows it.
  • Firewall: ufw status should list only OpenSSH, 80/tcp and 443/tcp. Port 8080 must not appear; it is bound to localhost.
  • Health: curl -s https://vault.example.com/alive returns a timestamp. A free uptime monitor hitting that URL every few minutes is enough.
  • Admin panel: once configured, consider removing ADMIN_TOKEN entirely to disable /admin until you need it again. Vaultwarden then refuses admin logins outright.

Which plan to pick

For a personal or family vault choose BVCX23 (2 vCPU, 4 GB, 40 GB, from $11/mo) in Germany or Finland, or BVX11 if you want a US or Singapore location. Pay for a month in Bitcoin or Monero, point vault.example.com at the IP in the access email, and follow the sections above in order: prep, Docker and Caddy, compose, fail2ban, backups. Configure a server, pick Ubuntu 24.04, and the root credentials arrive by magic link once the payment confirms.

Ready to run this yourself?

Linux KVM servers with NVMe storage in the EU, US and Singapore. Root access arrives by magic link; no account, no KYC.

Configure a server — pay with Bitcoin, no account needed