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:
sudo ufw allow 80/tcp
sudo ufw allow 443/tcpHTTPS 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:
sudo docker run --rm -it vaultwarden/server:1.37.4 /vaultwarden hashType a long passphrase twice (keep it in your password manager once it exists). It prints a line like:
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
sudo mkdir -p /opt/vaultwarden
cd /opt/vaultwardenCreate /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:
DOMAIN=https://vault.example.com
ADMIN_TOKEN='$argon2id$v=19$m=65540,t=3,p=4$...'
SIGNUPS_ALLOWED=truesudo chmod 600 .envCreate /opt/vaultwarden/compose.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:DOMAINis the full public address. Vaultwarden uses it for links, for WebAuthn (passkeys and security keys) and for attachments; a wrong value breaks those.SIGNUPS_ALLOWEDstarts astrueso you can create your own accounts, and defaults tofalseif 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:80keeps the vault off the internet except through Caddy. Docker's published ports bypass ufw, so never publish it as"8080:80".
Start it:
sudo docker compose up -dCheck it worked
sudo docker compose ps
curl -s http://127.0.0.1:8080/aliveThe container becomes (healthy), and /alive prints the current time in quotes.
Put Caddy in front for HTTPS
Install Caddy from its repository:
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 caddyReplace /etc/caddy/Caddyfile with:
vault.example.com {
encode zstd gzip
reverse_proxy 127.0.0.1:8080
}sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddyCaddy 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
curl -I https://vault.example.comIt 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
In the web vault, choose Create account for yourself and anyone else who should have one.
Turn sign-ups off. Change the line in
.envtoSIGNUPS_ALLOWED=false, then recreate the container so it reads the new value:cd /opt/vaultwarden sudo docker compose up -dTurn 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:
cd /opt/vaultwarden
sudo docker compose exec vaultwarden /vaultwarden backupIt prints Backup to 'data/db_20261021_231305.sqlite3' was successful. Then archive the volume without the live database files, and remove the snapshot:
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:
cd /opt/vaultwarden
sudo docker compose pull
sudo docker compose up -d
sudo docker image prune -fTroubleshooting
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
- Check the vault from outside with Uptime Kuma.
- Harden the server's own SSH with the SSH hardening checklist.
- Read Vaultwarden's wiki, especially the pages on hardening and backups.