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
#!/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 .envThis 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
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: bridgeThis 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
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: webtraefik.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
#!/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.shDaily 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
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)
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.