Self-host Vaultwarden safely with Docker and HTTPS

Run Vaultwarden, the Bitwarden-compatible password server, in Docker behind Caddy with HTTPS, sign-ups closed, a hashed admin token and backups you can restore.

Tested on Ubuntu 24.04 LTS

Before you start

  • A server with Docker Engine and the Compose plugin
  • A domain or subdomain for the vault
  • Root or a user with sudo
  • SSH access from your computer

To self-host Vaultwarden, run the vaultwarden/server image with Docker Compose, keep its port on 127.0.0.1, and put Caddy in front so the vault is only reachable over HTTPS. Create your accounts, then turn sign-ups off, protect the admin page with an Argon2 hashed token, and back up the database with Vaultwarden's own backup command every night.

Vaultwarden is an unofficial, community-written server that speaks the Bitwarden API, so the official Bitwarden apps, browser extensions and CLI work with it. It is not made or supported by Bitwarden, Inc. It is light enough for the smallest server, and it holds every password you have, so the steps below are about keeping it private and recoverable.

What In this tutorial
The domain vault.example.com
The server's public IPv4 address 203.0.113.10
The project directory /opt/vaultwarden
Version Vaultwarden 1.37.4

Decide before you start

Self-hosting a password manager makes you responsible for three things a hosted service does for you:

  • Availability. Bitwarden apps keep an encrypted copy of your vault and work offline, so a short outage is not a disaster. But you can't add a new device or share an item while the server is down.
  • Updates. Security fixes only reach you when you pull them. Watch the releases.
  • Backups. If the server and its only copy are lost, the vault is gone. Your apps' cached copies are not a backup you can restore.

Vault items are encrypted on your devices with your master password before they reach the server, so the server never sees them in plain text. Use a strong master password and two-step login all the same.

Point the domain and open the web ports

Add an A record (and AAAA, if the server has IPv6) for vault.example.com, then:

bash
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

HTTPS is required, not just recommended. The web vault does its encryption with the browser's Web Crypto API, which browsers only allow on https:// pages (and localhost), so over plain HTTP the web vault fails to create an account or log in.

Make the admin token

Vaultwarden's admin page (/admin) can change every setting, list users and delete them. Protect it with a token stored as an Argon2id hash, not as plain text. Generate the hash with Vaultwarden itself:

bash
sudo docker run --rm -it vaultwarden/server:1.37.4 /vaultwarden hash

Type a long passphrase twice (keep it in your password manager once it exists). It prints a line like:

text
ADMIN_TOKEN='$argon2id$v=19$m=65540,t=3,p=4$...'

You log in to /admin with the passphrase; the server only stores the hash.

Write the Compose file

bash
sudo mkdir -p /opt/vaultwarden
cd /opt/vaultwarden

Create /opt/vaultwarden/.env with your domain and the line hash printed, single quotes included. The quotes stop Compose from treating the $ signs in the hash as variables:

bash
DOMAIN=https://vault.example.com
ADMIN_TOKEN='$argon2id$v=19$m=65540,t=3,p=4$...'
SIGNUPS_ALLOWED=true
bash
sudo chmod 600 .env

Create /opt/vaultwarden/compose.yaml:

yaml
services:
  vaultwarden:
    image: vaultwarden/server:1.37.4
    restart: unless-stopped
    environment:
      DOMAIN: ${DOMAIN:?set DOMAIN in .env}
      ADMIN_TOKEN: ${ADMIN_TOKEN}
      SIGNUPS_ALLOWED: ${SIGNUPS_ALLOWED:-false}
      INVITATIONS_ALLOWED: "false"
      SHOW_PASSWORD_HINT: "false"
    volumes:
      - vw_data:/data
    ports:
      - "127.0.0.1:8080:80"

volumes:
  vw_data:
  • DOMAIN is the full public address. Vaultwarden uses it for links, for WebAuthn (passkeys and security keys) and for attachments; a wrong value breaks those.
  • SIGNUPS_ALLOWED starts as true so you can create your own accounts, and defaults to false if the line is missing.
  • INVITATIONS_ALLOWED: "false" stops organization owners from inviting new users, which would otherwise let anyone with an organization add accounts while sign-ups are off. Set it to "true" if you want invitations, which need email (SMTP) settings.
  • SHOW_PASSWORD_HINT: "false" stops the login page from showing hints to whoever asks.
  • 127.0.0.1:8080:80 keeps the vault off the internet except through Caddy. Docker's published ports bypass ufw, so never publish it as "8080:80".

Start it:

bash
sudo docker compose up -d

Check it worked

bash
sudo docker compose ps
curl -s http://127.0.0.1:8080/alive

The container becomes (healthy), and /alive prints the current time in quotes.

Put Caddy in front for HTTPS

Install Caddy from its repository:

bash
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install -y caddy

Replace /etc/caddy/Caddyfile with:

text
vault.example.com {
	encode zstd gzip
	reverse_proxy 127.0.0.1:8080
}
bash
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy

Caddy gets the certificate and passes the websocket connection that live sync uses; current Vaultwarden versions serve it on the same port, so nothing else is needed.

Check it worked

bash
curl -I https://vault.example.com

It answers HTTP/2 200. Open the address in a browser and you see the web vault's login page.

Create your accounts, then close sign-ups

  1. In the web vault, choose Create account for yourself and anyone else who should have one.

  2. Turn sign-ups off. Change the line in .env to SIGNUPS_ALLOWED=false, then recreate the container so it reads the new value:

    bash
    cd /opt/vaultwarden
    sudo docker compose up -d
  3. Turn on two-step login for every account: Settings, Security, Two-step login.

Check it worked

Try to create another account in a private browser window. Vaultwarden refuses with Registration not allowed or user already exists.

To add someone later, set SIGNUPS_ALLOWED=true, run sudo docker compose up -d, let them register, and set it back.

Use the admin page carefully

Open https://vault.example.com/admin and log in with the passphrase you hashed. Settings you save there are written to config.json in the data volume, and they take priority over the environment variables. If a change to .env seems to have no effect, check whether the same setting was saved on the admin page.

If you don't need the admin page, remove the ADMIN_TOKEN line from .env and run sudo docker compose up -d: without a token, /admin is turned off.

Back it up

The data volume holds the SQLite database, rsa_key.pem (signs login tokens), and folders for attachments and Sends if you use them. Copying db.sqlite3 while Vaultwarden runs can give you a broken copy, so let Vaultwarden write a consistent snapshot first:

bash
cd /opt/vaultwarden
sudo docker compose exec vaultwarden /vaultwarden backup

It prints Backup to 'data/db_20261021_231305.sqlite3' was successful. Then archive the volume without the live database files, and remove the snapshot:

bash
sudo docker run --rm -v vaultwarden_vw_data:/data -v /root:/backup alpine sh -c \
  "tar czf /backup/vaultwarden-\$(date +%F).tgz -C /data --exclude=./db.sqlite3 --exclude=./db.sqlite3-shm --exclude=./db.sqlite3-wal --exclude=./tmp . && rm /data/db_*.sqlite3"

Copy the archive off the server every night, encrypted if it goes to storage you don't control (restic encrypts by default). To restore, unpack it into an empty volume and rename the db_*.sqlite3 file to db.sqlite3 before starting the container.

Also export your own vault now and then (Tools, Export vault, encrypted JSON): it doesn't depend on the server at all.

Update

Read the release notes, back up, change the tag in compose.yaml, then:

bash
cd /opt/vaultwarden
sudo docker compose pull
sudo docker compose up -d
sudo docker image prune -f

Troubleshooting

The web vault shows an error about the Web Crypto API or crypto.subtle, or account creation does nothing. You opened it over plain HTTP. Use https://vault.example.com.

Caddy logs could not get certificate from issuer. DNS doesn't point at this server yet, or port 80 or 443 is blocked. Check getent hosts vault.example.com and sudo ufw status, then sudo systemctl reload caddy.

Registration not allowed or user already exists when you create your own first account. SIGNUPS_ALLOWED isn't true, or the container wasn't recreated after the change. Run sudo docker compose up -d after editing .env, and check that the admin page hasn't saved the opposite value.

The admin page refuses the token, or the log warns about a plain text ADMIN_TOKEN. The hash lost its $ signs: keep the single quotes around it in .env. You type the passphrase on the admin page, not the hash.

Passkeys, security keys or attachment links fail. DOMAIN doesn't match the address in the browser, including https://.

Next steps

Be first when orders open

Join the waitlist and we'll send you one email when VPSNine launches. Pick a plan if you already know which one you want, and we'll size the first servers around it.

  • One email, on launch day
  • Monthly or yearly, no lock-in
  • 14-day money-back guarantee