HOWTOIntermediate

Deploying Vaultwarden: A Self-Hosted Password Manager

Stand up a lightweight, Bitwarden-compatible password vault on your own infrastructure — with Docker, TLS via reverse proxy, and an automated backup routine.

Dylan H.

Tutorials

August 31, 2026
6 min read
Deploying Vaultwarden: A Self-Hosted Password Manager

Introduction

Password reuse and weak credentials remain the single biggest driver of account compromise for SMBs and homelabs alike. A password manager fixes this, but cloud-hosted vaults put your entire credential store behind a third party's uptime, pricing, and breach history. Vaultwarden (formerly bitwarden_rs) is a lightweight, Rust-based server implementation of the Bitwarden API — it's a drop-in replacement that works with the official Bitwarden browser extensions, mobile apps, and CLI, but runs on hardware you control, using a fraction of the resources of Bitwarden's own self-hosted stack.

In this guide you will:

  • Deploy Vaultwarden with Docker Compose
  • Put it behind a reverse proxy with TLS and security headers
  • Lock down admin access and disable open registration
  • Enable two-factor authentication (TOTP) enforcement
  • Automate encrypted backups of the vault database
  • Verify the deployment and troubleshoot common issues

Prerequisites

Before starting, confirm you have:

  1. A Linux host with Docker Engine and the Compose plugin installed
  2. A DNS record (e.g. vault.example.com) pointed at the host's public or LAN IP
  3. A reverse proxy already terminating TLS for other services (Traefik, Caddy, or nginx + certbot)
  4. At least 512MB of free RAM and 1GB of disk for the vault data volume — Vaultwarden's footprint is small compared to Bitwarden's official Java-based server

Step 1: Generate an Admin Token

Vaultwarden's /admin panel controls users, organizations, and diagnostics. It must be protected with a token — never leave it disabled on an internet-facing instance.

# Generate a strong Argon2 admin token hash
docker run --rm vaultwarden/server:latest /vaultwarden hash

Enter a strong passphrase when prompted. Copy the resulting $argon2id$... hash — you'll paste it into the environment file in Step 2.

Step 2: Create the Docker Compose Stack

Create a project directory and an .env file to keep secrets out of the compose file itself.

mkdir -p /opt/vaultwarden/data
cd /opt/vaultwarden
# .env
DOMAIN=https://vault.example.com
ADMIN_TOKEN='$argon2id$v=19$m=65540,t=3,p=4$...'
SIGNUPS_ALLOWED=false
INVITATIONS_ALLOWED=true
WEBSOCKET_ENABLED=true
# docker-compose.yml
services:
  vaultwarden:
    image: vaultwarden/server:latest
    container_name: vaultwarden
    restart: unless-stopped
    environment:
      DOMAIN: ${DOMAIN}
      ADMIN_TOKEN: ${ADMIN_TOKEN}
      SIGNUPS_ALLOWED: ${SIGNUPS_ALLOWED}
      INVITATIONS_ALLOWED: ${INVITATIONS_ALLOWED}
      WEBSOCKET_ENABLED: ${WEBSOCKET_ENABLED}
      LOG_FILE: /data/vaultwarden.log
    volumes:
      - ./data:/data
    networks:
      - proxy
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.vaultwarden.rule=Host(`vault.example.com`)"
      - "traefik.http.routers.vaultwarden.tls.certresolver=letsencrypt"
      - "traefik.http.services.vaultwarden.loadbalancer.server.port=80"
 
networks:
  proxy:
    external: true

SIGNUPS_ALLOWED=false is critical — it prevents anyone who finds the URL from creating an account. New users are added instead through invitations sent from the admin panel or by a logged-in org admin.

Bring the stack up:

docker compose up -d
docker compose logs -f vaultwarden

Step 3: Harden the Reverse Proxy

If you're using Traefik, add security headers via a middleware so the vault isn't served without HSTS or frame protection:

# traefik dynamic config
http:
  middlewares:
    vaultwarden-headers:
      headers:
        stsSeconds: 31536000
        stsIncludeSubdomains: true
        frameDeny: true
        contentTypeNosniff: true
        browserXssFilter: true

Attach it to the router with an additional label:

labels:
  - "traefik.http.routers.vaultwarden.middlewares=vaultwarden-headers"

For nginx, the equivalent is adding Strict-Transport-Security, X-Frame-Options: DENY, and X-Content-Type-Options: nosniff to the site's server block.

Step 4: Lock Down the Admin Panel

Log in to https://vault.example.com/admin using the plaintext passphrase you hashed in Step 1 (not the hash itself). From here:

  • Under Settings, confirm signups are disabled and set an SMTP relay so invitation and 2FA emails deliver reliably
  • Under Users, review any accounts and revoke ones you don't recognize
  • Consider restricting /admin at the reverse-proxy layer to your LAN or VPN CIDR as a second layer of defense:
# Traefik IP allowlist middleware for /admin
http:
  middlewares:
    admin-allowlist:
      ipAllowList:
        sourceRange:
          - "10.0.0.0/8"
          - "192.168.0.0/16"

Step 5: Enforce Two-Factor Authentication

Vaultwarden supports TOTP, WebAuthn/FIDO2, email codes, and Duo. To require 2FA org-wide, create an organization from a user account, then under Organization Settings → Policies, enable "Require two-step login." Members who haven't configured 2FA will be blocked from vault access until they do.

Individual users enable TOTP from the Bitwarden client under Settings → Security → Two-step Login, scanning the QR code with an authenticator app.

Step 6: Automate Backups

The entire vault lives in /data — primarily db.sqlite3, the attachments/ directory, and rsa_key* files used for JWT signing. Losing the RSA keys invalidates all existing sessions and API keys, so back them up alongside the database.

#!/usr/bin/env bash
# /opt/vaultwarden/backup.sh
set -euo pipefail
 
BACKUP_DIR="/opt/vaultwarden/backups"
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
mkdir -p "$BACKUP_DIR"
 
# SQLite-safe snapshot without stopping the container
docker exec vaultwarden sqlite3 /data/db.sqlite3 ".backup '/data/db-backup.sqlite3'"
 
tar -czf "$BACKUP_DIR/vaultwarden-$TIMESTAMP.tar.gz" \
  -C /opt/vaultwarden/data db-backup.sqlite3 attachments rsa_key.pem rsa_key.pub.pem
 
rm /opt/vaultwarden/data/db-backup.sqlite3
 
# Retain 14 days of backups
find "$BACKUP_DIR" -name "vaultwarden-*.tar.gz" -mtime +14 -delete
chmod +x /opt/vaultwarden/backup.sh

Schedule it with cron and ship the archive off-host (rsync, rclone to object storage, etc.) — a backup that lives on the same disk as the vault doesn't protect against disk failure or ransomware.

0 3 * * * /opt/vaultwarden/backup.sh >> /var/log/vaultwarden-backup.log 2>&1

Verification

Confirm the deployment end-to-end:

# Container is healthy
docker compose ps
 
# HTTPS responds with a valid cert
curl -sI https://vault.example.com | head -n 1
 
# API is reachable
curl -s https://vault.example.com/alive

Install the Bitwarden browser extension or mobile app, choose "Self-hosted" in server settings, enter your domain, and log in with an invited account. Save a test entry, then confirm it syncs by checking the same vault from a second device.

Restore-test the backup at least once:

docker compose stop vaultwarden
tar -xzf backups/vaultwarden-<timestamp>.tar.gz -C /tmp/restore-test
# verify db-backup.sqlite3 opens cleanly
sqlite3 /tmp/restore-test/db-backup.sqlite3 ".tables"
docker compose start vaultwarden

Troubleshooting

"Username or password is incorrect" on first login — Confirm SIGNUPS_ALLOWED was true at the moment you registered the first account, or use an admin-panel invite instead.

WebSocket sync not working (changes don't push to other devices) — Ensure WEBSOCKET_ENABLED=true is set and that your reverse proxy passes through the Upgrade and Connection headers on the /notifications/hub path.

Admin panel returns 404 — The admin interface is disabled entirely if ADMIN_TOKEN is unset or empty. Check docker compose logs vaultwarden for a startup warning confirming the admin route is active.

Emails (invites, 2FA codes) never arrive — Vaultwarden needs explicit SMTP configuration (SMTP_HOST, SMTP_FROM, SMTP_USERNAME, SMTP_PASSWORD); without it, invitation links must be copied manually from the admin panel and shared out-of-band.

Container restarts in a loop after an update — Check for a schema migration failure in the logs; restore the previous backup and pin the image to the last known-good tag before retrying.

Summary

Vaultwarden gives you a fully compatible Bitwarden vault without the resource overhead or vendor dependency of the official self-hosted stack. The critical hardening steps are non-negotiable for anything internet-facing: disable open signups, protect the admin token, terminate TLS properly, enforce 2FA at the organization level, and — above all — back up the SQLite database and RSA keys somewhere that isn't the same disk. A password manager is only as trustworthy as its recovery story.