$linuxjunkies
>

Self-Host Vaultwarden (Bitwarden Server)

Deploy a self-hosted Vaultwarden (Bitwarden-compatible) password server using Docker Compose, nginx reverse proxy, Let's Encrypt TLS, and automated backups.

IntermediateUbuntuDebianFedoraArch12 min readUpdated June 7, 2026

Before you start

  • Docker Engine 24+ and the docker compose plugin installed
  • A domain name with a DNS A record pointing to your server's public IP
  • Ports 80 and 443 open in your firewall
  • A non-root user with sudo privileges and membership in the docker group

Vaultwarden is a lightweight, unofficial Bitwarden-compatible server written in Rust. It runs comfortably on a cheap VPS or even a Raspberry Pi, uses a fraction of the resources of the official server, and lets you keep full control of your password vault. This guide walks through a production-ready setup: Docker Compose, an nginx reverse proxy with TLS, the admin panel, automated backups, and connecting clients.

Prerequisites and Architecture Overview

You need a server with a public IP, a domain name (or subdomain) pointed at it, and Docker + Docker Compose installed. Vaultwarden itself runs as a container; nginx runs alongside it and terminates TLS. Certbot manages certificates. Data lives in a named volume that your backup script snapshots nightly.

  • Port 80 and 443 open inbound (for Let's Encrypt and HTTPS traffic)
  • Docker Engine 24+ and the docker compose plugin (v2 syntax)
  • A DNS A record for vault.example.com already propagated
  • A non-root user with sudo and membership in the docker group

Step 1 — Generate the Admin Token

Vaultwarden's admin panel is protected by a bcrypt-hashed token, not a plain-text password. Generate the hash before writing any config files.

docker run --rm -it vaultwarden/server /vaultwarden hash --preset owasp

You'll be prompted to type a passphrase. Copy the $argon2id$... or $2y$... hash that is printed — you'll paste it into the Compose file shortly. Store the original passphrase somewhere safe; that's what you'll type in the browser.

Step 2 — Create the Project Directory and docker-compose.yml

mkdir -p /opt/vaultwarden && cd /opt/vaultwarden

Create docker-compose.yml with the following content, replacing the placeholder values:

cat > docker-compose.yml <<'EOF'
services:
  vaultwarden:
    image: vaultwarden/server:latest
    container_name: vaultwarden
    restart: unless-stopped
    volumes:
      - vw-data:/data
    environment:
      DOMAIN: "https://vault.example.com"
      SIGNUPS_ALLOWED: "false"          # disable after creating your account
      ADMIN_TOKEN: "PASTE_BCRYPT_HASH_HERE"
      SMTP_HOST: "smtp.example.com"     # optional, for email verification
      SMTP_PORT: "587"
      SMTP_SECURITY: "starttls"
      SMTP_USERNAME: "[email protected]"
      SMTP_PASSWORD: "smtp-password"
      SMTP_FROM: "[email protected]"
    networks:
      - proxy

  nginx:
    image: nginx:stable-alpine
    container_name: nginx-vw
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - /etc/letsencrypt:/etc/letsencrypt:ro
      - /var/www/certbot:/var/www/certbot:ro
    networks:
      - proxy
    depends_on:
      - vaultwarden

volumes:
  vw-data:

networks:
  proxy:
EOF

Step 3 — Configure nginx

mkdir -p nginx/conf.d
cat > nginx/conf.d/vaultwarden.conf <<'EOF'
server {
    listen 80;
    server_name vault.example.com;
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }
    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl;
    server_name vault.example.com;

    ssl_certificate     /etc/letsencrypt/live/vault.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/vault.example.com/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         HIGH:!aNULL:!MD5;

    # Increase body size for large vault imports
    client_max_body_size 128M;

    location / {
        proxy_pass         http://vaultwarden:80;
        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;
    }

    # WebSocket support for live sync
    location /notifications/hub {
        proxy_pass         http://vaultwarden:3012;
        proxy_http_version 1.1;
        proxy_set_header   Upgrade $http_upgrade;
        proxy_set_header   Connection "upgrade";
    }

    location /notifications/hub/negotiate {
        proxy_pass http://vaultwarden:80;
    }
}
EOF

Step 4 — Obtain a TLS Certificate

Bring up nginx temporarily on port 80 only (comment out the HTTPS server block or use the HTTP-only config), then run Certbot standalone — or use the webroot method if nginx is already running.

# Install Certbot on Debian/Ubuntu
sudo apt install -y certbot

# Install Certbot on Fedora/RHEL
sudo dnf install -y certbot

# Install Certbot on Arch
sudo pacman -S certbot
sudo certbot certonly --standalone \
  -d vault.example.com \
  --email [email protected] \
  --agree-tos --no-eff-email

Certbot writes certificates to /etc/letsencrypt/live/vault.example.com/. The nginx container mounts that path read-only, so renewals are picked up automatically. Add a systemd timer or cron job for renewal:

sudo systemctl enable --now certbot.timer   # Debian/Ubuntu package creates this
# or manually:
sudo crontab -e
# add: 0 3 * * * certbot renew --quiet && docker exec nginx-vw nginx -s reload

Step 5 — Start the Stack

cd /opt/vaultwarden
docker compose up -d
docker compose logs -f   # watch for errors, Ctrl-C when stable

Navigate to https://vault.example.com — you should see the Vaultwarden login page. Create your account now while SIGNUPS_ALLOWED is still true. Then immediately edit docker-compose.yml, set it to false, and redeploy:

docker compose up -d vaultwarden

Step 6 — Access the Admin Panel

Visit https://vault.example.com/admin and enter the passphrase you chose in Step 1 (not the hash). The admin panel lets you invite users, configure 2FA policy, review diagnostics, and send test emails. Critically, confirm the SMTP test email works here before inviting other users — Bitwarden clients require email verification for some operations.

Step 7 — Automate Backups

Vaultwarden stores everything — the encrypted vault database, attachments, and RSA keys — under the vw-data volume, which maps to /var/lib/docker/volumes/vaultwarden_vw-data/_data by default. Back up that directory while the container is running; SQLite is safe to copy live because Vaultwarden uses WAL mode, but a brief stop is safer for a consistent snapshot.

cat > /opt/vaultwarden/backup.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
BACKUP_DIR="/opt/vaultwarden/backups"
DATE=$(date +%Y%m%d-%H%M%S)
mkdir -p "$BACKUP_DIR"

docker compose -f /opt/vaultwarden/docker-compose.yml stop vaultwarden
sqlite3 /var/lib/docker/volumes/vaultwarden_vw-data/_data/db.sqlite3 \
  ".backup '$BACKUP_DIR/db-$DATE.sqlite3'"
docker compose -f /opt/vaultwarden/docker-compose.yml start vaultwarden

# Archive attachments and sends
tar -czf "$BACKUP_DIR/attachments-$DATE.tar.gz" \
  /var/lib/docker/volumes/vaultwarden_vw-data/_data/attachments \
  /var/lib/docker/volumes/vaultwarden_vw-data/_data/sends 2>/dev/null || true

# Keep 30 days of backups
find "$BACKUP_DIR" -mtime +30 -delete
EOF
chmod +x /opt/vaultwarden/backup.sh
# Schedule nightly at 02:30
(crontab -l 2>/dev/null; echo "30 2 * * * /opt/vaultwarden/backup.sh >> /var/log/vw-backup.log 2>&1") | crontab -

Offload backups to a remote location (S3, rsync to a second server, etc.). A backup that lives on the same machine as the data is not a backup.

Step 8 — Connect Clients

All official Bitwarden clients support custom server URLs. The procedure is the same across apps:

  1. Open the Bitwarden app (browser extension, desktop, or mobile).
  2. On the login screen, click or tap the gear / region selector.
  3. Set Server URL to https://vault.example.com.
  4. Log in with the account you created in Step 5.

The browser extension is available for Chrome/Chromium, Firefox, and other browsers. The desktop client works on Linux under both X11 and Wayland. On mobile, install the official Bitwarden app from F-Droid, Google Play, or the App Store — it will talk to your server, not Bitwarden's cloud.

Verification

# Check containers are running
docker compose ps

# Check Vaultwarden version and health
curl -s https://vault.example.com/alive

# Confirm WebSocket endpoint
curl -s -o /dev/null -w "%{http_code}" \
  -H "Connection: Upgrade" -H "Upgrade: websocket" \
  https://vault.example.com/notifications/hub

The /alive endpoint returns Ok. The WebSocket endpoint returns 400 from a plain curl, which is correct — it means nginx is proxying it rather than blocking it.

Troubleshooting

"Invalid admin token" at /admin

You likely pasted the hash with whitespace, or used a plain-text token instead of a bcrypt hash. Re-run the docker run ... hash command, copy the output carefully, and make sure the entire $2y$... string including dollar signs is inside double quotes in the Compose file.

WebSocket notifications not working (vault doesn't sync live)

Double-check the /notifications/hub and /notifications/hub/negotiate location blocks in nginx. Omitting the proxy_http_version 1.1 and Upgrade headers is the most common mistake. Check nginx logs with docker compose logs nginx-vw.

Certificate renewal breaks nginx

After certbot renew the nginx container needs a reload to pick up the new cert. Add && docker exec nginx-vw nginx -s reload to your renewal cron job or deploy hook.

Clients show "cannot connect to server"

Verify that port 443 is open in your firewall. On nftables/ufw/firewalld:

# ufw
sudo ufw allow 443/tcp

# firewalld
sudo firewall-cmd --permanent --add-service=https && sudo firewall-cmd --reload
tested on:Ubuntu 24.04Debian 12Fedora 40Arch rolling

Frequently asked questions

Is Vaultwarden safe to use for real passwords?
Yes, with caveats. Vaultwarden uses the same client-side encryption as the official Bitwarden service, so your passwords are encrypted before they leave your device. The risk is operational: you are responsible for keeping the server patched, backed up, and properly secured. Run it behind TLS, restrict the admin endpoint, and keep Docker images updated.
Can I migrate my existing Bitwarden cloud vault to Vaultwarden?
Yes. In the official Bitwarden web vault or app, go to Tools → Export Vault (encrypted JSON format is safest). Log into your Vaultwarden instance and import that file via Tools → Import Data. Attachments must be re-uploaded manually.
Why use nginx instead of Caddy or Traefik?
This guide uses nginx because it is ubiquitous and its configuration is easy to audit. Caddy is a popular alternative that handles TLS automatically without a separate certbot step. Traefik suits setups with many containers. The Vaultwarden wiki has example configs for all three.
Do I need to expose port 3012 publicly for WebSockets?
No. Nginx proxies WebSocket traffic from port 443 to the container's internal port 3012. Port 3012 should not be published to the host or opened in your firewall — the proxy_pass in the nginx config handles the internal routing.
How do I update Vaultwarden?
Pull the new image and recreate the container. Run: docker compose pull && docker compose up -d. Vaultwarden applies any database migrations automatically on startup. Check the GitHub releases page for breaking changes before upgrading major versions.

Related guides