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 Methods

Basic 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 (Recommended):

version: '3'

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

Start with Docker Compose:

docker compose up -d

Method 2: Standalone Binary

Download latest release:

# Linux x86_64
wget https://github.com/dani-garcia/vaultwarden/releases/latest/download/vaultwarden-linux-x86_64.tar.gz
tar -xzf vaultwarden-linux-x86_64.tar.gz

# ARM (Raspberry Pi)
wget https://github.com/dani-garcia/vaultwarden/releases/latest/download/vaultwarden-linux-arm.tar.gz
tar -xzf vaultwarden-linux-arm.tar.gz

Run Vaultwarden:

export ROCKET_PORT=8080
export DATA_FOLDER=/var/lib/vaultwarden
./vaultwarden

Systemd service:

# /etc/systemd/system/vaultwarden.service
[Unit]
Description=Vaultwarden Server
After=network.target

[Service]
User=vaultwarden
Group=vaultwarden
ExecStart=/usr/local/bin/vaultwarden
Environment="DATA_FOLDER=/var/lib/vaultwarden"
Environment="ROCKET_PORT=8080"
Restart=always

[Install]
WantedBy=multi-user.target

Enable and start:

sudo systemctl enable --now vaultwarden

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

Update Standalone Binary

# Download latest
wget https://github.com/dani-garcia/vaultwarden/releases/latest/download/vaultwarden-linux-x86_64.tar.gz

# Stop service
sudo systemctl stop vaultwarden

# Replace binary
sudo tar -xzf vaultwarden-linux-x86_64.tar.gz -C /usr/local/bin/

# Start service
sudo systemctl start vaultwarden

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