Vaultwarden is a lightweight, open-source, self-hosted password manager - a Rust reimplementation of the Bitwarden server API. You run the server; you connect with the official Bitwarden phone apps and browser extensions. Your vault never leaves a box you own.
This deploys it behind Traefik, which handles TLS and routing automatically.
docker-compose.yml
# /srv/vaultwarden.example.com/docker-compose.yml
name: vaultwarden-example-com
services:
vaultwarden:
image: vaultwarden/server:latest
container_name: vault.example.com
restart: always
mem_limit: 256M
environment:
- SIGNUPS_ALLOWED=false
- DOMAIN=https://vaultwarden.example.com
env_file:
- ./admin.env # holds ADMIN_TOKEN - see below (keeps the $ safe)
volumes:
- ./data:/data
labels:
- com.centurylinklabs.watchtower.enable=true
- traefik.enable=true
- traefik.http.routers.vaultwarden.rule=Host(`vaultwarden.example.com`)
- traefik.http.routers.vaultwarden.entrypoints=https
- traefik.http.routers.vaultwarden.tls=true
- traefik.http.routers.vaultwarden.tls.certresolver=lets-encrypt
# Vaultwarden listens on :80 inside the container - tell Traefik explicitly
- traefik.http.services.vaultwarden.loadbalancer.server.port=80Two changes from a bare setup, both intentional:
loadbalancer.server.port=80- withexposedByDefault: false, Traefik needs the port spelled out or the route silently 502s.entrypoints=https- pins the router to the TLS entrypoint (the HTTP one just redirects).
Securing the admin page (the ADMIN_TOKEN)
The admin panel lives at /admin. Never ship ADMIN_TOKEN=TODO_CHANGE_THIS -
that’s a full-control backdoor. Vaultwarden accepts a plaintext token, but the
recommended form is an Argon2 hash so the secret isn’t stored in plaintext:
# needs the `argon2` CLI (e.g. `apt install argon2`)
echo -n 'a-long-random-admin-password' | argon2 "$(openssl rand -base64 32)" -e -id -k 65540 -t 3 -p 4
# → $argon2id$v=19$m=65540,t=3,p=4$....$....Put the result in ./admin.env (not inline in environment:):
# admin.env - chmod 600, and gitignore it
ADMIN_TOKEN=$argon2id$v=19$m=65540,t=3,p=4$....$....Gotcha: the hash is full of
$. Docker Compose interpolates$in theenvironment:block, so an inline token gets mangled - you’d have to double every$to$$. Usingenv_file:sidesteps that entirely (its values are passed literally). That’s why the compose above reads fromadmin.env.
Prefer not to run an admin page at all? Set DISABLE_ADMIN_TOKEN=true only if you
also lock /admin some other way (e.g. Traefik Basic Auth or
forward-auth).
First run
docker compose up -d, then openhttps://vaultwarden.example.com.- Because
SIGNUPS_ALLOWED=false, you can’t register from the front page - go to/admin, log in with your admin token, and invite your own account. - Create your user, sign in from the Bitwarden app/extension pointed at your
DOMAIN, then confirm the invite from/admin.
Keeping signups off means only you (via the admin page) decide who gets an account.
Back it up
Everything lives in ./data - the SQLite database, attachments, and keys. That
directory is your vault. Back it up off-box regularly:
# simple nightly copy of the whole data dir
tar czf "vaultwarden-$(date +%F).tgz" ./dataFor a hot database, prefer sqlite3 ./data/db.sqlite3 ".backup '/backup/db.sqlite3'"
so you copy a consistent snapshot rather than a file mid-write.