Setting Up a Self-Hosted Password Manager with Bitwarden

Setting Up a Self-Hosted Password Manager with Bitwarden

I stopped trusting cloud password managers about two years ago. Not because Bitwarden's cloud is insecure—it's actually excellent—but because I wanted total control over my vault. Now I run Bitwarden entirely on my own infrastructure, and I'm going to walk you through exactly how to do it. By the end of this guide, you'll have a fully functional, encrypted password manager that only you can access.

Why Self-Host Your Password Manager?

When you self-host Bitwarden, your encrypted vault lives on hardware you control. Your master password never leaves your device. There's no middle-man, no compliance audits to worry about (unless you're in heavily regulated work), and most importantly, you own your data completely. I prefer this because it aligns with my philosophy: important stuff stays close to home.

The trade-off? You're responsible for backups, SSL certificates, and keeping the server patched. But if you're already running a VPS for other services, the overhead is minimal. And if you don't have a VPS yet, a basic 1-core, 1GB RAM instance from RackNerd runs about $40 per year—easily worth it for password management alone.

What You'll Need

For this setup, you need:

If you're shopping for a VPS, I've been using RackNerd's offerings for small projects like this. Their new-year promotions typically offer instances that handle Bitwarden with room to spare, and pricing is genuinely competitive for annual commitments.

Installing Docker and Docker Compose

Log into your VPS and start fresh. I always install Docker's official repository to get the latest stable version.

#!/bin/bash
# Update system
sudo apt update && sudo apt upgrade -y

# Install Docker prerequisites
sudo apt install -y apt-transport-https ca-certificates curl software-properties-common

# Add Docker's official GPG key
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

# Add Docker repository
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# Install Docker and Docker Compose
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

# Start Docker daemon
sudo systemctl start docker
sudo systemctl enable docker

# Add your user to the docker group (optional but convenient)
sudo usermod -aG docker $USER
newgrp docker

Verify everything works:

docker --version
docker compose version

Setting Up Bitwarden with Docker Compose

Now I'll create the Bitwarden stack. Create a directory for your project and set up the compose file:

mkdir -p ~/bitwarden && cd ~/bitwarden
nano docker-compose.yml

Paste this configuration:

version: '3.8'

services:
  bitwarden:
    image: bitwardenrs/server:latest
    container_name: bitwarden
    restart: always
    environment:
      DOMAIN: https://vault.example.com
      SIGNUPS_ALLOWED: "false"
      INVITATIONS_ORG_ALLOW_USER: "true"
      SHOW_PASSWORD_HINT: "false"
      LOG_FILE: /data/bitwarden.log
      LOG_LEVEL: info
      EXTENDED_LOGGING: "true"
      EXTENDED_LOGGING_FILE: /data/extended.log
      ADMIN_TOKEN: ${ADMIN_TOKEN}
      WEBSOCKET_ENABLED: "true"
      ROCKET_PORT: 80
    volumes:
      - ./bw-data:/data
    ports:
      - "127.0.0.1:8000:80"
    networks:
      - bitwarden_net
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/alive"]
      interval: 30s
      timeout: 5s
      retries: 3

  caddy:
    image: caddy:latest
    container_name: caddy-bitwarden
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - ./caddy-data:/data
      - ./caddy-config:/config
    networks:
      - bitwarden_net
    environment:
      DOMAIN: vault.example.com

networks:
  bitwarden_net:
    driver: bridge

Create the Caddyfile for SSL termination:

nano Caddyfile

Add this:

vault.example.com {
    reverse_proxy http://bitwarden:80
    encode gzip
    header / {
        X-Frame-Options "SAMEORIGIN"
        X-Content-Type-Options "nosniff"
        X-XSS-Protection "1; mode=block"
        Referrer-Policy "same-origin"
    }
}

Replace vault.example.com with your actual domain. Caddy will automatically provision a Let's Encrypt certificate on first run.

Generating the Admin Token

You need an admin token to access the admin panel. Generate one before starting:

nano .env

Add this line (generate a strong random string):

ADMIN_TOKEN=your_random_admin_token_here_min_32_chars

To generate a token, I use:

openssl rand -base64 32
Watch out: Your admin token grants full control over Bitwarden. Keep it secret, store it in a password manager, and change it regularly. Never commit it to version control or share it with anyone.

Starting the Services

Pull the images and start everything:

docker compose pull
docker compose up -d

Check the logs to ensure everything initialized correctly:

docker compose logs -f bitwarden

Wait 30-60 seconds for the database to initialize. You should see messages about listening on port 80 and the database being created. When you see clean logs with no errors, the container is ready.

Configuring Bitwarden

Visit https://vault.example.com/admin in your browser. Enter your admin token. From here, you can:

Create your main account at https://vault.example.com/#/register. Use a strong master password—this is the key to everything. Then immediately disable further signups in the admin panel by removing the admin token.

Tip: Enable two-factor authentication on your Bitwarden account immediately. Go to settings, find "Two-step Login," and add authenticator apps or FIDO2 security keys. This is non-negotiable for a password manager.

Securing Your Instance

Self-hosting means you're responsible for security. Here's my checklist:

Firewall rules: Only open ports 80 and 443 to the world. SSH access should be restricted to your IP or a Tailscale network.

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

Disable the admin panel after setup: Once configured, remove or set a random admin token. You can always restart with a new one if needed.

Automate updates: Use Watchtower to keep images patched. Add this service to your compose file:

  watchtower:
    image: containrrr/watchtower:latest
    container_name: watchtower
    restart: always
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    command: --interval 86400 --cleanup
    networks:
      - bitwarden_net

Set up backups: Back up your bw-data directory daily. I use a simple cron job that compresses and ships it to cold storage.

#!/bin/bash
BACKUP_DIR="/backups/bitwarden"
DATA_DIR="$HOME/bitwarden/bw-data"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)

mkdir -p $BACKUP_DIR
tar -czf $BACKUP_DIR/bitwarden_$TIMESTAMP.tar.gz $DATA_DIR
find $BACKUP_DIR -name "*.tar.gz" -mtime +30 -delete

Accessing Bitwarden Remotely

Your Bitwarden instance is now live on the public internet. Use the web vault at https://vault.example.com, or install the official browser extensions and mobile apps. Just log in with your account credentials.

For additional security, I recommend using a Tailscale network to restrict access. Change your Caddyfile to only accept connections from your Tailscale subnet, or put the entire instance behind Authelia for zero-trust access.

Migrating from Cloud Services

If you're coming from Bitwarden Cloud, the migration is seamless. Export your vault (Settings → Tools → Export Vault) as encrypted JSON, import it into your self-hosted instance, and you're done. All passwords transfer over.

For other password managers (LastPass, 1Password, KeePass), export to CSV or JSON, and Bitwarden can import those formats too.

Next Steps

You've now got a fully encrypted, self-hosted password manager. Next, consider:

Questions? Self-hosting Bitwarden is rock-solid—the official documentation is thorough, and the community is helpful. Drop a comment below if you hit any snags.

Discussion