how to setup vaultwarden on vps with docker
Learn how to self-host Vaultwarden on a VPS using Docker. We cover HTTPS setup, admin token, Fail2ban, and how to backup your data volume properly.
Wetin you dey build
You dey build password manager wey you get full control: Vaultwarden wey dey run inside one small container behind reverse proxy wey dey handle HTTPS. You go use official Bitwarden apps for your phone, laptop, and browser connect to am. Vaultwarden re-implement Bitwarden server API inside Rust and e dey use same protocol as bitwarden.com. This mean say every official client go work with am without any change — but e only dey use about 100 MB of RAM, unlike the official stack wey need many containers.
The install itself na just dozen lines of Compose. Three things na dem really matter — and dem na wetin dey cause wahala: TLS must dey before you open the web vault, public signups must close once your own account don exist, and you must backup and test-restore the data volume, because na that one directory hold every password wey you get.
Prerequisites and the honest gotchas
- VPS wey get Docker Engine and Compose plugin, for fresh Ubuntu 24.04 KVM box wey get root or sudo access. 512 MB of RAM enough; 1 GB go make am easy. This one na one of the lightest things wey you fit run — e dey near the top of the shortlist of services worth self-hosting.
- Domain wey get A record (and AAAA if you get IPv6) wey point
vault.example.comto the VPS. TLS certificate na for this exact name, so DNS must work before you start. - Ports 80 and 443 must open to the internet, and your reverse proxy must handle dem — never let Vaultwarden handle dem directly. Port 80 na for ACME certificate challenge and HTTP-to-HTTPS redirect only.
- The biggest problem wey you go face: Bitwarden clients no go gree talk to any server wey no get HTTPS. No "test it over http first" — that way no dey work, because of one concrete reason wey we go explain next.
Why Vaultwarden, no be official Bitwarden stack
Client dem na de same, but weight small well-well. Official self-hosted Bitwarden dey come inside bundle of containers (MSSQL, Nginx, Identity, Api, Admin and more) and e dey take roughly 2 GB of RAM. Vaultwarden na single binary wey dey store everything inside SQLite database by default, and e dey use just few tens of megabytes when e dey idle. For one person, family, or small team, na de obvious choice, and because e dey follow Bitwarden API faithfully, your data fit move easy between am and bitwarden.com.
Wetin you go lose na most of de enterprise features: no SCIM provisioning (even though experimental OpenID Connect SSO land inside 1.35.0), and you na de operator, so patching, HTTPS, and backups na your job. Dis guide na for dem three jobs.
Why HTTPS no be optional
Bitwarden web vault and browser extensions dey use Web Crypto API (window.crypto.subtle) derive your encryption keys inside browser. Browsers only dey allow use crypto.subtle inside secure context — wey be HTTPS, or the special case of http://localhost. If you dey use plain http://vault.example.com, e no dey undefined, so as the app wan derive key, e go fail, and console go show:
Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'importKey')The page go hang or e go show generic crypto error, and nothing go log in. Desktop, mobile, and browser clients dey run their own check against self-hosted URL, and if dem see http (or unreachable) endpoint, dem go refuse am with:
This is not a recognized Bitwarden server. You may need to check with your provider or update your server.Both of dem get same cause: no valid HTTPS. So, make you set up TLS first and no ever open the vault over http, even if na just to quick look.
Step 1 — DNS and the reverse proxy (TLS first)
Point the record for your VPS and check if e resolve to the correct address:
dig +short vault.example.comThe line wey e print must be your VPS IP. If e blank or e wrong, fix your DNS and wait for TTL to finish — certificate issuance go fail if the name no resolve.
For the HTTPS front end, this guide dey use Traefik. Traefik dey issue and renew Let's Encrypt certificates automatically and e dey work well with Compose. If you never start to run am, follow the Traefik reverse proxy and automatic TLS setup first; e dey create one external Docker network (proxy below) and one ACME resolver (letsencrypt) wey the Vaultwarden service go connect to. Plain nginx with certificate wey you hand-issue go work exactly same for Vaultwarden side.
You prefer nginx and Certbot instead of Traefik? Put Vaultwarden on 127.0.0.1:8080 (add ports: ["127.0.0.1:8080:80"] to the service and remove the Traefik labels), then issue certificate and proxy to am. We cover the certificate part for issuing Let's Encrypt certificates with Certbot and nginx. The important extra part na the WebSocket upgrade for the notifications path:
server {
listen 443 ssl;
server_name vault.example.com;
client_max_body_size 525M;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}Look the X-Real-IP line — na wetin dey make Fail2ban see the real attacker instead of 127.0.0.1 later. Everything else for this guide na the same whether Traefik or nginx dey front.
Step 2 — the Compose file
First, create the project directory. Dis guide dey use /opt/vaultwarden, wey go make the Compose project name — and the data volume, vaultwarden_vw-data — easy to predict; de Fail2ban and backup steps wey dey below depend on dat exact name.
sudo mkdir -p /opt/vaultwarden
cd /opt/vaultwardenCreate one .env for de admin secret and de Compose file inside dat directory.
# .env
ADMIN_TOKEN=paste-a-strong-token-hereUse openssl rand -base64 48 generate dat token den paste am. (Next section go explain how to use stronger hashed form; for now, long random string go work fine.)
# docker-compose.yml
services:
vaultwarden:
image: vaultwarden/server:latest
container_name: vaultwarden
restart: unless-stopped
environment:
DOMAIN: "https://vault.example.com"
SIGNUPS_ALLOWED: "true" # closed in Step 4, keep true just to register
ADMIN_TOKEN: "${ADMIN_TOKEN}"
IP_HEADER: "X-Forwarded-For" # X-Real-IP if your proxy sends that instead
LOG_FILE: "/data/vaultwarden.log"
LOG_LEVEL: "warn"
volumes:
- vw-data:/data
networks:
- proxy
labels:
- "traefik.enable=true"
- "traefik.http.routers.vw.rule=Host(`vault.example.com`)"
- "traefik.http.routers.vw.entrypoints=websecure"
- "traefik.http.routers.vw.tls.certresolver=letsencrypt"
- "traefik.http.services.vw.loadbalancer.server.port=80"
volumes:
vw-data:
networks:
proxy:
external: trueTwo things for dis file na de main part of de design. No be any ports: mapping, so people fit reach Vaultwarden only through Traefik and its TLS — if you publish its port on de host, people go dey use de vault over http by accident. And DOMAIN must be de full public HTTPS URL: de system bake am inside attachment links, WebAuthn 2FA, and de notifications endpoint, so if you put wrong value or http, dem go fail even if de site load well. De latest tag na special case for de usual never-latest rule — Vaultwarden dey release stable versions as single rolling image, wey de :testing be de separate pre-release channel — so update on purpose and check de release notes before you pull.
Run am den watch de log:
docker compose up -d
docker compose logs -f vaultwardenIf everything correct, de log go end with one line like Rocket has launched from http://0.0.0.0:80. Give Traefik few seconds to fetch de certificate, den load https://vault.example.com — you go see de Bitwarden web vault with valid padlock and no certificate warning.
Step 3 — strong ADMIN_TOKEN, and the $$ trap
ADMIN_TOKEN dey guard /admin, the panel wey fit read every user and setting for your instance, so treat am like root password. Two ways work.
The simple way na the random string wey you don already generate with openssl rand -base64 48. Because base64 no dey carry $, e go enter .env straight without any escaping.
The hardened way na Argon2 PHC hash, so the plaintext token no go ever dey store for disk. Generate one for the same image:
docker run --rm -it vaultwarden/server /vaultwarden hash --preset owaspE go ask you twice and print string wey start with $argon2id$v=19$.... Dis na the trap wey dey make people waste one hour: Docker Compose dey treat $ as variable interpolation, so you must double every $ to $$ when you paste the hash inside the Compose file. Put am directly under environment:, no use .env, and no wrap am inside quotes:
environment:
ADMIN_TOKEN: $$argon2id$$v=19$$m=19456,t=2,p=1$$c29tZXNhbHQ$$RdescudvJCsgt3ub+b+dWRWJTmaaJObGIf you leave single $ signs, Compose go warn The "argon2id" variable is not set and blank the token, and /admin go reject your correct password. Run docker compose up -d, and keep the plaintext wey you type for prompt inside your own password store.
Step 4 — register your account, then lock the door
Use SIGNUPS_ALLOWED: "true", open https://vault.example.com, click Create account, and register with your email and one strong master password. You no fit recover this master password if you forget am — no reset option dey — so make sure you keep am for safe place first.
Now lock the door. Edit the Compose file so signups go off:
SIGNUPS_ALLOWED: "false"Re-apply the change with docker compose up -d. This one no be hardening wey you fit do later. If you leave am open, anyone wey find the URL — and crawlers dey find dem — fit create account for your server. Dem no fit read your vault, but dem go consume your resources and turn your private instance to open service. If you see say you leave am open, /admin go list accounts wey you no ever create.
If you want add family or teammates later without reopening public signups, use the Invite User button for /admin; that path need SMTP configuration so the person wey you invite fit receive their link.
Step 5 — reaching /admin
Go to https://vault.example.com/admin and type the plaintext admin token (the random string, or the password wey you hash — no be the hash itself). Inside dis page, you fit list users, change settings, send test email, and take database snapshot.
If di page show 404 Not Found, e mean say ADMIN_TOKEN empty or e no set, and dat one go lock di panel completely — dat one na valid choice if you no need am at all. If di page load but e reject your token, check di $$ escaping trap for di failure list below. You forget di token? No be say dem dey give recovery prompt; edit .env or di Compose file, set new one, and docker compose up -d.
Step 6 — connect the Bitwarden clients
Every official client fit point to self-hosted server. So, install Bitwarden desktop, mobile, or browser client from normal stores — you no need any special Vaultwarden build.
Before you log in, open the settings gear for the login screen (wey dem label as Self-hosted or Region → Self-hosted), set Server URL to https://vault.example.com, and save am. After that, log in with the email and master password wey you register; the client suppose connect sharp-sharp and ask if you wan fill and save credentials.
If one client show This is not a recognized Bitwarden server. You may need to check with your provider or update your server., e mean say the URL wrong, e dey use http, or the certificate no be trusted — check again if https://vault.example.com dey load well for browser first. Slow updates for other devices na WebSocket push, we go explain am below.
Step 7 — a Fail2ban jail for the login endpoint
Vaultwarden dey log every failed login for the file wey you set for LOG_FILE — na exactly wetin brute-force guard need. If you never start to run Fail2ban, you fit see how to install and basic settings for the Fail2ban SSH hardening guide; for here, we wan add one jail for the vault.
First, find where the named volume dey live for host, so Fail2ban go fit read the log:
docker volume inspect vaultwarden_vw-data --format '{{ .Mountpoint }}'E go print something like /var/lib/docker/volumes/vaultwarden_vw-data/_data; the log dey vaultwarden.log inside am. Create the filter:
# /etc/fail2ban/filter.d/vaultwarden.conf
[Definition]
failregex = ^.*Username or password is incorrect\. Try again\. IP: <ADDR>\. Username:.*$
ignoreregex =And the jail:
# /etc/fail2ban/jail.d/vaultwarden.local
[vaultwarden]
enabled = true
filter = vaultwarden
logpath = /var/lib/docker/volumes/vaultwarden_vw-data/_data/vaultwarden.log
banaction = iptables-allports
chain = DOCKER-USER
maxretry = 5
findtime = 600
bantime = 3600Reload am with sudo systemctl restart fail2ban and confirm am with sudo fail2ban-client status vaultwarden.
Three Docker details go decide if this one go protect anything. First, if the log dey show IP: 127.0.0.1 or your proxy address for every failed attempt, Vaultwarden dey ban the proxy — set IP_HEADER to the header wey your proxy dey really send (X-Forwarded-For for Traefik, X-Real-IP for the nginx block wey dey above, CF-Connecting-IP for dem wey dey behind Cloudflare). Second, the right iptables chain depend on your proxy: if you dey run Traefik as container with published ports, traffic dey pass through Docker FORWARD path, so the ban must dey DOCKER-USER like we do before; but if you pick the host-nginx option for Step 1, connections dey end for nginx for the host INPUT chain and a DOCKER-USER ban no go see dem — if na so, delete the chain = DOCKER-USER line make Fail2ban use the default INPUT chain. Third, use banaction = iptables-allports instead of the port-based default — this jail no define any port, and an all-ports ban for DOCKER-USER go block the offender from every published service for the box.
Step 8 — back up the vault, then actually restore it
vw-data volume na your password manager. E hold db.sqlite3 (every entry), the attachments/ and sends/ directories, the rsa_key.* files wey dey sign login sessions, and config.json from the admin panel. If backup skip any of these, e go fail when you need am.
If you copy db.sqlite3 while Vaultwarden dey write, you fit get half-written or corrupt file. So, take cold snapshot — the downtime na just few seconds:
#!/usr/bin/env bash
set -euo pipefail
STAMP=$(date +%F)
DEST=/root/vw-backups
VOL=$(docker volume inspect vaultwarden_vw-data --format '{{ .Mountpoint }}')
mkdir -p "$DEST"
docker compose -f /opt/vaultwarden/docker-compose.yml stop vaultwarden
tar czf "$DEST/vw-$STAMP.tgz" -C "$VOL" .
docker compose -f /opt/vaultwarden/docker-compose.yml start vaultwardenRun am from cron nightly and copy the .tgz out of the box — backup wey dey only stay for the server wey you dey protect no be real backup. The clean way to ship am na nightly restic backup to another server or object storage, wey go encrypt the archive and deduplicate repeated snapshots for you. The Backup Database button for admin panel na quick snapshot of the SQLite file alone, but e no go carry attachments and keys.
Now, the ritual wey dey separate real backup from one wey be just hope — restore am once and prove say e dey work:
mkdir -p /tmp/vw-restore
tar xzf /root/vw-backups/vw-2026-07-15.tgz -C /tmp/vw-restore
docker run --rm -p 127.0.0.1:8888:80 -v /tmp/vw-restore:/data vaultwarden/serverFrom your laptop, tunnel to am with ssh -L 8888:127.0.0.1:8888 you@your-vps and open http://localhost:8888. Because localhost na secure context, crypto.subtle go dey available and the vault go decrypt over plain http for here — na only that one place wey dem allow. Log in with your master password and check if your entries dey: if dem dey, your database, RSA keys and master password all round-trip, and you fit rebuild everything for fresh VPS in minutes. Stop the container with Ctrl-C and delete /tmp/vw-restore.
Failure modes, with the strings you will see
Cannot read properties of undefined (reading 'importKey') for browser console. The vault load over http, so crypto.subtle no dey; use only https:// and add the HTTP-to-HTTPS redirect for the proxy.
This is not a recognized Bitwarden server... for client. The Server URL na http, or you type am wrong, or the certificate no be trusted; check if https://vault.example.com show valid padlock, then type am again for client self-hosted settings.
/admin reject correct password. The Argon2 hash lose its escaping — every $ must be $$ for Compose — or you type the hash instead of the plaintext wey e represent.
Slow cross-device sync; console show WebSocket connection to 'wss://vault.example.com/notifications/hub' failed. The proxy no dey forward the Upgrade/Connection headers; Traefik dey do am automatically, but nginx need the two upgrade lines from Step 1. The vault still dey work, e just dey sync when you open am. The old dedicated port 3012 don commot since v1.31.0, so you no need separate WebSocket route.
Fail2ban report ban but attacker still dey connect. E dey ban 127.0.0.1 because IP_HEADER wrong, or the ban dey inside wrong iptables chain — set chain = DOCKER-USER and banaction = iptables-allports.
Upgrades
Pull the new image and recreate am; the named volume and all your data go still dey there:
docker compose pull
docker compose up -dVaultwarden dey release new versions many times. Follow the project's release notes instead of to pin one patch version, because some releases get migration notes inside. Take fresh backup before you do any major bump; you fit roll back by to restore the tarball into a new volume.
FAQ
Vaultwarden and Bitwarden na the same thing?
E be compatible, independent server, e no be the official one. Vaultwarden re-implement the Bitwarden server API for Rust, so all the official desktop, mobile, browser, and CLI clients go work well with am, but e no go chop much resources like the official stack. The vault format na the same, so you fit migrate go front or back by using export and import.
I must use HTTPS, or I fit run am for http for my LAN?
You need HTTPS for anything wey no be localhost test. The Bitwarden web vault and extensions dey use browser Web Crypto API, and dat one dey only work for secure context. If you use plain http, the client go throw Cannot read properties of undefined and e no go allow you log in. The only http address wey go work na http://localhost, na why the restore test for Step 8 dey use SSH tunnel.
How I fit stop strangers from register for my server?
Set SIGNUPS_ALLOWED: "false" for your Compose file and run docker compose up -d immediately after you create your own account. After dat, use the Invite User button for /admin to add new people, but you must configure SMTP so dem fit receive the invitation link. Check the admin user list every now and then to make sure say no strange account appear.
How I fit back up my Vaultwarden vault?
Stop the container for small time and archive the whole vw-data volume — db.sqlite3, attachments/, sends/, config.json and the rsa_key.* files — then copy the archive commot for the server, e go beta if you use nightly cron. If you copy the live SQLite file while the server dey run, the snapshot fit corrupt, so make sure you stop the server first. Most important thing be say, try restore am once inside one throwaway container and log in, so you go know say the backup real before you depend on am.
E safe to self-host my passwords?
Yes, if you do the three things wey dis guide talk: real HTTPS, close signups plus strong admin token, and tested backups. Your vault dey encrypt for client-side with your master password, so even the server no go see your passwords for plain text — if dem steal db.sqlite3, e no go mean anything without the password. The only catch be say patching and backups na your responsibility now, na why Fail2ban and the restore ritual no be optional for here.