Last updated: March 20, 2026

Vaultwarden is a lightweight, open-source implementation of Bitwarden’s password vault API. Self-hosting Vaultwarden gives you full control over your credentials without trusting a third-party SaaS provider. This guide covers full Docker deployment, reverse proxy configuration with Nginx, SSL certificate automation with Let’s Encrypt, automated backups, security hardening, and the maintenance workflow for running your own password vault.

Quick Setup Steps

  1. Provision a VPS or local server with at least 512 MB RAM and Docker installed
  2. Pull the Vaultwarden image: docker pull vaultwarden/server:latest
  3. Create a Docker Compose file with volume mounts for persistent data storage
  4. Configure environment variables including ADMIN_TOKEN, DOMAIN, and SMTP_HOST
  5. Set up Nginx or Caddy as a reverse proxy with SSL termination
  6. Obtain SSL certificates using Certbot or Caddy automatic HTTPS
  7. Start the container: docker-compose up -d
  8. Create your admin account at https://yourdomain.com/admin
  9. Configure automated backups of the SQLite database to encrypted offsite storage
  10. Set up Watchtower for automatic container image updates

Table of Contents

Why Self-Host Vaultwarden?

Bitwarden’s SaaS offering is privacy-respecting, but self-hosting provides:

Trade-off - You manage infrastructure, backups, SSL certificates, and server updates.

System Requirements

Minimum specs for single-user Vaultwarden:

For multiple users (family, small team):

Deployment options:

Prerequisites

Before you begin, make sure you have the following ready:

Step 1 - Install ation: Docker Compose Setup

Step 1 - Install Docker

On Ubuntu/Debian:

Update system packages
sudo apt update && sudo apt upgrade -y

Install Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh

Add current user to docker group (avoid sudo for docker commands)
sudo usermod -aG docker $USER
newgrp docker

Verify installation
docker --version
docker run hello-world

On macOS:

Install via Homebrew
brew install docker-compose

Or download Docker Desktop from docker.com
Docker Desktop includes docker and docker-compose

Step 2 - Create directory structure

Create vaultwarden directory
mkdir -p ~/vaultwarden/{data,nginx,letsencrypt}
cd ~/vaultwarden

Create subdirectories
mkdir -p data/attachments
mkdir -p nginx/ssl

Step 3 - Create Docker Compose configuration

Create docker-compose.yml:

version: '3.8'

services:
  vaultwarden:
    image: vaultwarden/server:latest
    container_name: vaultwarden
    restart: always
    environment:
      DOMAIN: "https://vault.example.com"
      SIGNUPS_ALLOWED: "false"
      SHOW_PASSWORD_HINT: "false"
      LOG_LEVEL: "info"
      LOG_FILE: "/data/vaultwarden.log"
      EXTENDED_LOGGING: "true"
      EXTENDED_LOGGING_FORMAT: "json"

      # Admin panel token (change this to a long random string)
      ADMIN_TOKEN: "$argon2id$v=19$m=19456,t=2,p=1$your-long-random-string-here"

      # Database (SQLite for single-user, PostgreSQL for scale)
      DATABASE_URL: "sqlite:///data/db.sqlite3"

      # Email notifications (optional)
      # SMTP_HOST: "smtp.gmail.com"
      # SMTP_PORT: "587"
      # SMTP_SECURITY: "starttls"
      # SMTP_USERNAME: "your-email@gmail.com"
      # SMTP_PASSWORD: "your-app-password"

    ports:
      - "127.0.0.1:8080:80"

    volumes:
      - ./data:/data
      - ./data/attachments:/data/attachments
      - ./data/vaultwarden.log:/data/vaultwarden.log

    # Resource limits
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 256M

    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/alive"]
      interval: 30s
      timeout: 3s
      retries: 3
      start_period: 30s

  # PostgreSQL (optional, for scale and reliability)
  # postgres:
  #   image: postgres:15-alpine
  #   container_name: vaultwarden-db
  #   restart: always
  #   environment:
  #     POSTGRES_USER: vaultwarden
  #     POSTGRES_PASSWORD: "long-random-password"
  #     POSTGRES_DB: vaultwarden
  #   volumes:
  #     - ./data/postgres:/var/lib/postgresql/data
  #   ports:
  #     - "127.0.0.1:5432:5432"

networks:
  default:
    name: vaultwarden-network

Generate admin token:

Generate a strong admin token
openssl rand -base64 32

Use with htpasswd to create Argon2id hash
sudo apt install apache2-utils  # or brew install httpd
htpasswd -c -B -C 10 /tmp/admin admin
Copy the hash to ADMIN_TOKEN in docker-compose.yml

Or use an online generator (less secure): Argon2 generator

Step 4 - Start Vaultwarden

Navigate to vaultwarden directory
cd ~/vaultwarden

Start the container
docker-compose up -d

View logs
docker-compose logs -f vaultwarden

Expected output:
[INFO] vaultwarden 1.0.37
[INFO] Rocket has launched from http://0.0.0.0
[INFO] Database - sqlite:///data/db.sqlite3

Vaultwarden is now running on http://localhost:8080 (local access only).

Step 2 - Reverse Proxy: Nginx Configuration

Running Vaultwarden directly on the internet is risky. Use Nginx as a reverse proxy to:

Step 1 - Create Nginx configuration

Create nginx/vaultwarden.conf:

Upstream vaultwarden server
upstream vaultwarden {
    server vaultwarden:80;
    keepalive 32;
}

Rate limiting
limit_req_zone $binary_remote_addr zone=vaultwarden_limit:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=identity_limit:10m rate=5r/m;

server {
    listen 80;
    server_name vault.example.com www.vault.example.com;

    # Redirect HTTP to HTTPS
    return 301 https://$server_name$request_uri;
}

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

    # SSL certificates (Let's Encrypt)
    ssl_certificate /etc/letsencrypt/live/vault.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/vault.example.com/privkey.pem;

    # Modern SSL configuration
    ssl_protocols TLSv1.3 TLSv1.2;
    ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;
    ssl_session_tickets off;
    ssl_stapling on;
    ssl_stapling_verify on;

    # Security headers
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Content-Type-Options nosniff always;
    add_header X-Frame-Options DENY always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;

    # Logging
    access_log /var/log/nginx/vaultwarden_access.log;
    error_log /var/log/nginx/vaultwarden_error.log;

    # Body size limit (for file uploads)
    client_max_body_size 525M;

    # Root location
    root /var/www/html;
    index index.html;

    # Main reverse proxy
    location / {
        proxy_pass http://vaultwarden;
        proxy_http_version 1.1;

        # Headers for WebSocket support
        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;
        proxy_set_header X-Forwarded-Host $server_name;

        # Timeouts for long-running operations
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }

    # API rate limiting
    location /identity {
        limit_req zone=identity_limit burst=2 nodelay;
        proxy_pass http://vaultwarden;
        proxy_http_version 1.1;
        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 /api {
        limit_req zone=vaultwarden_limit burst=20 nodelay;
        proxy_pass http://vaultwarden;
        proxy_http_version 1.1;
        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;
    }

    # Block admin panel from public internet
    location /admin {
        allow 127.0.0.1;
        allow 192.168.1.0/24;  # Your home network
        deny all;

        proxy_pass http://vaultwarden;
        proxy_http_version 1.1;
        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;
    }
}

Step 2 - Add Nginx to Docker Compose

Update docker-compose.yml:

  nginx:
    image: nginx:latest
    container_name: vaultwarden-nginx
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/vaultwarden.conf:/etc/nginx/conf.d/vaultwarden.conf:ro
      - ./letsencrypt:/etc/letsencrypt:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
    depends_on:
      - vaultwarden
    networks:
      - vaultwarden-network

Step 3 - SSL Certificates: Let’s Encrypt Setup

Option 1 - Certbot (recommended for dynamic DNS)

Install Certbot on your host machine (not in Docker):

Ubuntu/Debian
sudo apt install certbot python3-certbot-nginx

macOS
brew install certbot

Request initial certificate
sudo certbot certonly --standalone -d vault.example.com -d www.vault.example.com

Expected output:
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Certificate is saved at - /etc/letsencrypt/live/vault.example.com/fullchain.pem
Key is saved at - /etc/letsencrypt/live/vault.example.com/privkey.pem

Auto-renew with cron job:

Edit crontab
sudo crontab -e

Add line (runs daily at 2 AM):
0 2 * * * certbot renew --quiet && systemctl reload nginx

Option 2 - Docker-based (Certbot in container)

Add to docker-compose.yml:

  certbot:
    image: certbot/certbot:latest
    container_name: vaultwarden-certbot
    restart: always
    volumes:
      - ./letsencrypt:/etc/letsencrypt
      - ./certbot/renewal:/var/lib/letsencrypt
    entrypoint: /bin/sh -c "certbot renew --quiet && sleep 86400 && exec /bin/sh -c \"$$@\"" -- /bin/sh
    environment:
      CERTBOT_EMAIL: "admin@example.com"

Step 4 - Backups: Automated Daily Backups

Create backup script backup.sh:

#!/bin/bash

Configuration
BACKUP_DIR="./backups"
DATA_DIR="./data"
RETENTION_DAYS=30
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$BACKUP_DIR/vaultwarden_backup_$TIMESTAMP.tar.gz"

Create backup directory
mkdir -p $BACKUP_DIR

Backup database, config, and attachments
tar -czf $BACKUP_FILE \
    -C $DATA_DIR db.sqlite3 config.json 2>/dev/null || \
    tar -czf $BACKUP_FILE \
    -C $DATA_DIR . \
    --exclude='*.log'

Log backup
echo "[$(date)] Backup created: $BACKUP_FILE ($(du -h $BACKUP_FILE | cut -f1))" >> $BACKUP_DIR/backup.log

Remove old backups (keep last 30 days)
find $BACKUP_DIR -name "vaultwarden_backup_*.tar.gz" -mtime +$RETENTION_DAYS -delete

Optional - Upload to cloud storage
aws s3 cp $BACKUP_FILE s3://your-backup-bucket/vaultwarden/

echo "Backup complete."

Add daily backup cron job:

Make script executable
chmod +x backup.sh

Edit crontab
crontab -e

Add line (runs daily at 3 AM):
0 3 * * * /home/user/vaultwarden/backup.sh >> /tmp/vaultwarden_backup.log 2>&1

Test backup recovery:

Extract backup
tar -xzf ./backups/vaultwarden_backup_20260320_030000.tar.gz

Verify files are intact
ls -la ./data/

Step 5 - Security Hardening

  1. Disable signups (if self-hosted for one user):

Already set in docker-compose.yml: SIGNUPS_ALLOWED: "false"

  1. Change admin token:

Generate a new token and restart:

Generate token
openssl rand -base64 32

Update docker-compose.yml ADMIN_TOKEN
Restart service
docker-compose restart vaultwarden
  1. Firewall rules:
UFW (Ubuntu)
sudo ufw allow 22/tcp    # SSH
sudo ufw allow 80/tcp    # HTTP
sudo ufw allow 443/tcp   # HTTPS
sudo ufw enable

iptables (direct)
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
iptables -A INPUT -j DROP
  1. Fail2ban (rate-limit login attempts):
Install
sudo apt install fail2ban

Create filter /etc/fail2ban/filter.d/vaultwarden.conf:
[Definition]
failregex = ^.* Invalid username or password\. \[.*?\] \[<HOST>\] .* -$
ignoreregex =

Create jail /etc/fail2ban/jail.d/vaultwarden.conf:
[vaultwarden]
enabled = true
port = http,https
filter = vaultwarden
logpath = /path/to/vaultwarden.log
maxretry = 5
findtime = 3600
bantime = 604800

Restart fail2ban
sudo systemctl restart fail2ban
  1. Monitor logs:
Follow logs in real-time
docker-compose logs -f vaultwarden

Check for suspicious activity
docker-compose logs vaultwarden | grep -i "error\|invalid\|unauthorized"

Step 6 - Perform Maintenance and Updates

Updating Vaultwarden:

Pull latest image
docker pull vaultwarden/server:latest

Restart container (will use new image)
docker-compose up -d vaultwarden

View version
docker-compose logs vaultwarden | grep "vaultwarden"

Monitoring disk space:

Check backup size
du -sh ./backups/

Check database size
du -sh ./data/db.sqlite3

Alert if backup grows too large
find ./backups -name "*.tar.gz" -mtime -1 -exec du -sh {} \; | awk '{print $1}'

Database maintenance:

Optimize SQLite database (monthly)
Connect to container and run:
docker-compose exec vaultwarden sqlite3 /data/db.sqlite3 "VACUUM;"

Step 7 - Client Setup

Bitwarden Web Vault - https://vault.example.com

Mobile/Desktop Apps:

  1. Download Bitwarden app (iOS, Android, macOS, Windows, Linux)
  2. In settings, set custom server: https://vault.example.com
  3. Create account (or import from cloud Bitwarden)
  4. Sync vault data

Browser Extension:

  1. Install Bitwarden extension
  2. Open extension settings
  3. Set server URL: https://vault.example.com
  4. Login with account credentials

Troubleshooting

Login fails with “connection refused”:

Check if vaultwarden is running
docker-compose ps

Restart if needed
docker-compose restart vaultwarden

HTTPS certificate errors:

Check certificate expiration
openssl x509 -in /etc/letsencrypt/live/vault.example.com/fullchain.pem -text -noout | grep -E "Not Before|Not After"

Manually renew
sudo certbot renew --force-renewal

Slow performance:

Check resource usage
docker stats vaultwarden

Increase memory limit in docker-compose.yml
Restart container
docker-compose restart vaultwarden

Database is locked:

Stop container
docker-compose stop vaultwarden

Delete lock file
rm ./data/db.sqlite3-wal ./data/db.sqlite3-shm 2>/dev/null

Restart
docker-compose start vaultwarden

Step 8 - Cost and Alternatives

Self-hosted Vaultwarden:

Bitwarden Cloud:

Alternative password managers:

For privacy-conscious users who want control, self-hosted Vaultwarden is the best value.

Frequently Asked Questions

How long does it take to self-host bitwarden vaultwarden: complete setup guide?

For a straightforward setup, expect 30 minutes to 2 hours depending on your familiarity with the tools involved. Complex configurations with custom requirements may take longer. Having your credentials and environment ready before starting saves significant time.

What are the most common mistakes to avoid?

The most frequent issues are skipping prerequisite steps, using outdated package versions, and not reading error messages carefully. Follow the steps in order, verify each one works before moving on, and check the official documentation if something behaves unexpectedly.

Do I need prior experience to follow this guide?

Basic familiarity with the relevant tools and command line is helpful but not strictly required. Each step is explained with context. If you get stuck, the official documentation for each tool covers fundamentals that may fill in knowledge gaps.

Is this approach secure enough for production?

The patterns shown here follow standard practices, but production deployments need additional hardening. Add rate limiting, input validation, proper secret management, and monitoring before going live. Consider a security review if your application handles sensitive user data.

Where can I get help if I run into issues?

Start with the official documentation for each tool mentioned. Stack Overflow and GitHub Issues are good next steps for specific error messages. Community forums and Discord servers for the relevant tools often have active members who can help with setup problems.

Related Articles