Skip to content

Vaultwarden

Vaultwarden is a self-hosted password manager compatible with Bitwarden clients — browser extensions, mobile apps, and the desktop client. It can hold access credentials to everything else you run; treat setup and backup accordingly.

Last run on a host 9 Nov 2026 · data restored from a backup

Pinned at 1.37.5 (released 1 Nov 2026) · the newest upstream release

In the upstream images: 1 CRITICAL · 28 HIGH with a published fix — per image

To try it: Docker, nothing else. See Try it locally.

To deploy it behind the proxy: Traefik running, a domain, and SMTP credentials (email verification is on by default).

On your own machine, with no proxy, domain or certificate. From apps/vaultwarden/:

Terminal window
cp .env.local.example .env.local
# The secrets ship empty; generate the ones this stack needs
{
echo "DB_PASSWORD=$(openssl rand -hex 32)"
echo "DB_ROOT_PASSWORD=$(openssl rand -hex 32)"
} >> .env.local
docker compose -f docker-compose.local.yml --env-file .env.local up -d

Then open http://localhost:8080. Stop it again with the same command and down.

From apps/vaultwarden/:

  1. Terminal window
    cp .env.example .env
  2. Set your domain in .env:

    APP_TRAEFIK_HOST=vault.example.com
  3. Set SMTP — required before the first account can verify its email:

    VW_SMTP_HOST=smtp.example.com
    VW_SMTP_FROM[email protected]
    VW_SMTP_PORT=587
    VW_SMTP_SECURITY=starttls
    VW_SMTP_USERNAME=your-smtp-user
    VW_SMTP_PASSWORD=your-smtp-password
  4. Generate database passwords. Hex is a convention here, not a requirement: the password ends up inside a connection URL, and hex avoids having to URL-encode characters that would otherwise break it.

    Terminal window
    sed -i "s|^DB_PASSWORD=.*|DB_PASSWORD=$(openssl rand -hex 32)|" .env
    sed -i "s|^DB_ROOT_PASSWORD=.*|DB_ROOT_PASSWORD=$(openssl rand -hex 32)|" .env
  5. Optional: the admin panel. VW_ADMIN_TOKEN ships empty, which keeps the panel at /admin off. Set it only when you need the panel, and never to a plain word — Vaultwarden accepts a plain-text token as it stands, and a plain token in .env is readable by anything that can read the file or inspect the container. Generate a hash instead:

    Terminal window
    docker compose run --rm --no-deps vaultwarden-app /vaultwarden hash

    Copy the full $argon2id$... output, then paste it into .env with every $ doubled (Compose escaping):

    VW_ADMIN_TOKEN=$$argon2id$$v=19$$m=65540,t=3,p=4$$...your-hash...

    This stack has no Docker Secrets. Every credential it uses is read from .env and passed to the containers as an environment variable, including the database passwords, the admin token, and the SMTP and push values once those are set. Anything passed that way appears in docker inspect and in docker compose config — treat both outputs as credential material and redact them before pasting either anywhere, and keep .env as tightly permissioned as a secrets directory.

  6. Allow signups for your first account — you’ll turn this off again right after:

    VW_SIGNUPS_ALLOWED=true
  7. Start:

    Terminal window
    docker compose up -d
    docker compose logs -f

    Wait for Starting Vaultwarden with no errors — MariaDB has a health check Vaultwarden waits on.

Terminal window
docker compose ps # both containers healthy
curl -sI https://your-domain/alive # expect 200 OK

Open https://your-domain — the login page should load over TLS.

  1. Click Create Account, register, and verify your email.

  2. In .env, turn signups back off:

    VW_SIGNUPS_ALLOWED=false

    Recreate the app container so it loads the changed environment:

    Terminal window
    docker compose up -d --force-recreate vaultwarden-app

    Confirm it took effect against the container rather than against the page:

    Terminal window
    docker compose exec vaultwarden-app env | grep SIGNUPS_ALLOWED # expect false

    Then reload the login page — “Create Account” should be gone.

  3. With the admin panel switched on (installation step 5), set at https://your-domain/admin:

    Setting Value
    Allow new signups false
    Allow password hints false
    Block non-global IPs true
    Disable 2FA remember true
  4. Enable 2FA on your account: Settings → Security → Two-step login.

The installation above uses the default, APP_TRAEFIK_ACCESS=access-vpn — the vault is reachable only over Tailscale/VPN. Setting APP_TRAEFIK_ACCESS=access-public makes it reachable from the open internet instead. That’s a deliberate change to your attack surface, not a default — decide it before exposing real credentials.

Do not add real credentials before a backup exists.

What to back up: volumes/mysql/ (database), volumes/data/attachments/, volumes/data/rsa_key.* (signing keys — without these, sessions break on restore even with an intact database), volumes/data/sends/.

Terminal window
docker exec -e MYSQL_PWD="$(grep DB_ROOT_PASSWORD .env | cut -d= -f2)" \
vaultwarden-db mariadb-dump -u root vaultwarden > backup-$(date +%Y%m%d).sql

The command above is a one-off dump. To have it run on a schedule alongside the data directories, and to hold the archives somewhere the server cannot reach, see backup and restore — Borgmatic on the host, with the database dump wired in as a pre-backup hook.

The three parts have to be restored together, from the same point in time. The database alone is not enough: without rsa_key.* every session breaks even though the vault contents are intact, and without the attachments directory entries lose their files.

  1. Check the release notes for breaking changes.
  2. Back up all three parts together — the database dump, volumes/data/rsa_key.* and volumes/data/attachments/. A database-only snapshot is not something you can roll back to: restoring it without the matching signing keys breaks every session even though the vault contents are intact.
  3. Update APP_TAG in .env.
  4. Terminal window
    docker compose pull
    docker compose up -d
  5. Verify /alive and log in.

Container not starting: check docker compose logs vaultwarden-app and docker compose logs db. Common causes: MariaDB not yet healthy (the app waits for it), or incorrect $$ escaping in VW_ADMIN_TOKEN.

Login page not loading but the container is healthy: check Traefik logs and confirm APP_TRAEFIK_HOST matches your DNS record exactly.

Email verification not arriving: recreate the app after changing the SMTP values — docker compose up -d --force-recreate vaultwarden-app — and confirm the container has them:

Terminal window
docker compose exec vaultwarden-app sh -c 'env | grep -E "^SMTP_HOST=|^SMTP_FROM=|^SMTP_PORT=|^SMTP_SECURITY=|^SMTP_USERNAME="'
docker compose logs vaultwarden-app --tail=200 | grep -iE "smtp|mail|error|lettre"

With the admin panel on, Admin → Users → Invite User is an end-to-end test. Mail that lands in junk points at the sender domain’s SPF, DKIM and DMARC records.

Push notifications do not sync: Unexpected push token received from bitwarden server: error decoding response body means EU credentials (a bitwarden.eu registration) without the EU relay. Set VW_PUSH_RELAY_URI=https://api.bitwarden.eu and VW_PUSH_IDENTITY_URI=https://identity.bitwarden.eu in .env and recreate the container.

Admin → Diagnostics reports Header 'x-frame-options' is present: the proxy is setting the frame header Vaultwarden controls per route. Confirm .env still has APP_TRAEFIK_HEADERS=headers-application-owned.

Admin → Diagnostics reports HTTP Response validation: Error: the address in use does not match DOMAIN, which the compose file sets to https://${APP_TRAEFIK_HOST}, or HTTPS is not enforced end to end.

/admin fails in the normal browser but works in a private window: clear the site data and check browser extensions before suspecting the server.