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.

Similar Posts