Storage choice: SQLite or PostgreSQL

Vaultwarden supports SQLite (default), PostgreSQL, and MySQL/MariaDB. For one user or a small family, SQLite is fine — the database is a single file, backups are trivial, and the access pattern is mostly tiny reads. For a team of more than a handful, or any high-availability setup, PostgreSQL is the right pick. This walkthrough uses SQLite for simplicity; the PostgreSQL difference is a single environment variable at the end.

Install via Podman or Docker

Create a directory for persistent data, then run the container. Quadlet form (see the Podman/Quadlet tutorial for the full pattern):

# ~/.config/containers/systemd/vaultwarden.container
[Unit]
Description=Vaultwarden
After=network-online.target

[Container]
Image=docker.io/vaultwarden/server:latest
ContainerName=vaultwarden
PublishPort=127.0.0.1:8080:80
Volume=%h/vaultwarden/data:/data:Z
Environment=DOMAIN=https://vault.example.com
Environment=SIGNUPS_ALLOWED=true
Environment=ADMIN_TOKEN=<long-random-string>

[Service]
Restart=always

[Install]
WantedBy=default.target

Plain docker-compose equivalent:

version: "3.9"
services:
  vaultwarden:
    image: vaultwarden/server: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"
      ADMIN_TOKEN: <long-random-string>

Generate the admin token with the Argon2-hashed form — plain-text admin tokens in env vars are explicitly discouraged in recent Vaultwarden:

docker run --rm vaultwarden/server /vaultwarden hash
# Enter a long password when prompted; copy the printed $argon2id$... hash
# into ADMIN_TOKEN.

Reverse proxy with TLS

Vaultwarden refuses to serve clients unless the connection is HTTPS — the client-side encryption assumes it. Caddy:

vault.example.com {
    reverse_proxy 127.0.0.1:8080

    # Vaultwarden has a separate WebSocket endpoint for live sync
    @ws path /notifications/hub
    handle @ws {
        reverse_proxy 127.0.0.1:8080 {
            transport http {
                read_timeout 1h
                write_timeout 1h
            }
        }
    }
}

nginx equivalent (the WebSocket location block matters):

server {
    listen 443 ssl http2;
    server_name vault.example.com;
    # ... ssl_certificate, ssl_certificate_key ...

    client_max_body_size 525M;     # for attachments

    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;
    }

    location /notifications/hub {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Upgrade    $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 600s;
    }
}

SMTP for invites and password reset

Without email, organizations cannot send invitations, and there's no password-hint email. Configure SMTP via environment:

SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_SECURITY=starttls
SMTP_FROM=vault@example.com
SMTP_FROM_NAME=Vault
SMTP_USERNAME=vault@example.com
SMTP_PASSWORD=...

Test from the admin panel → SMTP settings → "Send test email."

The admin panel

Visit https://vault.example.com/admin and enter the plaintext admin password (the one matching the $argon2id$ hash you set as ADMIN_TOKEN). The panel gives:

  • User management (resetting MFA, deleting accounts, disabling accounts)
  • Org management
  • Live config editor for env vars (changes persist to data/config.json and take effect without a restart for most settings)
  • SMTP test send
  • Backup hint (it's just data/db.sqlite3 + data/attachments/)

Hardening after first user is created

Two changes once your account exists:

  1. Set SIGNUPS_ALLOWED=false (admin panel → General Settings) — further accounts go via emailed invite only. Without this, your /api/accounts/register endpoint is open to the internet.
  2. Set SHOW_PASSWORD_HINT=false — otherwise the API will return the password hint for any registered email, which is a username-enumeration channel.

Enable MFA on every account from a client (Settings → Two-step Login — TOTP, FIDO2/WebAuthn, or Email).

fail2ban / CrowdSec

Vaultwarden logs failed admin/login attempts; a fail2ban jail or a CrowdSec scenario blocks repeated failures at the firewall. Minimal fail2ban filter:

# /etc/fail2ban/filter.d/vaultwarden.conf
[Definition]
failregex = ^.*Username or password is incorrect\. Try again\. IP: <ADDR>\..*$
            ^.*Invalid admin token\. IP: <ADDR>.*$
ignoreregex =
# /etc/fail2ban/jail.d/vaultwarden.conf
[vaultwarden]
enabled  = true
port     = 80,443
filter   = vaultwarden
logpath  = /home/youruser/vaultwarden/data/vaultwarden.log
maxretry = 5
findtime = 5m
bantime  = 1h

Set LOG_FILE=/data/vaultwarden.log as a container env var so the logs land at a stable path.

Backups

Three things to capture:

  • data/db.sqlite3 — the entire vault, encrypted at rest with each user's account key. Use sqlite3 data/db.sqlite3 ".backup /backup/$(date +%F).sqlite3" to get a consistent copy without quiescing.
  • data/attachments/ — user-uploaded files attached to vault items.
  • data/sends/, data/rsa_key.* — Send feature state and the JWT signing keys.

The whole data/ directory packaged into a nightly restic backup (see restic + S3) is sufficient. Even if the storage backend is breached, the vault content is still client-side encrypted — an attacker has to also know each user's master password to decrypt.

PostgreSQL variant

Swap SQLite for Postgres by setting one env var:

DATABASE_URL=postgresql://vw_user:<password>@postgres-host:5432/vaultwarden

The schema is migrated automatically on first start. Backups become pg_dump instead of file copy; everything else is identical.

Migrating from the official Bitwarden

The data formats are the same. Export from the official client (Settings → Tools → Export vault → .json, encrypted), point the same client at the Vaultwarden URL, create a new account with the same master password, and import the .json. No conversion step.