Self-HostingDocker

Vaultwarden with Docker: a lightweight Bitwarden password server with HTTPS (Linux, Windows, Mac)

Before you start

  • 15 min read
  • 12 to run in terminal
  • 5 file contents

Boxes marked Run in terminal are what you type; plain text explains why. Check what you need first

What you get in one minute#

Vaultwarden is a small, community-made server that speaks the Bitwarden protocol, so the normal Bitwarden apps and browser extensions work against your own machine.

It is written in Rust and runs happily on a modest home server. People use it to keep their passwords, passkeys, notes, attachments and shared family vaults at home instead of in someone else's cloud. This guide sets it up the careful way: a pinned image, HTTPS in front, a hashed admin token, signups switched off after your first account, and a backup you test by restoring it.

One thing to know up front: Vaultwarden is not made, endorsed or supported by Bitwarden, Inc. It is an independent project.

WhatValue
Current version1.37.4, released 2026-10-05 (GitHub releases)
Pinned imagevaultwarden/server:1.37.4 (the tag exists on Docker Hub)
Time neededAbout 20 minutes if Traefik already runs
Data~/docker_volumes/vaultwarden/data (SQLite file, attachments, keys)
Port inside the container80 (Traefik talks to it; nothing is published on the host)
Official docsVaultwarden wiki

Which version, and why. 1.37.4 is a security release. It fixes seven advisories, one of them rated High (organization member revocation), and the maintainers say to update soon. I looked for reports that it fails to install or loses data and found none. I did find a few user reports of mobile and Android sync errors on 1.37.4, tied to organization members who accepted an invite but were never confirmed (VERIFY on the issue tracker before you upgrade). That is a narrow case, so this guide uses 1.37.4. If you hit exactly that problem on an existing server, 1.37.3 is a short-term fallback; confirm or remove the pending members first.

How it fits together#

Your Bitwarden apps talk HTTPS to Traefik, Traefik passes plain HTTP to the Vaultwarden container on the private Docker network, and Vaultwarden keeps everything in one data folder on disk.

How Vaultwarden is wired: Bitwarden apps connect over HTTPS to Traefik, Traefik forwards plain HTTP on the internal Docker network to the Vaultwarden container, which reads and writes the data folder with the SQLite database, attachments and RSA keys; a nightly backup copy leaves the host

The container publishes no port on your host. The only way in is through Traefik, which is also the only thing that needs a certificate.

Before you start#

You need Docker, a name for your vault such as vault.example.com, and a Traefik that already works.

SystemWhat you need
LinuxDocker Engine with the Compose plugin, bash. Optional: sqlite3 on the host for the backup command
WindowsDocker Desktop in Linux-container mode (WSL2 backend), PowerShell 7
MacDocker Desktop for Mac, Terminal

Official sizing numbers for Vaultwarden are not published; it is meant for small machines (VERIFY Docker Desktop's own minimums on docs.docker.com). A Linux box that is always on is the best home for a password server. Docker Desktop on a laptop only works while the laptop is awake.

Check Docker first:

Run in terminalbash
docker --version
docker compose version

Checklist:

  • [ ] A Traefik instance with an HTTPS entrypoint and a certificate resolver already runs (see the Traefik step below).
  • [ ] A DNS name points to the machine (an internal name is fine and safer, see "Security and testing").
  • [ ] You have somewhere separate from this machine to keep backups.
  • [ ] You will not alternate between Windows and WSL folders. Pick one and stay there, or you will end up with two different vaults.

Tested on: not run by DIY Lab Hub for this guide. Linux, Windows and Mac commands were checked against the official Vaultwarden wiki on 2026-10-26.

Step 1: Create the folder and the admin hash#

The admin page is protected by a password. Store only its argon2 hash, never the password.

Vaultwarden has a built-in tool for that: /vaultwarden hash. It asks for a password twice and prints a string starting with $argon2id$. You log in to /admin later with the password you typed, not with the hash.

Linux#

Run in terminalbash
umask 077
mkdir -p ~/docker_volumes/vaultwarden/data
cd ~/docker_volumes/vaultwarden
docker pull vaultwarden/server:1.37.4
docker run --rm -it vaultwarden/server:1.37.4 /vaultwarden hash

Windows (PowerShell)#

Codepowershell
New-Item -ItemType Directory -Force "$HOME\docker_volumes\vaultwarden\data" | Out-Null
Set-Location "$HOME\docker_volumes\vaultwarden"
docker pull vaultwarden/server:1.37.4
docker run --rm -it vaultwarden/server:1.37.4 /vaultwarden hash

Mac (Terminal)#

Same commands as Linux:

Run in terminalbash
umask 077
mkdir -p ~/docker_volumes/vaultwarden/data
cd ~/docker_volumes/vaultwarden
docker pull vaultwarden/server:1.37.4
docker run --rm -it vaultwarden/server:1.37.4 /vaultwarden hash

The default settings match Bitwarden's own (64 MiB memory, 3 passes, 4 threads). Add --preset owasp after hash for the lighter OWASP minimum. Both are fine for a home server.

Check it worked: the last line of output starts with $argon2id$v=19$m=. Copy it.

Step 2: Write the .env file and the compose file#

Put the secrets in .env and keep the compose file clean.

Create .env in the same folder. Use single quotes around the hash and single dollar signs. Docker Compose does not touch dollar signs inside a quoted .env value.

File contenttext
VW_VERSION=1.37.4
VW_HOST=vault.example.com
VW_SIGNUPS_ALLOWED=true
VAULTWARDEN_ADMIN_TOKEN='<PASTE_THE_WHOLE_ARGON2ID_STRING_HERE>'

Now docker-compose.yml. There is no version: line, because Compose no longer needs it. The names proxy, websecure and letsencrypt are examples; use the ones from your own Traefik.

File contentyaml
services:
  vaultwarden:
    image: vaultwarden/server:${VW_VERSION:?set VW_VERSION in .env}
    container_name: vaultwarden
    restart: unless-stopped
    environment:
      DOMAIN: "https://${VW_HOST:?set VW_HOST in .env}"
      ADMIN_TOKEN: "${VAULTWARDEN_ADMIN_TOKEN:?set the argon2 hash in .env}"
      SIGNUPS_ALLOWED: "${VW_SIGNUPS_ALLOWED:-false}"
      LOG_FILE: "/data/vaultwarden.log"
    volumes:
      - ./data:/data
    networks:
      - proxy
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=proxy"
      - "traefik.http.routers.vaultwarden.rule=Host(`${VW_HOST}`)"
      - "traefik.http.routers.vaultwarden.entrypoints=websecure"
      - "traefik.http.routers.vaultwarden.tls=true"
      - "traefik.http.routers.vaultwarden.tls.certresolver=letsencrypt"
      - "traefik.http.services.vaultwarden.loadbalancer.server.port=80"

networks:
  proxy:
    external: true

Why it looks like this:

  • DOMAIN must be your real HTTPS address. Vaultwarden needs it for links, attachments and invitations.
  • ./data is the data folder you made in step 1, relative to the compose file. That works the same on all three systems.
  • LOG_FILE lets fail2ban read failed logins later.
  • If you ever paste the hash straight into the YAML instead of .env, every $ must be written $$. Do not mix the two ways.

Linux#

Run in terminalbash
chmod 700 ~/docker_volumes/vaultwarden
chmod 600 ~/docker_volumes/vaultwarden/.env

Windows (PowerShell)#

Windows has no chmod. Keep the folder inside your own user profile and do not share it. (VERIFY: tighten the folder's ACL to your account if other people use the PC.)

Codepowershell
Get-Item "$HOME\docker_volumes\vaultwarden\.env" | Select-Object FullName

Mac (Terminal)#

Same commands as Linux:

Run in terminalbash
chmod 700 ~/docker_volumes/vaultwarden
chmod 600 ~/docker_volumes/vaultwarden/.env

Put it behind HTTPS with Traefik#

The web vault will not work over plain HTTP. Browsers only allow the WebCrypto functions it needs on a secure (HTTPS) page, so HTTPS is not optional.

This section is for a server with a domain name. The labels above already do the work, but they assume Traefik is running with an HTTPS entrypoint and a certificate resolver. The labels do not install Traefik or fetch a certificate by themselves. The official wiki's own Traefik example makes the same assumptions (it is labelled Traefik v2 and was last edited in January 2025, so check it against your version).

If the shared network does not exist yet:

Run in terminalbash
docker network create proxy

Windows and Mac use the same command. Then start the vault:

Run in terminalbash
docker compose config --quiet
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail=50 vaultwarden

Avoid pasting the full output of docker compose config anywhere. It shows your resolved secrets.

Check it worked: docker compose ps shows vaultwarden as running, and https://vault.example.com opens the login page with a trusted padlock.

Local-only people can skip Traefik, but a browser vault still needs HTTPS. The one exception is localhost, which browsers treat as secure (VERIFY in your browser).

First-run setup#

Create your account first, then lock the door behind you.

  1. Open your vault address. Make sure the certificate is trusted, with no warning to click through.
  2. Click Create account. Use a strong master password that is different from the admin password.
  3. Edit .env and set VW_SIGNUPS_ALLOWED=false, then run docker compose up -d to recreate the container.
  4. Open the page again and confirm that strangers can no longer register.
  5. Open /admin and log in with the password you typed in step 1. Look at the diagnostics page: it checks HTTPS, the domain and the clock.
  6. In each Bitwarden app, choose "self-hosted" on the login screen and enter your exact vault address.
  7. Turn on two-step login for your account and keep the recovery code somewhere that is not this server.

Two things trip people up. If you save settings in the admin page, Vaultwarden writes data/config.json, and those values win over your environment variables from then on. If a change in .env seems to do nothing, look there first. And turning signups off does not stop you inviting people: the admin page can still invite users.

Invitations, email and notifications#

Email is optional, but invitations and password hints are much nicer with it.

Add SMTP settings under environment: (put the real values in .env, not in the compose file):

File contentyaml
      SMTP_HOST: "${SMTP_HOST}"
      SMTP_FROM: "${SMTP_FROM}"
      SMTP_PORT: "587"
      SMTP_SECURITY: "starttls"
      SMTP_USERNAME: "${SMTP_USERNAME}"
      SMTP_PASSWORD: "${SMTP_PASSWORD}"

For port 465 use SMTP_SECURITY: "force_tls". The admin page has a "send test email" button; click Save first. Some mail providers only allow OAuth2 logins, which this plain setup cannot use.

Live sync in browsers and desktop apps uses WebSockets, and since version 1.31.0 they run on the main port. Delete any old WEBSOCKET_ENABLED, WEBSOCKET_PORT or 3012 lines you find in older guides. Traefik passes WebSocket upgrades on its own. To test, open the browser's network tab and look for status 101 on /notifications/hub.

Phone push notifications are separate. They need an installation ID and key from Bitwarden's registration page, set as PUSH_ENABLED, PUSH_INSTALLATION_ID and PUSH_INSTALLATION_KEY (VERIFY the exact names on the wiki page "Enabling Mobile Client push notification"). Only official-store app builds receive pushes. A push does not make a private server reachable from outside your network; the phone still needs a VPN to sync.

Security and testing#

A password server should be boring and hard to reach. Keep it off the public internet if you can.

  • Secrets: only the argon2 hash goes in .env. .env, config.json, the rsa_key* files and your backups never go into Git.
  • Private by design: do not forward ports on your router. Use an internal DNS name, or a VPN, and get a real certificate with DNS validation so the host never has to be reachable from outside. The wiki has a page on running a private instance with Let's Encrypt certificates, but its provider examples are old (last edited March 2024).
  • Admin page: protect /admin at the proxy too (for example, allow it only from your LAN). If you do not need it, remove ADMIN_TOKEN and config.json's admin_token, then recreate the container to switch it off.
  • Two-step login on every account, and encrypted off-host backups.
  • Proxy IPs in 1.37.4: if you set IP_HEADER=X-Forwarded-For, Vaultwarden now uses the rightmost address that is not in IP_HEADER_TRUSTED_PROXIES. With several proxies in a row, list all of them there, or your logs and rate limits will see the proxy instead of the user.
  • Redact tokens in logs: WebSocket URLs can carry a login token in the query string. The wiki's hardening guide says to keep those out of proxy logs.

Fail2ban bans IPs that keep guessing passwords. The official filter looks for this log line (the log is data/vaultwarden.log because of LOG_FILE):

Codetext
Username or password is incorrect. Try again. IP: <ADDR>. Username: ...

Follow the wiki page "Fail2Ban Setup" for the filter and jail. Docker traffic does not always pass the host's normal INPUT chain, so a ban can silently do nothing. Do not trust it until you have made a failed login on purpose, seen the real client IP in the log, and watched the ban work (VERIFY). CrowdSec: I found no official Vaultwarden recipe, so treat it as your own project to test.

Simple checks that everything is healthy:

Run in terminalbash
curl -I https://vault.example.com
docker compose ps
docker compose logs --tail=100 vaultwarden

Troubleshooting#

ProblemSolution
Page works over HTTPS but the vault will not load over plain HTTPExpected. Use HTTPS. WebCrypto needs a secure page
Log says You are using a plain text ADMIN_TOKEN which is insecure.The value is not a valid hash, or data/config.json overrides it. Check docker exec vaultwarden printenv ADMIN_TOKEN (no quotes, no $$) and grep admin_token data/config.json
WARNING: The argon2id variable is not set. Defaulting to a blank string.Compose read the $ signs. Use a single-quoted .env value, or double every $ in the YAML
You can log in to /admin with the whole $argon2id... stringYour Vaultwarden is too old to understand hashes. Update
No status 101 on /notifications/hubThe WebSocket upgrade failed. Check the proxy sends everything to port 80 and remove old port 3012 routing
Failed logins show 127.0.0.1 or the proxy's IPFix the real-IP header and IP_HEADER_TRUSTED_PROXIES, or fail2ban will ban the proxy
Admin settings will not save after an upgrade1.37.4 removed several experimental feature flags. Remove the old entries from EXPERIMENTAL_CLIENT_FEATURE_FLAGS
No compatible authentication mechanism was found when testing emailThe provider wants OAuth2 or a different login. Use an SMTP relay that accepts a username and password
Browser extension logs in but the vault looks emptySettings, Vault options, Sync now. If that fails, reinstall the extension and check the server is new enough for your client version

Cryptography Initialization error (mobile)#

Some users report this exact message, or "endless loading", in the Android and iOS apps on 1.37.4 (VERIFY on the issue tracker). The reports link it to organization members who accepted an invite and were never confirmed. Confirm or remove those pending members first. Do not reset keys or export and re-import as a first reaction. Another report named Invalid asymmetric type, got type 0 with 1 parts for the same pending-member situation.

Known bugs#

An open issue from 2026-10-21 says the web vault's "Assign to collections" or move-to-organization action fails on 1.37.4. If you rely on organizations, read the issue before upgrading.

Diagnostic commands#

Run in terminalbash
docker logs vaultwarden --tail 100
docker compose ps
docker inspect vaultwarden --format '{{.State.Status}}'

Updates and backups#

Pin the tag, back up first, then change one line.

Update#

  1. Read the release notes of the new version.
  2. Make and check a backup (below).
  3. Change VW_VERSION in .env.
  4. Run the commands:
Run in terminalbash
docker compose pull vaultwarden
docker compose up -d vaultwarden
docker compose logs --tail=100 vaultwarden

Test the web vault, the browser extension and a phone. Do not assume an older image can read a database that a newer version already changed. To go back, restore the matching pre-upgrade backup together with the old image tag.

What to back up#

ItemNeeded?
db.sqlite3Yes, and use a proper SQLite backup, not a plain copy of a running database
attachments/Yes
config.jsonRecommended (holds the admin token hash and SMTP login)
rsa_key*Recommended. Without them everyone is logged out and open invitations stop working
sends/Optional
icon_cache/Optional
.env, docker-compose.ymlYes, kept with the backup

Backup script#

The Vaultwarden image has no sqlite3 tool and no cron, so the backup runs on the host. The official command is sqlite3 data/db.sqlite3 ".backup '<file>'". Since 1.32.1 there is also a built-in docker exec vaultwarden /vaultwarden backup (VERIFY where it writes its file on your version).

Linux#

File contentbash
#!/usr/bin/env bash
set -euo pipefail
umask 077
APP="$HOME/docker_volumes/vaultwarden"
OUT="$HOME/vaultwarden_backups/$(date +%Y%m%d-%H%M)"
mkdir -p "$OUT/data"
sqlite3 "$APP/data/db.sqlite3" ".backup '$OUT/data/db.sqlite3'"
for f in attachments sends config.json rsa_key.pem rsa_key.pub.der rsa_key.der; do
  [ -e "$APP/data/$f" ] && cp -Rp "$APP/data/$f" "$OUT/data/"
done
cp "$APP/docker-compose.yml" "$APP/.env" "$OUT/"
sqlite3 "$OUT/data/db.sqlite3" "PRAGMA integrity_check;"

Expected last line: ok. Schedule it at 03:00 with crontab -e:

File contenttext
0 3 * * * /home/<you>/bin/vaultwarden-backup.sh

Copy the result to another machine. Encrypt it first, because config.json and rsa_key.pem are sensitive.

Windows (PowerShell)#

Windows does not ship sqlite3, so this version stops the container for a few seconds and copies the whole data folder. Keep db.sqlite3 and its db.sqlite3-wal together.

Codepowershell
$App = "$HOME\docker_volumes\vaultwarden"
$Out = "$HOME\vaultwarden_backups\$(Get-Date -Format yyyyMMdd-HHmm)"
New-Item -ItemType Directory -Force $Out | Out-Null
Set-Location $App
docker compose stop vaultwarden
try {
  Copy-Item "$App\data" $Out -Recurse -Force -ErrorAction Stop
  Copy-Item "$App\docker-compose.yml","$App\.env" $Out -Force -ErrorAction Stop
} finally {
  docker compose start vaultwarden
}

Schedule it with Task Scheduler (a daily trigger at 03:00 that runs pwsh -File <path-to-script>).

Mac (Terminal)#

Install sqlite3 if it is missing (macOS usually has it), then use the same script as Linux. Schedule it with crontab -e at 03:00 as above, or with launchd.

Restore test#

Never practise on your live vault. Restore into a second folder and start a throwaway copy:

Run in terminalbash
mkdir -p ~/vw-restore-test/data
cp -Rp ~/vaultwarden_backups/<backup-folder>/data/. ~/vw-restore-test/data/
rm -f ~/vw-restore-test/data/db.sqlite3-wal
docker run --rm -d --name vw-restore-test -p 127.0.0.1:8081:80 \
  -v ~/vw-restore-test/data:/data vaultwarden/server:1.37.4

On Windows, use $HOME\vw-restore-test\data and the same docker run line (replace ~/ with $HOME\). Open http://localhost:8081, log in with your normal account, open an entry that has an attachment, and edit something. If it all works, the backup is real. Then stop the test with docker stop vw-restore-test and delete the folder. A copied config.json can carry your live settings, so check it before you ever point real clients at a test copy.

Performance and resources#

Vaultwarden is light, and SQLite is plenty for a household.

The wiki does not give an official RAM or CPU minimum, and I will not invent one. Measure yours:

Run in terminalbash
docker stats --no-stream vaultwarden

Leave room for attachments, backups and logs on disk. On Windows and Mac, Docker Desktop's own virtual machine takes memory too. Use PostgreSQL or MariaDB only if you have a real reason, because SQLite is the most used and most tested backend. Keep the server newer than your clients: the release notes say 1.37.0 is needed for clients 2026.7.0 and later, and 1.37.2 for 2026.8.0 and later.

FAQ#

Is Vaultwarden the official Bitwarden server? No. It is an independent project that works with the official Bitwarden apps. Bitwarden, Inc. does not support it.

Can I skip HTTPS because it is only on my home network? No. The web vault needs a secure page for its encryption functions. Use a trusted HTTPS name even on a private network.

Do I have to open ports 80 and 443 to the internet to get a certificate? No. DNS validation lets you get a trusted certificate without letting the internet reach the server.

Does turning signups off stop all new users? It stops public sign-up. The admin page can still invite people, and organization invitations have their own switch.

Is copying db.sqlite3 enough for a backup? No. Attachments live outside the database, and a running SQLite file can depend on its write-ahead log. Use .backup or stop the container, copy everything, and prove it with a restore test.

Sources#

Checked against the official docs on 2026-10-26. Several wiki pages are older than six months and no newer official page exists; they are used as the current canonical pages. Items marked VERIFY were not confirmed against a running install.