Vaultwarden Self-Hosted Deployment

6 October 2025 · Updated 6 October 2025

Vaultwarden is a Rust reimplementation of the Bitwarden server API. Much lighter on resources than the official server, runs happily on a Pi, and has no paywall on features like 2FA, organisations, or file attachments.

Overview

Vaultwarden is an independent Rust implementation of the Bitwarden server API. It uses a fraction of the RAM and CPU the official server needs, which makes it well suited to self-hosting.

Repository

https://github.com/dani-garcia/vaultwarden

Key Differences from Official Bitwarden

FeatureVaultwardenOfficial Bitwarden
LanguageRustC# (.NET)
Resource Usage~10-20 MB RAM~2-4 GB RAM
DatabaseSQLite, MySQL, PostgreSQLMS SQL Server, PostgreSQL
ContainersSingle container8+ containers
LicenseGPL-3.0AGPL-3.0 + Proprietary
Premium FeaturesAll freeRequires paid license
Official SupportCommunityBitwarden Inc.
Enterprise SSO
ComplexityLowHigh

When to Use Vaultwarden

✓ Good for:

  • Personal use
  • Small teams (< 50 users)
  • Home labs / self-hosting
  • Limited resources (Raspberry Pi, VPS)
  • Free premium features

✗ Not ideal for:

  • Enterprise deployments
  • Regulatory compliance requirements
  • Need for official support
  • Enterprise SSO requirements

System Requirements

Minimum Requirements

  • CPU: 1 core
  • RAM: 256 MB
  • Storage: 1 GB
  • OS: Linux, Windows, macOS
  • Docker: 20.10+
  • CPU: 2 cores
  • RAM: 512 MB
  • Storage: 5 GB SSD
  • Reverse proxy: NGINX, Caddy, Traefik
  • SSL Certificate: Let’s Encrypt or valid cert

Vaultwarden will run on something as small as a Pi Zero.

Installation

Vaultwarden publishes container images and nothing else. There are no prebuilt binaries on the releases page, so the options are the image below or building it yourself from source.

Docker run

docker run -d --name vaultwarden \
  -e ROCKET_PORT=8080 \
  -v /vw-data:/data \
  -p 8080:8080 \
  --restart unless-stopped \
  vaultwarden/server:latest

Docker Compose

services:
  vaultwarden:
    image: vaultwarden/server:latest
    container_name: vaultwarden
    restart: unless-stopped
    environment:
      - DOMAIN=https://vault.example.com
      - SIGNUPS_ALLOWED=false
      - INVITATIONS_ALLOWED=true
      - SHOW_PASSWORD_HINT=false
      - WEBSOCKET_ENABLED=true
      - SMTP_HOST=smtp.example.com
      - SMTP_FROM=vaultwarden@example.com
      - SMTP_PORT=587
      - SMTP_SECURITY=starttls
      - SMTP_USERNAME=vaultwarden@example.com
      - SMTP_PASSWORD=smtp_password
      - ADMIN_TOKEN=your_secure_admin_token
      - LOG_FILE=/data/vaultwarden.log
      - LOG_LEVEL=info
    volumes:
      - ./vw-data:/data
    ports:
      - "8080:80"
      - "3012:3012"  # WebSocket port
docker compose up -d

Configuration

Environment Variables

Core settings:

# Domain (REQUIRED for proper function)
DOMAIN=https://vault.example.com

# Database (defaults to SQLite)
# For SQLite (default)
DATABASE_URL=data/db.sqlite3

# For MySQL
DATABASE_URL=mysql://user:password@host:port/database

# For PostgreSQL
DATABASE_URL=postgresql://user:password@host:port/database

# Rocket (web server) settings
ROCKET_ADDRESS=0.0.0.0
ROCKET_PORT=8080
ROCKET_WORKERS=10

# WebSocket support (for live sync)
WEBSOCKET_ENABLED=true
WEBSOCKET_ADDRESS=0.0.0.0
WEBSOCKET_PORT=3012

Security settings:

# Disable new user registration (recommended)
SIGNUPS_ALLOWED=false

# Allow invitations from existing users
INVITATIONS_ALLOWED=true

# Require email verification
SIGNUPS_VERIFY=true

# Disable password hints
SHOW_PASSWORD_HINT=false

# Enforce 2FA for all users (optional)
REQUIRE_DEVICE_EMAIL=true

# Admin token (for /admin panel)
ADMIN_TOKEN=your_secure_random_token

SMTP (Email) settings:

SMTP_HOST=smtp.example.com
SMTP_FROM=vaultwarden@example.com
SMTP_FROM_NAME=Vaultwarden
SMTP_PORT=587
SMTP_SECURITY=starttls  # or 'force_tls' or 'off'
SMTP_USERNAME=vaultwarden@example.com
SMTP_PASSWORD=smtp_password
SMTP_TIMEOUT=15

Advanced settings:

# File upload size limit (in MB)
FILE_SIZE_LIMIT=512

# Attachment folder path
ATTACHMENTS_FOLDER=/data/attachments

# Icon service (for website favicons)
ICON_SERVICE=internal  # or 'duckduckgo', 'google'

# Disable icon downloads
DISABLE_ICON_DOWNLOAD=false

# Enable U2F (for FIDO2 keys)
ENABLE_U2F=true

# Logging
LOG_FILE=/data/vaultwarden.log
LOG_LEVEL=info  # or debug, trace, warn, error
EXTENDED_LOGGING=true

# Emergency access
EMERGENCY_ACCESS_ALLOWED=true

Generate Admin Token

Create secure admin token:

openssl rand -base64 48

Use this as ADMIN_TOKEN value.

Configuration File (Alternative)

Instead of environment variables, use config.json:

{
  "domain": "https://vault.example.com",
  "signups_allowed": false,
  "invitations_allowed": true,
  "show_password_hint": false,
  "admin_token": "your_secure_token",
  "smtp_host": "smtp.example.com",
  "smtp_from": "vaultwarden@example.com",
  "smtp_port": 587,
  "smtp_security": "starttls",
  "smtp_username": "vaultwarden@example.com",
  "smtp_password": "smtp_password"
}

Mount in Docker:

volumes:
  - ./config.json:/data/config.json

Reverse Proxy Configuration

NGINX

server {
    listen 443 ssl http2;
    server_name vault.example.com;

    # SSL configuration
    ssl_certificate /etc/letsencrypt/live/vault.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/vault.example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;

    # Security headers
    add_header Strict-Transport-Security "max-age=31536000" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;

    client_max_body_size 525M;

    location / {
        proxy_pass http://localhost: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;
    }

    # WebSocket support
    location /notifications/hub {
        proxy_pass http://localhost:3012;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        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/negotiate {
        proxy_pass http://localhost: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;
    }
}

# HTTP redirect
server {
    listen 80;
    server_name vault.example.com;
    return 301 https://$server_name$request_uri;
}

Caddy (Simplest)

Caddyfile:

vault.example.com {
    reverse_proxy localhost:8080 {
        header_up X-Real-IP {remote_host}
    }

    reverse_proxy /notifications/hub localhost:3012
}

Caddy automatically handles SSL with Let’s Encrypt!

Traefik (Docker)

docker-compose.yml with Traefik labels:

version: '3'

services:
  vaultwarden:
    image: vaultwarden/server:latest
    container_name: vaultwarden
    restart: unless-stopped
    environment:
      - DOMAIN=https://vault.example.com
      - WEBSOCKET_ENABLED=true
    volumes:
      - ./vw-data:/data
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.vaultwarden.rule=Host(`vault.example.com`)"
      - "traefik.http.routers.vaultwarden.entrypoints=websecure"
      - "traefik.http.routers.vaultwarden.tls.certresolver=letsencrypt"
      - "traefik.http.services.vaultwarden.loadbalancer.server.port=80"

      # WebSocket
      - "traefik.http.routers.vaultwarden-ws.rule=Host(`vault.example.com`) && Path(`/notifications/hub`)"
      - "traefik.http.routers.vaultwarden-ws.entrypoints=websecure"
      - "traefik.http.routers.vaultwarden-ws.tls.certresolver=letsencrypt"
      - "traefik.http.services.vaultwarden-ws.loadbalancer.server.port=3012"
    networks:
      - traefik

networks:
  traefik:
    external: true

Admin Panel

Access at: https://vault.example.com/admin

Login with admin token (set via ADMIN_TOKEN)

Admin Panel Features

  • Users: View, delete, deauthorize users
  • Invitations: View/delete pending invitations
  • Organizations: View organizations
  • Diagnostics: Check config, connectivity
  • Settings: Modify runtime configuration

Disable Admin Panel (After Setup)

For security, disable after initial configuration:

# Remove or comment out ADMIN_TOKEN
# ADMIN_TOKEN=

Or use fail2ban to limit access attempts.

Database Options

SQLite (Default)

Pros:

  • Zero configuration
  • Perfect for small deployments
  • Single file backup

Cons:

  • Not suitable for high concurrency
  • Limited to single server

Location:

/data/db.sqlite3

MySQL / MariaDB

Setup:

  1. Create database:

    CREATE DATABASE vaultwarden CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
    CREATE USER 'vaultwarden'@'%' IDENTIFIED BY 'password';
    GRANT ALL PRIVILEGES ON vaultwarden.* TO 'vaultwarden'@'%';
    FLUSH PRIVILEGES;
  2. Configure Vaultwarden:

    DATABASE_URL=mysql://vaultwarden:password@mysql:3306/vaultwarden

PostgreSQL

Setup:

  1. Create database:

    CREATE DATABASE vaultwarden;
    CREATE USER vaultwarden WITH PASSWORD 'password';
    GRANT ALL PRIVILEGES ON DATABASE vaultwarden TO vaultwarden;
  2. Configure Vaultwarden:

    DATABASE_URL=postgresql://vaultwarden:password@postgres:5432/vaultwarden

Backup and Restore

Backup with SQLite

Manual backup:

# Stop Vaultwarden
docker stop vaultwarden

# Backup data directory
tar -czf vaultwarden-backup-$(date +%Y%m%d).tar.gz ./vw-data/

# Start Vaultwarden
docker start vaultwarden

Automated backup script:

#!/bin/bash
# vaultwarden-backup.sh

BACKUP_DIR="/backups/vaultwarden"
DATA_DIR="./vw-data"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)

# Stop container for consistent backup
docker stop vaultwarden

# Create backup
mkdir -p "$BACKUP_DIR"
tar -czf "$BACKUP_DIR/vaultwarden-$TIMESTAMP.tar.gz" "$DATA_DIR"

# Start container
docker start vaultwarden

# Keep last 30 backups
find "$BACKUP_DIR" -name "vaultwarden-*.tar.gz" -mtime +30 -delete

echo "Backup completed: $BACKUP_DIR/vaultwarden-$TIMESTAMP.tar.gz"

Cron job (daily at 2 AM):

0 2 * * * /home/user/vaultwarden-backup.sh >> /var/log/vaultwarden-backup.log 2>&1

Restore from Backup

# Stop Vaultwarden
docker stop vaultwarden

# Remove existing data
rm -rf ./vw-data

# Extract backup
tar -xzf vaultwarden-20250106.tar.gz

# Start Vaultwarden
docker start vaultwarden

Backup with MySQL/PostgreSQL

Use standard database backup tools:

MySQL:

mysqldump -u vaultwarden -p vaultwarden > vaultwarden-$(date +%Y%m%d).sql

PostgreSQL:

pg_dump -U vaultwarden vaultwarden > vaultwarden-$(date +%Y%m%d).sql

Security Hardening

1. Disable User Registration

SIGNUPS_ALLOWED=false
INVITATIONS_ALLOWED=true  # Allow existing users to invite

2. Enforce 2FA

In admin panel:

  1. Access /admin
  2. Navigate to Settings
  3. Enable relevant 2FA options

3. Implement Fail2Ban

Create filter:

# /etc/fail2ban/filter.d/vaultwarden.conf
[Definition]
failregex = ^.*Username or password is incorrect\. Try again\. IP: <ADDR>\. Username:.*$
ignoreregex =

Create jail:

# /etc/fail2ban/jail.d/vaultwarden.conf
[vaultwarden]
enabled = true
port = 80,443
filter = vaultwarden
logpath = /path/to/vw-data/vaultwarden.log
maxretry = 3
bantime = 14400
findtime = 14400

4. Restrict Admin Panel

NGINX - Allow only from specific IP:

location /admin {
    allow 192.168.1.0/24;
    deny all;

    proxy_pass http://localhost:8080;
    # ... other proxy settings
}

5. Enable Extended Logging

LOG_LEVEL=info
EXTENDED_LOGGING=true

Monitoring

Check Vaultwarden Status

Docker:

docker ps | grep vaultwarden
docker logs vaultwarden
docker stats vaultwarden

Health Check

HTTP endpoint:

curl -I https://vault.example.com/alive

Expected: HTTP/1.1 200 OK

Prometheus Monitoring

Vaultwarden doesn’t expose Prometheus metrics natively, but you can monitor via:

  • Blackbox exporter - HTTP endpoint monitoring
  • Docker exporter - Container metrics
  • Log exporters - Parse logs for metrics

Updates

Update Docker Container

# Pull latest image
docker pull vaultwarden/server:latest

# Stop and remove old container
docker stop vaultwarden
docker rm vaultwarden

# Start with new image (using same docker run command)
docker run -d --name vaultwarden \
  -v /vw-data:/data \
  -p 8080:8080 \
  vaultwarden/server:latest

With Docker Compose:

docker compose pull
docker compose up -d

Migrating from Official Bitwarden

Export from Bitwarden

  1. Login to Bitwarden web vault
  2. ToolsExport Vault
  3. Select format: .json
  4. Download export

Import to Vaultwarden

  1. Login to Vaultwarden web vault
  2. ToolsImport Data
  3. Select format: Bitwarden (json)
  4. Upload export file

Note: Organization data requires manual re-creation.

Troubleshooting

WebSocket Not Working

Symptoms: Live sync doesn’t work, requires manual refresh

Fix:

  1. Ensure WEBSOCKET_ENABLED=true
  2. Check reverse proxy WebSocket configuration
  3. Verify port 3012 is accessible

Email Not Sending

Check SMTP settings:

# Test SMTP from container
docker exec -it vaultwarden sh
telnet smtp.example.com 587

Common issues:

  • Wrong SMTP port
  • SMTP_SECURITY mismatch
  • Firewall blocking outbound SMTP
  • ISP blocking port 25

Database Locked (SQLite)

Cause: SQLite doesn’t handle concurrent writes well

Solutions:

  • Migrate to MySQL/PostgreSQL
  • Reduce concurrent user operations
  • Increase ROCKET_WORKERS

High Memory Usage

Normal memory usage:

  • SQLite: 10-50 MB
  • MySQL: 20-100 MB
  • Heavy usage: Up to 200 MB

If excessive:

  • Check for large attachments
  • Review FILE_SIZE_LIMIT
  • Monitor database growth

Comparison: Vaultwarden vs Official Bitwarden

FeatureVaultwardenOfficial Bitwarden
2FA✓ TOTP, YubiKey, Duo, Email✓ TOTP, YubiKey, Duo, Email
Organizations✓ Free✓ Paid ($3-5/user/month)
File Attachments✓ Free (1GB/user)✓ Paid (1GB-100GB)
Emergency Access✓ Free✓ Paid
Directory Sync✓ Paid
Enterprise SSO✓ Paid
Event Logs✓ Basic✓ Advanced (paid)
SupportCommunityOfficial
Compliance Certs✓ SOC 2, HIPAA

Additional Resources

Key Topics

Core Configuration