Bitwarden Self-Hosted Setup: Complete Ubuntu Server Guide
Bitwarden Self-Hosted Setup: Complete Ubuntu Server Guide
Self-hosting Bitwarden gives you complete control over your password vault. Unlike cloud-based services, self-hosted Bitwarden runs on your own infrastructure, meaning your encryption keys never leave your server. This guide walks through installing Bitwarden on Ubuntu using Docker Compose, securing it with HTTPS, and setting up automated backups.
What You’ll Need Before Starting
- Ubuntu Server 20.04 LTS or newer (22.04 LTS recommended)
- Docker and Docker Compose installed
- A domain name (e.g., passwords.example.com)
- Root or sudo access to your server
- Minimum 2GB RAM and 10GB storage for Bitwarden container
- Open ports 80 and 443 for HTTP/HTTPS traffic
Step 1: Install Docker and Docker Compose
First, update your system packages and install Docker:
sudo apt update && sudo apt upgrade -y
sudo apt install -y docker.io docker-compose git curl wget
sudo systemctl start docker
sudo systemctl enable docker
sudo usermod -aG docker $USER
Verify Docker installation:
docker --version
docker-compose --version
Log out and log back in to apply Docker group changes, or run:
newgrp docker
Step 2: Create Directory Structure and Download Bitwarden
Create a dedicated directory for Bitwarden:
mkdir -p /opt/bitwarden
cd /opt/bitwarden
mkdir -p data logs
Clone the official Bitwarden repository (or download the Docker image directly):
wget https://github.com/bitwarden/server/releases/download/v2024.1.0/docker-compose.yml
Alternatively, create a custom docker-compose.yml file (see Step 3 below).
Step 3: Configure Docker Compose
Create a custom docker-compose.yml file optimized for self-hosting:
cat > /opt/bitwarden/docker-compose.yml << 'EOF'
version: '3.8'
services:
bitwarden:
image: vaultwarden/server:latest
container_name: bitwarden
restart: always
ports:
- "127.0.0.1:80:80"
- "127.0.0.1:443:443"
volumes:
- ./data:/data
- ./logs:/logs
environment:
- DOMAIN=https://passwords.example.com
- SIGNUPS_ALLOWED=false
- INVITATIONS_ORG_ALLOW_USER=true
- SHOW_PASSWORD_HINT=false
- LOG_FILE=/logs/bitwarden.log
- LOG_LEVEL=info
- EXTENDED_LOGGING=true
- EXTENDED_LOGGING_FILE=/logs/extended.log
- DATABASE_URL=sqlite:/data/db.sqlite3
networks:
- bitwarden_network
nginx:
image: nginx:alpine
container_name: bitwarden_nginx
restart: always
ports:
- "0.0.0.0:80:80"
- "0.0.0.0:443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./ssl:/etc/nginx/ssl:ro
depends_on:
- bitwarden
networks:
- bitwarden_network
networks:
bitwarden_network:
driver: bridge
EOF
Important Environment Variables Explained:
| Variable | Purpose | Recommended Value |
|---|---|---|
| DOMAIN | Your Bitwarden's public URL | https://passwords.example.com |
| SIGNUPS_ALLOWED | Allow new user registration | false (for self-hosted) |
| INVITATIONS_ORG_ALLOW_USER | Allow organization invitations | true |
| SHOW_PASSWORD_HINT | Display password hints on login | false (security best practice) |
| LOG_LEVEL | Logging verbosity | info (use debug for troubleshooting) |
Step 4: Configure Nginx Reverse Proxy
Create an Nginx configuration file to handle SSL termination and reverse proxy:
cat > /opt/bitwarden/nginx.conf << 'EOF'
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
client_max_body_size 525M;
# Rate limiting
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login_limit:10m rate=5r/m;
upstream bitwarden {
server bitwarden:80;
}
# HTTP redirect to HTTPS
server {
listen 80 default_server;
server_name _;
return 301 https://$host$request_uri;
}
# HTTPS server
server {
listen 443 ssl http2;
server_name passwords.example.com;
ssl_certificate /etc/nginx/ssl/certificate.crt;
ssl_certificate_key /etc/nginx/ssl/private.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
location / {
limit_req zone=api_limit burst=20 nodelay;
proxy_pass http://bitwarden;
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_http_version 1.1;
proxy_set_header Connection "upgrade";
}
location /identity {
limit_req zone=login_limit burst=5 nodelay;
proxy_pass http://bitwarden;
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;
}
}
}
EOF
Step 5: Set Up HTTPS with Let's Encrypt
Install Certbot for automatic SSL certificate generation:
sudo apt install -y certbot python3-certbot-dns-cloudflare
If using Cloudflare (recommended for auto-renewal), create credentials file:
mkdir -p ~/.secrets/certbot
cat > ~/.secrets/certbot/cloudflare.ini << 'EOF'
dns_cloudflare_api_token = your_cloudflare_api_token
EOF
chmod 600 ~/.secrets/certbot/cloudflare.ini
Generate certificate:
sudo certbot certonly --dns-cloudflare \
--dns-cloudflare-credentials ~/.secrets/certbot/cloudflare.ini \
-d passwords.example.com
Copy certificates to Bitwarden directory:
sudo mkdir -p /opt/bitwarden/ssl
sudo cp /etc/letsencrypt/live/passwords.example.com/fullchain.pem /opt/bitwarden/ssl/certificate.crt
sudo cp /etc/letsencrypt/live/passwords.example.com/privkey.pem /opt/bitwarden/ssl/private.key
sudo chown -R 1000:1000 /opt/bitwarden/ssl
Set up automatic renewal:
sudo systemctl enable certbot.timer
sudo systemctl start certbot.timer
Alternative: Self-Signed Certificates (Testing Only)
For development or testing environments:
mkdir -p /opt/bitwarden/ssl
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /opt/bitwarden/ssl/private.key \
-out /opt/bitwarden/ssl/certificate.crt
Step 6: Start Bitwarden Services
Navigate to the Bitwarden directory and launch containers:
cd /opt/bitwarden
sudo docker-compose up -d
Verify containers are running:
sudo docker-compose ps
Check logs for errors:
sudo docker-compose logs -f bitwarden
Wait 30-60 seconds for initialization. You should see:
[INFO] Bitwarden is ready
Step 7: Configure DNS and Access Bitwarden
Update your DNS A record to point to your server's IP:
- Type: A
- Name: passwords (or your subdomain)
- Content: Your server's public IP address
- TTL: 3600 (or auto)
Access Bitwarden at https://passwords.example.com and create your admin account on first login.
Step 8: Set Up Automated Backups
Create a backup script:
cat > /opt/bitwarden/backup.sh << 'EOF'
#!/bin/bash
BACKUP_DIR="/opt/bitwarden/backups"
DATE=$(date +%Y%m%d_%H%M%S)
RETENTION_DAYS=30
mkdir -p $BACKUP_DIR
# Stop containers
cd /opt/bitwarden
sudo docker-compose down
# Backup data directory
tar -czf $BACKUP_DIR/bitwarden_backup_$DATE.tar.gz \
/opt/bitwarden/data \
/opt/bitwarden/logs
# Restart containers
sudo docker-compose up -d
# Remove backups older than retention period
find $BACKUP_DIR -name "bitwarden_backup_*.tar.gz" -mtime +$RETENTION_DAYS -delete
echo "Backup completed: bitwarden_backup_$DATE.tar.gz"
EOF
chmod +x /opt/bitwarden/backup.sh
Schedule daily backups with cron:
sudo crontab -e
Add this line (runs backup at 2 AM daily):
0 2 * * * /opt/bitwarden/backup.sh >> /var/log/bitwarden_backup.log 2>&1
Alternative: Remote Backup with Rclone
For offsite backups to cloud storage:
sudo apt install -y rclone
rclone config # Configure your cloud storage provider
# Add to backup script:
rclone copy $BACKUP_DIR remote:bitwarden-backups/
Step 9: Security Hardening
Firewall Configuration (UFW):
sudo ufw enable
sudo ufw allow 22/tcp # SSH
sudo ufw allow 80/tcp # HTTP
sudo ufw allow 443/tcp # HTTPS
sudo ufw default deny incoming
sudo ufw default allow outgoing
Fail2Ban Installation (Brute-Force Protection):
sudo apt install -y fail2ban
cat | sudo tee /etc/fail2ban/jail.local << 'EOF'
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 5
[sshd]
enabled = true
[nginx-limit-req]
enabled = true
logpath = /var/log/nginx/error.log
EOF
sudo systemctl restart fail2ban
Additional Hardening Steps:
- Disable signup in Bitwarden (already set in docker-compose.yml:
SIGNUPS_ALLOWED=false) - Enable two-factor authentication (2FA) on your admin account
- Regularly update Docker images:
sudo docker-compose pull && sudo docker-compose up -d - Review logs monthly:
docker-compose logs --tail=100 bitwarden - Keep your server OS patched:
sudo apt update && sudo apt upgrade -y
Step 10: Monitoring and Maintenance
Check Container Health:
sudo docker stats bitwarden
sudo docker-compose exec bitwarden sqlite3 /data/db.sqlite3 ".tables"
Update Bitwarden:
cd /opt/bitwarden
sudo docker-compose pull
sudo docker-compose up -d
View Recent Logs:
tail -n 50 /opt/bitwarden/logs/bitwarden.log
Troubleshooting Common Issues
| Issue | Solution |
|---|---|
| SSL certificate errors | Verify domain DNS is correct. Wait 5-10 min for DNS propagation. Re-run certbot with --force-renewal |
| "Connection refused" errors | Check if containers are running: docker-compose ps. Check firewall rules with sudo ufw status |
| Bitwarden web UI won't load | Verify DOMAIN variable matches your URL. Check nginx logs: docker logs bitwarden_nginx |
| Can't log in after setup | Clear browser cache. Check if signup is disabled (it should be). Wait 60 seconds for DB initialization |
| High CPU or memory usage | Increase container resources in docker-compose.yml. Check for malicious login attempts in logs |
Conclusion
You now have a fully functional, self-hosted Bitwarden instance with HTTPS encryption, automated backups, and security hardening. Your password vault is stored on your own infrastructure with complete privacy.
Key Takeaways:
- Self-hosted Bitwarden provides maximum control and privacy
- Docker Compose simplifies deployment and management
- Let's Encrypt certificates provide free, auto-renewing HTTPS
- Regular backups are essential for disaster recovery
- Security hardening (firewall, fail2ban, 2FA) protects against attacks
- Monthly updates keep your installation secure and stable
For additional help, consult the Vaultwarden Wiki and official Bitwarden documentation.
