Self-Host Bitwarden With Vaultwarden: Setup Guide
To self host Bitwarden, run Vaultwarden — a lightweight, Bitwarden-compatible server written in Rust — in Docker behind a reverse proxy that terminates HTTPS. It works with every official Bitwarden client, uses SQLite by default, and runs comfortably on a Raspberry Pi or a $5 VPS.
That's the whole idea. The rest of this guide is the parts that actually bite you: HTTPS being mandatory, the admin token needing to be hashed, and the one docker-compose gotcha that locks half the people out of their own admin panel on day one.
Why self host Bitwarden with Vaultwarden?
The official Bitwarden self-hosted server works, but it's heavy for a homelab: multiple containers, a real SQL database, and roughly 2 GB of RAM before you've stored a single password. Vaultwarden is a from-scratch reimplementation of the Bitwarden server API in Rust. It's a single container, uses SQLite out of the box, and idles at around 10 MB of RAM. It'll run on a Pi Zero.
The trade you're making: Vaultwarden is community-maintained and not affiliated with Bitwarden or 8bit Solutions. It implements the client API faithfully, so the official apps don't know the difference, but it isn't the vendor's supported product. For a household, a small team, or a homelab, that's a trade I'll take every time. If you're deploying for a company that needs a support contract, use the official server.
One naming note so you don't get confused reading older tutorials: Vaultwarden was called bitwarden_rs. It was renamed in v1.21.0 (2021) to avoid trademark confusion with Bitwarden. Same project, different name. If a guide tells you to pull bitwardenrs/server, it's out of date — the image is vaultwarden/server now.
Official Bitwarden server vs Vaultwarden
| Official Bitwarden (self-hosted) | Vaultwarden | |
|---|---|---|
| Stack | .NET, MSSQL | Single Rust binary |
| Default database | MSSQL | SQLite (MariaDB/MySQL/PostgreSQL optional) |
| Containers | Several | One |
| RAM at idle | ~2 GB | ~10 MB |
| Runs on a Raspberry Pi | Not really | Yes |
| Bitwarden clients | Yes | Yes (unmodified) |
| Official support | Yes | No (community) |
What you need before you start
- A Linux host with Docker and Docker Compose v2. A cheap VPS or a home box both work.
- A domain or subdomain (e.g.
vault.example.com) with a DNS record pointing at the server. A free DuckDNS subdomain is fine. - Ports 80 and 443 reachable, so the reverse proxy can get a Let's Encrypt certificate.
The domain isn't optional, and neither is HTTPS. Bitwarden clients use the browser's Web Crypto API, which most browsers refuse to run over plain HTTP. The mobile apps flat-out reject self-signed certificates too — I've watched people burn an evening on "the app won't connect" when the real answer was a self-signed cert the phone silently distrusted. Use a real domain and a real certificate. The only exception is localhost, which browsers treat as a secure context for testing.
How do I install Vaultwarden with Docker Compose?
The clean setup is two containers: Vaultwarden itself, and Caddy as a reverse proxy. Caddy fetches and renews Let's Encrypt certificates automatically, which is one fewer thing to babysit. Nginx Proxy Manager or Traefik work equally well if you already run one — the Vaultwarden config doesn't change.
Make a directory and create the compose file:
mkdir -p ~/vaultwarden && cd ~/vaultwarden
nano docker-compose.yml
services:
vaultwarden:
image: vaultwarden/server:latest # pin this — see the releases page for the current tag
container_name: vaultwarden
restart: unless-stopped
environment:
DOMAIN: "https://vault.example.com"
SIGNUPS_ALLOWED: "true" # set to false after you create your account
volumes:
- ./vw-data:/data
# no host ports published — Caddy reaches it over the compose network
caddy:
image: caddy:2
container_name: caddy
restart: unless-stopped
ports:
- 80:80
- 443:443
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy-data:/data
- ./caddy-config:/config
A quick opinion: don't run :latest in production. It'll upgrade you to a new major version the next time you pull, sometimes when you weren't expecting it. Pin to a specific tag (Vaultwarden is on the 1.34/1.35 series as of early 2026 — check the releases page for the exact current tag) and update deliberately.
Now the Caddyfile. This is the entire thing:
nano Caddyfile
vault.example.com {
reverse_proxy vaultwarden:80
}
That's it. If you've seen older Vaultwarden guides with a second reverse_proxy /notifications/hub block pointing at port 3012 — ignore them. WebSockets have been served on the main port since v1.29.0, and port 3012 was removed entirely in v1.31.0. The extra block does nothing now and just confuses people.
Bring it up:
docker compose up -d
docker compose logs -f vaultwarden
Give Caddy a few seconds to pull the certificate, then open https://vault.example.com. You should see the web vault login. Create your account now, because the next step turns off registration.
How do I secure the admin page and admin token?
Vaultwarden has an admin panel at /admin for managing users, SMTP, and settings. It's disabled until you set an ADMIN_TOKEN. Older guides tell you to set a plain-text token — don't. Vaultwarden logs a warning for plain-text tokens and prefers an Argon2 PHC hash. Generate one with the built-in command:
docker exec -it vaultwarden /vaultwarden hash
It prompts for a password twice and prints something like:
ADMIN_TOKEN='$argon2id$v=19$m=65540,t=3,p=4$RCpl3a...$d7UfKf...'
Here's the gotcha that catches everyone, me included. In a docker-compose file, $ triggers variable interpolation. Paste the hash as-is and Compose eats half of it, and you'll get "Invalid admin token" at /admin with no obvious reason. You have to double every dollar sign — $ becomes $$:
environment:
DOMAIN: "https://vault.example.com"
ADMIN_TOKEN: "$$argon2id$$v=19$$m=65540,t=3,p=4$$RCpl3a...$$d7UfKf..."
Then docker compose up -d again. The other thing that trips people up: you log in with the password you typed into the hash command, not the hash string itself. The hash is just what the server stores. I've watched more than one person paste the whole $argon2id... string into the login box and conclude the feature is broken.
Careful with the admin UI: saving anything through it writes a
config.jsonin your data folder, and that file overrides your environment variables from then on. If you later changeADMIN_TOKENin compose and it seems ignored, an oldconfig.jsonis the reason.
How do I lock it down after setup?
Fresh out of the box, anyone who finds your URL can register. Close that immediately:
- Set
SIGNUPS_ALLOWED: "false"once your own account exists, then restart. If you want to add family later without reopening registration, setINVITATIONS_ALLOWED: "true"and invite them from the admin panel (SMTP required for the email). - Don't expose
/adminto the whole internet if you can avoid it. Restrict it by IP at the reverse proxy, or only reach it over a VPN like WireGuard/Tailscale. A password manager's admin panel is a juicy target. - Consider Fail2Ban. A public Vaultwarden gets brute-force login attempts within hours — it's not paranoia, it's Tuesday. Point it at Vaultwarden's log by adding
LOG_FILE: "/data/vaultwarden.log"andLOG_LEVEL: "warn", and it'll ban the noisy offenders.
How do I back up Vaultwarden?
Everything lives in the volume you mounted at ./vw-data: db.sqlite3, the RSA keys, attachments, sends, and config.json. Back up the whole folder. The one wrinkle is the SQLite database — copying it while the container is writing can give you a torn file. Either stop the container for the copy, or use SQLite's online backup, which is safe on a live database:
docker exec vaultwarden sqlite3 /data/db.sqlite3 ".backup '/data/backup.sqlite3'"
Then copy backup.sqlite3 plus the attachments and keys somewhere off the machine. A backup sitting on the same disk as the thing it's backing up isn't a backup. And test a restore at least once — self-hosting your passwords with untested backups is genuinely worse than using a cloud provider, because when it fails you have no one to call. There's a dedicated backup page on the Vaultwarden wiki worth reading before you rely on any of this.
Connecting the Bitwarden clients
This is the good part: you use the real Bitwarden apps, unchanged. No sideloading, no forks.
- Install Bitwarden from the App Store, Play Store, or your browser's extension store.
- On the login screen, before logging in, open the settings/region selector and set the self-hosted server URL to
https://vault.example.com. - Log in with the account you created. Autofill and browser integration behave exactly like Bitwarden cloud.
If the app refuses to connect, it's almost always TLS: an untrusted or self-signed certificate, or a DOMAIN value that doesn't exactly match the URL you're hitting (including the https://). Fix the cert and match the domain and it connects.
FAQ
Is Vaultwarden safe for real passwords?
Yes, with the usual caveats of self-hosting anything sensitive. Your vault is encrypted client-side, so the server stores ciphertext. Your real risks are operational: no HTTPS, an exposed admin panel, or missing backups. Handle those three and it's solid.
Does Vaultwarden work with the official Bitwarden apps?
Yes — every official client (browser, desktop, iOS, Android) works unmodified. You just point them at your server URL instead of the Bitwarden cloud.
Can I run it without a domain or HTTPS?
Not usefully. Browsers block the Web Crypto API on insecure origins, and the mobile apps reject self-signed certs. Use a real domain with Let's Encrypt. localhost works only for local testing.
How much RAM does Vaultwarden need?
Very little — it idles around 10 MB and runs happily on a Raspberry Pi or the smallest VPS tier. The reverse proxy uses more than Vaultwarden does.
Is Vaultwarden the same as bitwarden_rs?
Yes. bitwarden_rs was renamed to Vaultwarden in v1.21.0. Same project — just use the vaultwarden/server image and ignore old bitwardenrs/ references.
Do I still need the WEBSOCKET_ENABLED variable?
No. WebSockets are built in and on by default since v1.29.0. WEBSOCKET_ENABLED and WEBSOCKET_PORT are deprecated and ignored; the old port 3012 was removed in v1.31.0. You can set ENABLE_WEBSOCKET: "false" to turn notifications off, but there's rarely a reason to.
Once this is running, the natural next moves are locking the admin panel behind a VPN and automating that off-site backup. Both are cheap insurance for the one service you really don't want to lose access to.