Skip to content
Learni
View all tutorials
Sécurité

How to Deploy Self-Hosted Bitwarden in 2026

Lire en français

Introduction

Bitwarden is the leading open-source password manager, but its cloud service raises data sovereignty concerns. In 2026, Vaultwarden (a lightweight, high-performance Bitwarden fork) leads self-hosted deployments with full API compatibility and minimal resource use (native SQLite, <100MB RAM). This advanced tutorial walks you through a production-ready setup: Docker Compose, Traefik SSL reverse proxy, LDAP/SAML authentication, encrypted backups, and API automations.

Why it matters: 80% of breaches stem from weak passwords (Verizon DBIR 2025). Self-host for complete control, GDPR-compliant audits, and costs under €10/month. We cover Docker fundamentals to enterprise configs like Prometheus monitoring and horizontal scaling. Result: a highly available instance accessible via official Bitwarden mobile and desktop apps. Estimated time: 45 minutes.

Prerequisites

  • Linux server (Ubuntu 24.04+ or VPS like Hetzner/DigitalOcean, 2 vCPU/4GB RAM)
  • Docker 27+ and Docker Compose 2.29+
  • Domain with A/AAAA DNS record pointing to server IP
  • Wildcard SSL certificate (Let's Encrypt via Traefik)
  • Docker, YAML, and bash knowledge (advanced level)
  • Ports 80/443 open (UFW/AWS SG firewall)

1. Project Structure and .env

setup.sh
#!/bin/bash
mkdir -p ~/bitwarden/{data,backups,logs}
cd ~/bitwarden
cat > .env << EOF
DOMAIN=bitwarden.votredomaine.com
EMAIL=admin@votredomaine.com
TZ=Europe/Paris
BITWARDEN_ADMIN_TOKEN=supersecretadmintoken1234567890abcdef
WEBSOCKET_ENABLED=true
SIGNUPS_ALLOWED=false
INVITATIONS_ALLOWED=false
DISABLE_USER_REGISTRATION=true
EOF
chmod 600 .env

This script sets up the directory structure and creates a secure .env file with critical variables (admin token generated via openssl rand -base64 48). DOMAIN handles Traefik/SSL routing, SIGNUPS_ALLOWED=false for production. Pitfall: weak token risks takeover; use pwgen or 1Password to generate a strong one.

Initial Configuration

Run bash setup.sh in a dedicated directory. The .env centralizes all secrets: never commit it to Git. ADMIN_TOKEN enables access to the /admin interface after deployment. TZ prevents log time drifts. Next, use docker-compose to orchestrate Vaultwarden + Traefik.

2. Docker Compose with Vaultwarden + Traefik

docker-compose.yml
version: '3.8'
services:
  traefik:
    image: traefik:v3.0
    restart: unless-stopped
    ports:
      - '80:80'
      - '443:443'
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./traefik.yml:/traefik.yml:ro
      - ./acme.json:/acme.json
    environment:
      - TZ=${TZ}
    networks:
      - vaultwarden

  vaultwarden:
    image: vaultwarden/server:latest
    restart: unless-stopped
    volumes:
      - ./data/:/data/
    environment:
      - DOMAIN=https://${DOMAIN}
      - WEBSOCKET_ENABLED=${WEBSOCKET_ENABLED}
      - SIGNUPS_ALLOWED=${SIGNUPS_ALLOWED}
      - ADMIN_TOKEN=${BITWARDEN_ADMIN_TOKEN}
      - TZ=${TZ}
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.vaultwarden.rule=Host(`$${DOMAIN}`)"
      - "traefik.http.routers.vaultwarden.tls.certresolver=letsencrypt"
      - "traefik.http.routers.vaultwarden.entrypoints=websecure"
      - "traefik.http.services.vaultwarden.loadbalancer.server.port=80"
    networks:
      - vaultwarden

networks:
  vaultwarden:
    driver: bridge

This docker-compose.yml deploys Vaultwarden behind Traefik for automatic SSL (Let's Encrypt). Traefik labels route HTTPS://DOMAIN to the internal port 80. Volumes persist SQLite data. Pitfall: without WEBSOCKET_ENABLED, real-time sync (mobile apps) fails; test with docker compose up -d.

3. Static Traefik Config

traefik.yml
global:
  checkNewVersion: false
  sendAnonymousUsage: false

entryPoints:
  web:
    address: ':80'
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
          permanente: true
  websecure:
    address: ':443'

providers:
  docker:
    exposedByDefault: false

certificatesResolvers:
  letsencrypt:
    acme:
      email: ${EMAIL}
      storage: acme.json
      httpChallenge:
        entryPoint: web

traefik.yml sets up HTTP/HTTPS entrypoints with automatic redirects and Let's Encrypt ACME. acme.json (chmod 600 after pre-setup) stores certificates. Pitfall: without exposedByDefault: false, all containers are exposed—a security risk; restart Traefik after creating an empty acme.json.

First Startup and Admin Setup

Create touch acme.json && chmod 600 acme.json. Launch docker compose up -d. Check logs with docker compose logs -f traefik. Access https://bitwarden.yourdomain.com. Create an admin account at /admin using the token. In the interface, enable 2FA with TOTP/YubiKey.

4. Encrypted Automated Backup

backup.sh
#!/bin/bash
set -e
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR=~/bitwarden/backups
GPG_KEY=yourgpgkeyid
DATA_DIR=~/bitwarden/data
tar czf /tmp/vaultwarden-${DATE}.tar.gz -C ${DATA_DIR}/..
 gpg --batch --yes --encrypt --recipient ${GPG_KEY} --output ${BACKUP_DIR}/vaultwarden-${DATE}.tar.gz.gpg /tmp/vaultwarden-${DATE}.tar.gz
rm /tmp/vaultwarden-${DATE}.tar.gz
find ${BACKUP_DIR} -type f -mtime +30 -delete

# Cron: 0 2 * * * /path/to/backup.sh

Daily backup script for the data/ directory, encrypted with GPG and 30-day rotation. set -e halts on errors. Pitfall: without GPG_KEY (set up beforehand with gpg --gen-key), backups are plaintext—major risk; test with bash backup.sh and gpg --decrypt file.gpg.

Enterprise Integrations

LDAP/SAML: Add LDAP_SERVER=ldap://dc.example.com to .env and restart. Scaling: Add Redis for high availability (REDIS_URL=redis://redis:6379). Verify in /admin → Settings.

5. Prometheus + Grafana Monitoring

docker-compose.monitoring.yml
version: '3.8'
services:
  prometheus:
    image: prom/prometheus:latest
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--web.console.libraries=/etc/prometheus/console_libraries'
      - '--web.console.templates=/etc/prometheus/consoles'
    ports:
      - '9090:9090'

  grafana:
    image: grafana/grafana:latest
    ports:
      - '3000:3000'
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin123

  vaultwarden-exporter:
    image: ghcr.io/wystia/vaultwarden-exporter:latest
    command: --vaultwarden-url=http://vaultwarden:80
    ports:
      - '8080:8080'

Monitoring extension with Vaultwarden metrics exporter (logins, vaults). Create prometheus.yml to scrape the vaultwarden-exporter. Pitfall: without importing Grafana dashboard 1860 (Bitwarden), metrics are useless; access Grafana at :3000.

6. API Script: List Vaults (Node.js)

api-vaults.ts
import fetch from 'node-fetch';

const BW_URL = 'https://bitwarden.votredomaine.com';
const BW_EMAIL = 'admin@example.com';
const BW_PASSWORD = 'strongpassword123';
const BW_CLIENT_ID = 'yourclientid';
const BW_CLIENT_SECRET = 'yourclientsecret';

async function login() {
  const response = await fetch(`${BW_URL}/identity/connect/token`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'password',
      username: BW_EMAIL,
      password: BW_PASSWORD,
      scope: 'api',
      client_id: BW_CLIENT_ID,
      client_secret: BW_CLIENT_SECRET,
    }),
  });
  const data = await response.json();
  return data.access_token;
}

async function listVaults(token: string) {
  const response = await fetch(`${BW_URL}/api/accounts`, {
    headers: { Authorization: `Bearer ${token}` },
  });
  const accounts = await response.json();
  console.log('Vaults:', accounts.data);
}

(async () => {
  const token = await login();
  await listVaults(token);
})();

TypeScript example for the Bitwarden API: OAuth login and listing accounts/vaults. Create an app in /admin → API → Client ID/Secret. Run npm i node-fetch. Pitfall: 'api' scope is required; tokens expire after 5 minutes—use refresh_token for renewal.

Best Practices

  • Secrets rotation: Change ADMIN_TOKEN/passwords monthly via /admin → Rotate.
  • HA setup: Use 3 Vaultwarden nodes + Postgres (env DATABASE_URL=postgres://...) + Redis.
  • Audit logs: Enable LOG_FILE=/data/logs and ship to ELK/Fluentd.
  • Zero Trust: Fail2ban on Traefik logs + Cloudflare WAF.
  • MFA enforced: /admin → Policies → Require 2FA for all users.

Common Errors to Avoid

  • Non-persistent certs: Forgetting acme.json causes SSL downtime on every restart.
  • SQLite in production: For >100 users, migrate to Postgres (docker exec vaultwarden sqlite3 /data/db.sqlite3 .dump | psql).
  • Ignoring websockets: Apps disconnect; ensure DOMAIN=https:// and expose port 3012 if needed.
  • No backups: Irreversible data loss; test restores weekly.

Next Steps

Explore Vaultwarden docs, Bitwarden API. Integrate with Authentik for SSO. Check our Learni DevSecOps trainings for secure self-hosting masterclasses.

How to Deploy Bitwarden Self-Hosted 2026 | Learni