Skip to main content
COSMICBYTEZLABS
NewsSecurityHOWTOsToolsStudyTraining
ProjectsChecklistsAI RankingsNewsletterStatusTagsAbout
Subscribe

Press Enter to search or Esc to close

News
Security
HOWTOs
Tools
Study
Training
Projects
Checklists
AI Rankings
Newsletter
Status
Tags
About
RSS Feed
Reading List
Subscribe

Stay in the Loop

Get the latest security alerts, tutorials, and tech insights delivered to your inbox.

Subscribe NowFree forever. No spam.
COSMICBYTEZLABS

Your trusted source for IT intelligence, cybersecurity insights, and hands-on technical guides.

740+ Articles
120+ Guides

CONTENT

  • Latest News
  • Security Alerts
  • HOWTOs
  • Projects
  • Exam Prep

RESOURCES

  • Search
  • Browse Tags
  • Newsletter Archive
  • Reading List
  • RSS Feed

COMPANY

  • About Us
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CosmicBytez Labs. All rights reserved.

System Status: Operational
  1. Home
  2. Projects
  3. Self-Hosted Password Manager with Vaultwarden
Self-Hosted Password Manager with Vaultwarden
PROJECTIntermediate

Self-Hosted Password Manager with Vaultwarden

Deploy a fully self-hosted, Bitwarden-compatible password manager using Vaultwarden on Docker with Caddy reverse proxy, automatic TLS, WebSocket notifications, and enterprise-grade security hardening.

Dylan H.

Projects

April 22, 2026
10 min read
2-4 hours

Tools & Technologies

DockerDocker ComposeCaddyVaultwardenopensslfail2ban

Overview

Trusting a third-party SaaS with your most sensitive credentials is a calculated risk. Breaches at password manager vendors have happened — and they will happen again. Vaultwarden is a lightweight, MIT-licensed, unofficial Bitwarden server implementation written in Rust. It speaks the full Bitwarden API, meaning every official Bitwarden client (browser extensions, desktop, mobile, CLI) works against it without modification.

This project walks through a production-quality deployment: Vaultwarden behind a Caddy reverse proxy that auto-issues a Let's Encrypt TLS certificate, WebSocket notifications for real-time vault sync, SQLite persistence, an admin panel protected by an Argon2-hashed token, and fail2ban brute-force protection. By the end you will have a fully self-sovereign password manager accessible from any Bitwarden client.

What you will build:

  • Vaultwarden server running in Docker
  • Caddy reverse proxy with automatic HTTPS and HTTP/2
  • WebSocket endpoint for push notifications
  • Encrypted, offsite-ready backup strategy
  • Fail2ban rules watching Vaultwarden auth logs
  • Hardened configuration: signups locked, 2FA enforced, invite-only access

Architecture

Internet
    │
    ▼
[ Caddy :443 ]  ─── auto TLS (Let's Encrypt)
    │
    ├──  /notifications/hub  ──►  vaultwarden:3012  (WebSocket)
    │
    └──  /*                  ──►  vaultwarden:80    (HTTP API + Web Vault)
                                        │
                                   [ SQLite DB ]
                                   /data/db.sqlite3
                                        │
                                   [ RSA Keys ]
                                   /data/rsa_key*
                                        │
                                   [ Attachments ]
                                   /data/attachments/

All Bitwarden clients (browser extensions, desktop apps, mobile apps) connect to https://vault.your-domain.com using the standard Bitwarden protocol. Caddy sits in front, terminates TLS, and routes WebSocket connections to port 3012 and all other traffic to port 80 on the Vaultwarden container.

Minimum requirements:

ResourceMinimumRecommended
CPU1 vCPU2 vCPU
RAM256 MB512 MB
Disk1 GB5 GB
OSAny Docker hostUbuntu 22.04 LTS
DomainRequired (HTTPS mandatory)Subdomain of owned domain

HTTPS is non-negotiable. Browsers block the Web Crypto API on plain HTTP, which breaks password encryption entirely. You need a real domain with a valid TLS certificate.


Prerequisites

  1. A Linux host with Docker Engine and Docker Compose v2 installed
  2. A domain name you control (e.g., vault.example.com) with an A record pointing to your server's public IP
  3. Ports 80 and 443 open on your firewall/router
  4. Root or sudo access on the host

Verify Docker is ready:

docker --version        # Docker Engine 24.x or later
docker compose version  # Docker Compose v2.x

Step 1 — Directory Structure

Create a clean project directory to hold your compose file, Caddy config, and persistent data:

mkdir -p ~/vaultwarden/{data,caddy/{config,data}}
cd ~/vaultwarden

Your layout will be:

~/vaultwarden/
├── docker-compose.yml
├── .env
├── Caddyfile
├── data/               # Vaultwarden persistent data (SQLite, keys, attachments)
└── caddy/
    ├── config/         # Caddy runtime config (persisted)
    └── data/           # Caddy TLS certificates (persisted)

Step 2 — Generate a Secure Admin Token

Vaultwarden's admin panel is protected by a token. Use Argon2 hashing (recommended over plain text) so the raw secret never lives in the container environment:

# Generate a random 48-char secret
SECRET=$(openssl rand -base64 48)
echo "Save this secret: $SECRET"
 
# Hash it with Argon2 using the Vaultwarden image itself
docker run --rm -it vaultwarden/server /vaultwarden hash --preset owasp
# When prompted, paste your $SECRET value
# Copy the full $argon2id$... output string

You will use the raw $SECRET when logging into /admin, and the hashed $argon2id$... string as the ADMIN_TOKEN environment variable.


Step 3 — Environment File

Create .env in your project directory. Never commit this file to version control.

cat > .env << 'EOF'
# Domain — must match your DNS A record
DOMAIN=https://vault.example.com
 
# Admin panel — paste the full $argon2id$... hash here
ADMIN_TOKEN=$argon2id$v=19$m=65540,t=3,p=4$XXXXXXXXXXXXXXXXXXXX
 
# SMTP (optional — enables email verification, 2FA recovery, invites)
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_SECURITY=starttls
SMTP_USERNAME=no-reply@example.com
SMTP_PASSWORD=your_smtp_password
SMTP_FROM=no-reply@example.com
SMTP_FROM_NAME=Vaultwarden
 
# Timezone for accurate log timestamps (critical for fail2ban)
TZ=America/Edmonton
EOF

Replace vault.example.com with your actual subdomain and fill in your SMTP credentials if you have them. SMTP enables email 2FA recovery and user invitations — recommended but not required for initial setup.


Step 4 — Docker Compose

# docker-compose.yml
services:
  vaultwarden:
    image: vaultwarden/server:latest
    container_name: vaultwarden
    restart: always
    volumes:
      - ./data:/data
    environment:
      DOMAIN: ${DOMAIN}
      ADMIN_TOKEN: ${ADMIN_TOKEN}
      SIGNUPS_ALLOWED: "true"      # Set false after creating your account
      WEBSOCKET_ENABLED: "true"
      LOG_FILE: /data/vaultwarden.log
      LOG_LEVEL: info
      EXTENDED_LOGGING: "true"
      TZ: ${TZ}
      # SMTP (uncomment if configured in .env)
      # SMTP_HOST: ${SMTP_HOST}
      # SMTP_PORT: ${SMTP_PORT}
      # SMTP_SECURITY: ${SMTP_SECURITY}
      # SMTP_USERNAME: ${SMTP_USERNAME}
      # SMTP_PASSWORD: ${SMTP_PASSWORD}
      # SMTP_FROM: ${SMTP_FROM}
      # SMTP_FROM_NAME: ${SMTP_FROM_NAME}
    networks:
      - vw_net
 
  caddy:
    image: caddy:2-alpine
    container_name: caddy
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - ./caddy/data:/data
      - ./caddy/config:/config
    networks:
      - vw_net
    depends_on:
      - vaultwarden
 
networks:
  vw_net:
    driver: bridge

If Caddy already runs as your main reverse proxy for other services, add Vaultwarden to its existing Caddyfile and skip the Caddy service here.


Step 5 — Caddyfile

# Caddyfile
vault.example.com {
  encode gzip
 
  # WebSocket endpoint — must route before the main proxy
  reverse_proxy /notifications/hub vaultwarden:3012 {
    header_up X-Real-IP {remote_host}
  }
 
  # All other traffic to Vaultwarden HTTP port
  reverse_proxy vaultwarden:80 {
    header_up X-Real-IP {remote_host}
  }
}

Replace vault.example.com with your domain. Caddy automatically obtains and renews a Let's Encrypt certificate on first start. The WebSocket path /notifications/hub must come first in the Caddyfile — Caddy matches routes top-to-bottom.


Step 6 — Launch the Stack

# Start in detached mode
docker compose up -d
 
# Tail logs to watch for startup errors
docker compose logs -f --tail 50

Watch for these lines confirming a clean start:

vaultwarden  | INFO vaultwarden::db::sqlite::mod   > SQLite database opened successfully
vaultwarden  | INFO vaultwarden::api               > Listening on http://0.0.0.0:80
caddy        | {"level":"info","msg":"serving initial configuration"}
caddy        | {"level":"info","msg":"certificate obtained successfully"}

Navigate to https://vault.example.com — you should see the Bitwarden-compatible web vault UI with a valid TLS certificate.


Step 7 — Create Your Admin Account

  1. Open https://vault.example.com in your browser
  2. Click Create account
  3. Register with your email and a strong master password (this is the only password you need to remember — make it count)
  4. Log in and verify the vault loads correctly

Step 8 — Admin Panel & Security Hardening

Navigate to https://vault.example.com/admin and enter your plain-text $SECRET (not the hash).

Disable public signups

Once your accounts are created, lock down registration:

# Edit .env
SIGNUPS_ALLOWED=false
 
# Restart to apply
docker compose restart vaultwarden

Or toggle it in the admin panel under General Settings → Allow new signups.

Enforce 2FA

In the admin panel → Users → click any user → Enable 2FA.

For organisation-wide enforcement: create an Organization inside the vault, then set the 2FA policy under Organization Settings → Policies → Require two-step login.

Invite-only mode

With signups disabled, use the admin panel to send email invitations to specific addresses. This is how you onboard additional users without reopening public registration.

Review admin panel settings to check

SettingRecommended Value
Allow new signupsDisabled
Allow invitationsEnabled
Require email verificationEnabled (if SMTP configured)
Password iterations (PBKDF2)600,000 (default)
Disable 2FA rememberEnabled for higher security
DomainMatches your DOMAIN env var

Step 9 — Fail2Ban Integration

Vaultwarden logs failed authentication attempts to /data/vaultwarden.log. Fail2ban reads these logs and bans IPs after repeated failures.

Install fail2ban on the host:

sudo apt install fail2ban -y

Create the filter at /etc/fail2ban/filter.d/vaultwarden.conf:

[INCLUDES]
before = common.conf
 
[Definition]
failregex = ^.*Username or password is incorrect\. Try again\. IP: <ADDR>\..*$
            ^.*TOTP, email or Duo code is incorrect\. Try again\. IP: <ADDR>\..*$
 
ignoreregex =

Create the jail at /etc/fail2ban/jail.d/vaultwarden.conf:

[vaultwarden]
enabled  = true
port     = 80,443
filter   = vaultwarden
logpath  = /home/YOUR_USER/vaultwarden/data/vaultwarden.log
maxretry = 5
bantime  = 1h
findtime = 10m

Replace YOUR_USER with your actual username and adjust logpath to the absolute path of your data/ directory.

sudo systemctl restart fail2ban
sudo fail2ban-client status vaultwarden

You should see the jail as active with 0 bans initially.


Step 10 — Backup Strategy

Vaultwarden stores everything under ./data/. A safe backup requires the container to be stopped (or at minimum not writing) to avoid a corrupt SQLite snapshot.

Simple nightly backup script — save as /usr/local/bin/vaultwarden-backup.sh:

#!/bin/bash
set -euo pipefail
 
COMPOSE_DIR="/home/YOUR_USER/vaultwarden"
BACKUP_DIR="/mnt/backup/vaultwarden"
DATE=$(date +%Y-%m-%d)
 
mkdir -p "$BACKUP_DIR"
 
# Pause the container briefly for a consistent snapshot
docker compose -f "$COMPOSE_DIR/docker-compose.yml" stop vaultwarden
 
# Archive and compress
tar -czf "$BACKUP_DIR/vaultwarden-$DATE.tar.gz" -C "$COMPOSE_DIR" data/
 
# Restart immediately
docker compose -f "$COMPOSE_DIR/docker-compose.yml" start vaultwarden
 
# Retain 30 days of backups
find "$BACKUP_DIR" -name "*.tar.gz" -mtime +30 -delete
 
echo "Backup completed: vaultwarden-$DATE.tar.gz"
chmod +x /usr/local/bin/vaultwarden-backup.sh
 
# Schedule daily at 3:00 AM
echo "0 3 * * * root /usr/local/bin/vaultwarden-backup.sh >> /var/log/vaultwarden-backup.log 2>&1" \
  | sudo tee /etc/cron.d/vaultwarden-backup

For offsite storage, pipe the archive through rclone copy to an S3-compatible bucket, Backblaze B2, or another cloud provider. Encrypt the archive with gpg --symmetric before uploading if you want zero-knowledge offsite backups.


Testing

Functional verification

# Health endpoint
curl -s https://vault.example.com/alive
# Expected: ""  (empty 200 OK)
 
# Version info
curl -s https://vault.example.com/api/config | jq .version
 
# WebSocket connectivity (requires wscat: npm i -g wscat)
wscat -c wss://vault.example.com/notifications/hub
# Should connect and stay open

Client connectivity

  1. Install the Bitwarden browser extension (Chrome/Firefox/Edge)
  2. Click the extension icon → Settings → Self-hosted environment
  3. Set Server URL to https://vault.example.com
  4. Log in with your credentials — your vault should sync within seconds

Test the same process on the Bitwarden mobile app (iOS/Android) and the Bitwarden desktop app to confirm cross-platform compatibility.

TLS check

curl -vI https://vault.example.com 2>&1 | grep -E "SSL|TLS|issuer|expire"

Verify Let's Encrypt is the issuer and check the expiry — Caddy auto-renews 30 days before expiration.

Fail2ban verification

# Deliberately fail login 5 times from a test machine, then check:
sudo fail2ban-client status vaultwarden
# You should see the test IP in the "Banned IP list"
 
# Unban when done testing
sudo fail2ban-client set vaultwarden unbanip YOUR_TEST_IP

Deployment Notes

When running Vaultwarden behind an existing Traefik, Nginx, or Caddy instance, skip the dedicated Caddy service and instead add your routing rules to the existing proxy. The key points for any reverse proxy:

  • Forward the real client IP via X-Real-IP or X-Forwarded-For (required for fail2ban to ban the correct address)
  • Route /notifications/hub to port 3012 (WebSocket), all other paths to port 80
  • Ensure WebSocket upgrade headers are passed: Connection: Upgrade, Upgrade: websocket
  • TLS termination happens at the proxy; Vaultwarden itself speaks plain HTTP internally

For a Traefik-based homelab, labels on the Vaultwarden container handle routing automatically:

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

Extensions & Next Steps

Migrate to PostgreSQL

SQLite is fine for personal or small team use. For larger deployments, migrate to PostgreSQL for better concurrent write performance:

# Add to docker-compose.yml
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: vaultwarden
      POSTGRES_USER: vw
      POSTGRES_PASSWORD: strong_password_here
    volumes:
      - pg_data:/var/lib/postgresql/data
 
volumes:
  pg_data:

Then add DATABASE_URL: postgresql://vw:strong_password_here@postgres/vaultwarden to the Vaultwarden environment. Vaultwarden handles the schema migration automatically on first start.

Organizations and Sharing

Create an Organization inside the vault for shared credentials (team passwords, infrastructure secrets). Organizations support:

  • Role-based collections (admin, manager, user, custom)
  • Granular item permissions per collection
  • Event logs for audit trails (Enterprise plan equivalent)
  • 2FA policy enforcement

Emergency Access

Enable emergency access under Account Settings → Emergency Access. This lets a designated trusted contact request access to your vault after a configurable wait period — a critical feature for self-hosted deployments where there is no vendor support recovery path.

LDAP / Active Directory Sync

The vaultwarden-ldap sidecar container syncs users from LDAP/AD automatically:

  vaultwarden-ldap:
    image: vividboarder/vaultwarden_ldap:latest
    environment:
      VAULTWARDEN_URL: http://vaultwarden:80
      VAULTWARDEN_ADMIN_TOKEN: your_plain_text_secret
      LDAP_HOST: ldap://your-dc
      LDAP_BIND_DN: cn=svc-vw,ou=services,dc=example,dc=com
      LDAP_BIND_PASSWORD: ldap_service_account_password
      LDAP_SEARCH_BASE_DN: ou=users,dc=example,dc=com

Monitoring

Mount the log file into a Promtail/Loki container for centralized log ingestion, or set up a Grafana dashboard that tracks failed login attempts, vault syncs, and container health over time.

Vaultwarden Updates

docker compose pull vaultwarden
docker compose up -d vaultwarden

Always take a backup before pulling a new image. Vaultwarden uses SQLite migrations — downgrading after a migration is not supported, so keep recent backups before upgrading.


Vaultwarden gives you a fully featured, client-compatible Bitwarden deployment on hardware you control. Combined with automatic TLS, WebSocket sync, brute-force protection, and encrypted backups, this stack handles both personal and small-team credential management without relying on any third-party vault service.

#password-manager#vaultwarden#bitwarden#docker#security#homelab#self-hosted#privacy#caddy

Related Articles

Velociraptor DFIR: Endpoint Forensics and Incident Response at Scale

Deploy Velociraptor — the open-source DFIR platform — to collect forensic artifacts, run live endpoint hunts with VQL, and build an incident response...

11 min read

WireGuard Road Warrior VPN Server

Build a self-hosted WireGuard VPN server on Ubuntu for secure remote access — with NAT masquerading, DNS leak protection, QR-code client provisioning, and...

7 min read

Build a Collaborative IPS with CrowdSec

Deploy CrowdSec on a Linux server to get community-powered intrusion prevention — block brute-force attacks, credential stuffing, and vulnerability...

10 min read
Back to all Projects