How to Run Vaultwarden on VPS with Docker
Run Vaultwarden for Bitwarden apps on a VPS with Docker. This guide covers HTTPS, admin token, Fail2ban, and backups you fit restore-test.
Wetin you dey build
Na password manager wey na you fully own: Vaultwarden dey run inside one small container behind reverse proxy wey dey terminate HTTPS. Official Bitwarden apps for your phone, laptop, and browser go point to am. Vaultwarden re-implement Bitwarden server API for Rust and e dey use the same protocol with bitwarden.com. So every official client go work with am without any change. But e dey use about 100 MB RAM instead of the official stack wey need many containers.
The installation itself na about twelve lines of Compose. The three things wey really matter, and wey fit fail, na these: TLS must dey ready before you ever load the web vault; public signups must close immediately your own account don exist; and you must back up and test-restore the data volume. Na that one directory hold every password wey you own.
Prerequisites and the honest gotchas
- One VPS wey get Docker Engine and the Compose plugin, for fresh Ubuntu 24.04 KVM box with root or sudo. 512 MB RAM genuinely dey enough; 1 GB go comfortable. This na one of the lightest things wey you fit run, and e dey near the top of the shortlist of services wey worth self-hosting. But size the box based on wetin else dey share am: if you put self-hosted photo library like PhotoPrism or Immich for the same VPS, your RAM minimum go enter gigabytes, while Vaultwarden barely go increase am. The same calculation apply to media front ends wey you add later, because dressing a Jellyfin library up as a walkable 90s rental store mean another always-on container plus transcoding headroom from the same budget.
- One domain wey get A record (and AAAA if you get IPv6) pointing
vault.example.comto the VPS. Dem go issue the TLS certificate for this exact name, so DNS must resolve before you start. - Ports 80 and 443 must open to internet and your reverse proxy must terminate dem, never Vaultwarden directly. Port 80 dey used only for ACME certificate challenge and HTTP-to-HTTPS redirect.
- The biggest gotcha na this: Bitwarden clients no gree communicate with server wey no dey use HTTPS. No be say you fit "test am over http first"; that approach no work, and the next section explain the exact reason.
Why Vaultwarden, no be the official Bitwarden stack
Same clients, but e no heavy like am. The official self-hosted Bitwarden releases as bundle of containers (MSSQL, Nginx, Identity, Api, Admin and more) and e dey need around 2 GB of RAM. Vaultwarden na single binary wey dey store everything for SQLite database by default, and when e idle e dey use just some tens of megabytes. For one person, family, or small team, na the clear choice. Since e implement Bitwarden API well, your data fit move between am and bitwarden.com without wahala.
Wetin you lose na most of the enterprise features: no SCIM provisioning (although experimental OpenID Connect SSO enter for 1.35.0). You be the operator, so patching, HTTPS, and backups na your responsibility. This guide na about those three responsibilities.
HTTPS no be optional
Bitwarden web vault and browser extensions dey derive your encryption keys inside the browser with Web Crypto API (window.crypto.subtle). Browsers only expose crypto.subtle inside a secure context, like HTTPS, or the special case of http://localhost. For plain http://vault.example.com, e dey undefined, so as soon as the app try derive key, e throw error, and console go show:
Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'importKey')The page fit hang or show general crypto error, and nobody go fit log in. Desktop, mobile, and browser clients dey run their own check against self-hosted URL. If endpoint na http or e no reachable, dem go reject am with:
This is not a recognized Bitwarden server. You may need to check with your provider or update your server.Both cases get the same cause: valid HTTPS no dey. So we go set up TLS first, and we no go ever open the vault over http, not even once for quick check.
Step 1, DNS and reverse proxy (TLS first)
Point the record go your VPS and confirm say e resolve to the correct address:
dig +short vault.example.comThe line wey e print must be your VPS IP. If e blank or wrong, fix DNS and wait for the TTL to finish. Certificate issuance go fail if the name no resolve.
For the HTTPS front end, this guide dey use Traefik. E dey issue and renew Let's Encrypt certificates automatically, and e fit connect directly to Compose. If you no dey run am already, follow the Traefik reverse proxy and automatic TLS setup first. E go create an external Docker network (proxy below) and an ACME resolver (letsencrypt) wey the Vaultwarden service go attach to. Plain nginx with a certificate wey you issue manually go work the same way from Vaultwarden side.
You prefer nginx and Certbot instead of Traefik? Put Vaultwarden for 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 traffic to am. The certificate part dey covered for issuing Let's Encrypt certificates with Certbot and nginx. The important extra 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";
}
}Pay attention to the X-Real-IP line. Na e allow Fail2ban later see the real attacker instead of 127.0.0.1. Everything else for this guide dey the same whether Traefik or nginx dey in front.
Step 2, the Compose file
First create the project directory. This guide dey use /opt/vaultwarden. E make the Compose project name, and therefore the data volume, vaultwarden_vw-data predictable. The Fail2ban and backup steps below depend on this exact name.
sudo mkdir -p /opt/vaultwarden
cd /opt/vaultwardenCreate a .env for the admin secret and the Compose file inside that directory.
# .env
ADMIN_TOKEN=paste-a-strong-token-hereGenerate the token with openssl rand -base64 48 and paste am inside. (The next section cover a stronger hashed form; one long random string dey okay to start.)
# 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 this file carry the whole design. There is no ports: mapping, so na Traefik and its TLS dey give Vaultwarden access. If you publish its port on the host, people fit serve the vault over http by mistake. Also, DOMAIN must be the complete public HTTPS URL. The system embeds am inside attachment links, WebAuthn 2FA, and the notifications endpoint. So wrong value or http value go break dem even when the site loads. The latest tag na deliberate exception to the usual never-latest rule. Vaultwarden releases stable versions as one rolling image, while :testing na the separate pre-release channel. So update am deliberately and quickly check the release notes before you pull. But this exception narrow. Most containers wey go run for long time better make you pin dem to exact tag. Na this one dey keep an always-on agent wey you self-host for the same VPS predictable across reboots and pulls.
Bring am up and monitor the log:
docker compose up -d
docker compose logs -f vaultwardenCorrect startup go end with a line like Rocket has launched from http://0.0.0.0:80. Give Traefik some seconds to fetch the certificate, then open https://vault.example.com. You suppose see the Bitwarden web vault with valid padlock and no certificate warning.
Step 3, strong ADMIN_TOKEN, and the $$ trap
ADMIN_TOKEN dey protect /admin, the panel wey fit read every user and setting for your instance, so treat am like root password. Two forms dey work.
The simple form na the random string wey you don already generate with openssl rand -base64 48. Because base64 no dey ever contain $, you fit put am directly inside .env without escaping.
The hardened form na Argon2 PHC hash, so plaintext token no dey stored for disk. Generate one against the same image:
docker run --rm -it vaultwarden/server /vaultwarden hash --preset owaspE go prompt you two times and print string wey start with $argon2id$v=19$.... Na here the trap wey dey waste people one hour dey: Docker Compose dey treat $ as variable interpolation, so you must double every $ to $$ when you paste the hash inside Compose file. Put am directly under environment:, no be through .env, and no wrap am with quotes:
environment:
ADMIN_TOKEN: $$argon2id$$v=19$$m=19456,t=2,p=1$$c29tZXNhbHQ$$RdescudvJCsgt3ub+b+dWRWJTmaaJObGIf you leave the single $ signs, Compose go warn The "argon2id" variable is not set and clear the token, then /admin go reject your correct password. Run docker compose up -d, and keep the plaintext wey you type for the prompt inside your own password store.
Step 4, register your account, then lock the door
With SIGNUPS_ALLOWED: "true", open https://vault.example.com, click Create account, then register with your email and strong master password. You no fit recover this master password, and reset no dey. So store am for safe place wey durable before you continue.
Now lock am. Edit the Compose file make signups off:
SIGNUPS_ALLOWED: "false"Apply am again with docker compose up -d. You no fit postpone this security step. If you leave am open, anybody wey find the URL, including crawlers, fit create account for your server. Dem no fit read your vault, but dem go use resources and turn your private instance into open service. The sign say you leave am on be say /admin dey list accounts wey you never create.
If you wan add family members or teammates later without opening public signups again, use the Invite User button for /admin. That method need SMTP configured so the person wey you invite fit receive their link.
Step 5, reach /admin
Open https://vault.example.com/admin for browser, then enter the admin token as plain text (the random string, or the password wey you hash, no be the hash itself). Inside, you fit list users, adjust settings, send test email, and take database snapshot.
If the page return 404 Not Found, e mean say ADMIN_TOKEN empty or unset. This one disable the panel completely, and e fit still be the correct choice if you no ever need am. If the page load but e reject your token, check the $$ escaping trap for the failure list below. You don forget the token? No recovery prompt dey. Edit .env or the Compose file, set a new one, then docker compose up -d.
Step 6, connect the Bitwarden clients
Every official client fit connect to self-hosted server, so install the Bitwarden desktop, mobile, or browser client from the normal stores. You no need any special Vaultwarden build.
Before you log in, open the settings gear for the login screen (wey dem label Self-hosted or Region → Self-hosted). Set Server URL to https://vault.example.com, then save am. Log in with the email and master password wey you register. The client suppose connect immediately and offer to fill and save credentials.
If client show This is not a recognized Bitwarden server. You may need to check with your provider or update your server., the URL fit wrong, e fit dey use http, or the certificate fit no trusted. Check again say https://vault.example.com dey load cleanly for browser first. If updates slow for other devices, na WebSocket push cause am; we cover am below.
Step 7, Fail2ban jail for the login endpoint
Vaultwarden dey log every failed login for the file wey LOG_FILE set. Na exactly wetin brute-force protection need. If you never dey run Fail2ban before, installation and basic setup dey Fail2ban SSH hardening guide; for here, we go add one jail for the vault.
First find where the named volume dey for the host, so Fail2ban fit read the log:
docker volume inspect vaultwarden_vw-data --format '{{ .Mountpoint }}'That command go print something like /var/lib/docker/volumes/vaultwarden_vw-data/_data; the log dey inside am for vaultwarden.log. Create the filter:
# /etc/fail2ban/filter.d/vaultwarden.conf
[Definition]
failregex = ^.*Username or password is incorrect\. Try again\. IP: <ADDR>\. Username:.*$
ignoreregex =Then create 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 with sudo fail2ban-client status vaultwarden.
Three Docker details go determine whether this protection dey work. First, if the log dey show IP: 127.0.0.1 or your proxy address for every failed attempt, Vaultwarden dey ban the proxy instead of the real client. Set IP_HEADER to the header wey your proxy really dey send: X-Forwarded-For for Traefik, X-Real-IP for the nginx block above, or CF-Connecting-IP when traffic dey pass through Cloudflare. Second, the correct iptables chain depend on your proxy. When Traefik dey run as container with published ports, traffic dey pass through Docker FORWARD path. So the ban must dey for DOCKER-USER as shown above. But if you choose the host-nginx option for Step 1, connections dey terminate at nginx on the host's INPUT chain. A DOCKER-USER ban no go see those connections. For that case, delete the chain = DOCKER-USER line so Fail2ban go 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 on the box.
Step 8, back up the vault, then actually restore am
The 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. Backup wey skip any of these go fail when you need am.
If you copy db.sqlite3 while Vaultwarden dey write, e fit capture half-written, 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 every night and copy .tgz go outside the box. Backup wey dey only for the server wey you dey protect no be backup. The clean way to ship am na nightly restic backup go another server or object storage, wey encrypt the archive and deduplicate repeated snapshots for you. The admin panel Backup Database button na useful hot snapshot of the SQLite file alone, but e no include attachments and keys.
Now do the test wey separate real backup from backup wey you only hope say e work: restore am once and prove say e 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 go 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 dey available and the vault dey decrypt over plain http here, na the only place wey this one dey allowed. Log in with your master password and confirm say your entries dey there. If dem dey there, your database, RSA keys and master password all round-trip correctly, and you fit rebuild for fresh VPS within minutes. Stop the container with Ctrl-C and delete /tmp/vw-restore. Keep this tunnel habit for any other admin UI for the box wey no suppose face internet. Na so you go reach self-hosted open-kritt security scanner for port 5173 too.
Failure modes, plus the strings wey you go see
Cannot read properties of undefined (reading 'importKey') for browser console. Dem load the vault over http, so crypto.subtle no dey defined; you fit reach am only through https://, and add HTTP-to-HTTPS redirect for the proxy.
This is not a recognized Bitwarden server... for client. The Server URL na http, dem type am wrong, or the certificate no trusted; confirm say https://vault.example.com dey show valid padlock, then enter am again for the client's self-hosted settings.
/admin dey reject the correct password. The Argon2 hash lose its escaping. Every $ must be $$ for Compose. Or you enter the hash instead of the plaintext wey e represent.
Cross-device sync slow; console dey show WebSocket connection to 'wss://vault.example.com/notifications/hub' failed. The proxy no dey forward Upgrade/Connection headers. Traefik dey do this automatically. For nginx, you need the two upgrade lines from Step 1. The vault still dey work, but e go sync only when you open am. The old dedicated port 3012 don comot since v1.31.0, so you no need separate WebSocket route.
Fail2ban reports ban, but attacker still dey connect. E dey ban 127.0.0.1 because IP_HEADER dey wrong, or the ban dey for 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:
docker compose pull
docker compose up -dVaultwarden dey release new versions often. Monitor the project release notes instead of pinning patch version, because some releases get migration notes. Make fresh backup before any major upgrade; you fit roll back by restoring the tarball into a new volume.
FAQ
Vaultwarden na the same thing as Bitwarden?
Na compatible, independent server, e no be the official one. Vaultwarden re-implement Bitwarden server API for Rust, so official desktop, mobile, browser, and CLI clients all fit work with am, while e dey use far less resources than the official stack. Vault format na the same, so you fit migrate for either direction by exporting and importing.
I really need HTTPS, or I fit run am over http for my LAN?
You need HTTPS for anything apart from a localhost test. Bitwarden web vault and extensions dey use browser Web Crypto API, and e only dey available for secure context. So if you use plain http, client go throw Cannot read properties of undefined and e no go ever log in. The only http address wey dey work na http://localhost. Na why restore test for Step 8 dey use SSH tunnel.
How I fit stop strangers from registering for my server?
Set SIGNUPS_ALLOWED: "false" for Compose file and run docker compose up -d immediately after you create your own account. From that time, add new people through Invite User button for /admin. SMTP must dey configured so dem fit receive invitation link. Check admin user list sometimes to confirm say no unexpected account don appear.
How I fit back up my Vaultwarden vault?
Stop the container briefly and archive the whole vw-data volume, db.sqlite3, attachments/, sends/, config.json, and the rsa_key.* files. Then copy the archive comot from the server, ideally with nightly cron. If you copy live SQLite file while server dey run, snapshot fit corrupt, so take am cold. Most importantly, restore am once to a throwaway container and log in, so you go know say the backup real before you depend on am.
E really safe to self-host my passwords?
Yes, if you do the three things wey this guide cover: real HTTPS, closed signups with strong admin token, and tested backups. Your vault dey encrypted client-side with your master password, so even server no dey see your passwords in clear text. If person steal db.sqlite3, e useless without the master password. The trade-off na say patching and backups don become your responsibility. Na why Fail2ban and restore process no be optional here. After you put those things in place, take closer look at where self-hosted vault fit actually face attack na the useful next step. Since client don encrypt the entries themselves, the things wey remain to defend na admin token and backup archive.