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.
| What | Value |
|---|---|
| Current version | 1.37.4, released 2026-10-05 (GitHub releases) |
| Pinned image | vaultwarden/server:1.37.4 (the tag exists on Docker Hub) |
| Time needed | About 20 minutes if Traefik already runs |
| Data | ~/docker_volumes/vaultwarden/data (SQLite file, attachments, keys) |
| Port inside the container | 80 (Traefik talks to it; nothing is published on the host) |
| Official docs | Vaultwarden 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.
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.
| System | What you need |
|---|---|
| Linux | Docker Engine with the Compose plugin, bash. Optional: sqlite3 on the host for the backup command |
| Windows | Docker Desktop in Linux-container mode (WSL2 backend), PowerShell 7 |
| Mac | Docker 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:
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#
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)#
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:
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.
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.
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:
DOMAINmust be your real HTTPS address. Vaultwarden needs it for links, attachments and invitations../datais the data folder you made in step 1, relative to the compose file. That works the same on all three systems.LOG_FILElets 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#
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.)
Get-Item "$HOME\docker_volumes\vaultwarden\.env" | Select-Object FullName
Mac (Terminal)#
Same commands as Linux:
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:
docker network create proxy
Windows and Mac use the same command. Then start the vault:
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.
- Open your vault address. Make sure the certificate is trusted, with no warning to click through.
- Click Create account. Use a strong master password that is different from the admin password.
- Edit
.envand setVW_SIGNUPS_ALLOWED=false, then rundocker compose up -dto recreate the container. - Open the page again and confirm that strangers can no longer register.
- Open
/adminand log in with the password you typed in step 1. Look at the diagnostics page: it checks HTTPS, the domain and the clock. - In each Bitwarden app, choose "self-hosted" on the login screen and enter your exact vault address.
- 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):
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, thersa_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
/adminat the proxy too (for example, allow it only from your LAN). If you do not need it, removeADMIN_TOKENandconfig.json'sadmin_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 inIP_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):
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:
curl -I https://vault.example.com docker compose ps docker compose logs --tail=100 vaultwarden
Troubleshooting#
| Problem | Solution |
|---|---|
| Page works over HTTPS but the vault will not load over plain HTTP | Expected. 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... string | Your Vaultwarden is too old to understand hashes. Update |
No status 101 on /notifications/hub | The 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 IP | Fix the real-IP header and IP_HEADER_TRUSTED_PROXIES, or fail2ban will ban the proxy |
| Admin settings will not save after an upgrade | 1.37.4 removed several experimental feature flags. Remove the old entries from EXPERIMENTAL_CLIENT_FEATURE_FLAGS |
No compatible authentication mechanism was found when testing email | The 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 empty | Settings, 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#
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#
- Read the release notes of the new version.
- Make and check a backup (below).
- Change
VW_VERSIONin.env. - Run the commands:
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#
| Item | Needed? |
|---|---|
db.sqlite3 | Yes, and use a proper SQLite backup, not a plain copy of a running database |
attachments/ | Yes |
config.json | Recommended (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.yml | Yes, 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#
#!/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:
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.
$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:
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:
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#
- Releases, 1.37.4 (2026-10-05) and 1.37.3 (2026-09-13), read on 2026-10-26
- Wiki home, edited 2026-10-06
- Enabling admin page (hash command,
$$escaping), edited 2026-08-21 - Backing up your vault, edited 2026-04-10
- Proxy examples (Traefik labels) and Using Docker Compose
- Enabling HTTPS and private instance with Let's Encrypt
- Enabling WebSocket notifications, SMTP configuration, mobile push
- Fail2Ban setup and Hardening guide
- Issue tracker and discussions for the release-health check
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.