SSD Nodes Learn Hosting plans →
How to do am Matt ConnorBy Matt Connor

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

A password manager wey na you fully own: Vaultwarden dey run inside one small container behind a reverse proxy wey dey terminate HTTPS, and the official Bitwarden apps for your phone, laptop, and browser dey point to am. Vaultwarden re-implement the Bitwarden server API for Rust and e dey use the same protocol as bitwarden.com, so the official clients go work with am without any change as long as you keep the server current. New client releases fit sometimes need matching Vaultwarden release: 1.37.2 dey required for clients 2026.8.0 and newer. But e dey use about 100 MB RAM instead of the official stack wey need multiple containers.

The install na just about twelve lines of Compose. The three things wey really matter, and wey fit cause problem, na these: TLS must dey ready before you ever load the web vault, you must close public signups immediately your own account don exist, and you must back up and test-restore the data volume, because 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.com to 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

Na the same clients, but e dey use much less resources. The standard official self-hosted Bitwarden deployment dey come as bundle of containers (MSSQL Express, Identity, Api, Admin and more), and e need at least 2 GB of RAM. Bitwarden still get Bitwarden lite, wey be one Docker image and need at least 200 MB of RAM, but dem design am for personal use and home labs, no be for business. E still leave the database and all the backups entirely for you to manage. Vaultwarden na one 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. Because e implement Bitwarden API faithfully, your data fit move between am and bitwarden.com without wahala.

The main thing wey you give up na most enterprise features. E no support SCIM provisioning (though OpenID Connect SSO support land for 1.35.0). You be the operator, so patching, HTTPS, and backups na your responsibility. This guide dey cover these three jobs.

Why HTTPS no be optional

Bitwarden web vault and browser extensions dey derive encryption keys inside browser with Web Crypto API (window.crypto.subtle). Browsers only expose crypto.subtle inside a secure context, like HTTPS, or for the special case of loopback addresses such as http://localhost and http://127.0.0.1. For plain http://vault.example.com, e dey undefined, so as soon as the app derive key, e go 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 fit log in. Desktop, mobile, and browser clients dey run their own check against self-hosted URL. If na http endpoint or endpoint no dey 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 errors get the same cause: valid HTTPS no dey. So we set up TLS first, and we no ever open the vault over http, not even once just to take quick look.

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.com

The 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/vaultwarden

Create a .env for the admin secret and the Compose file inside that directory.

# .env
ADMIN_TOKEN=paste-a-strong-token-here

Generate 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: true

Two 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 vaultwarden

Correct 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, wey be 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 contain $, e fit enter .env directly 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 owasp

E go ask you twice and print string wey start with $argon2id$v=19$.... Na here the trap wey dey make people waste one hour dey: Docker Compose treats $ as variable interpolation, so if you paste the hash directly under environment: for the Compose file, you must double every $ to $$. You fit quote the value there; na the escaping matter:

    environment:
      ADMIN_TOKEN: $$argon2id$$v=19$$m=19456,t=2,p=1$$c29tZXNhbHQ$$RdescudvJCsgt3ub+b+dWRWJTmaaJObG

If you prefer keep the hash for .env, the rule change: leave the single $ signs, wrap the whole value inside single quotes (ADMIN_TOKEN='$argon2id$v=19$...'), and the existing ${ADMIN_TOKEN} line for the Compose file go pick am up. Never double $ for .env. If you leave single $ signs inside the Compose file itself, 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 there, you fit list users, adjust settings, send test email, and take database snapshot.

If the page instead show The admin panel is disabled, please configure the 'ADMIN_TOKEN' variable to enable it, ADMIN_TOKEN dey empty or dem never set am. This one disable the panel completely, and e fit still be correct choice if you never need am. If e 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 wey go protect login endpoint

Vaultwarden dey log every failed login for the file wey LOG_FILE set. Na exactly wetin brute-force guard need be this. If you never dey run Fail2ban before, check installation and basics for 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 }}'

E 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   = 3600

Reload am with sudo systemctl restart fail2ban and confirm am with sudo fail2ban-client status vaultwarden.

Three Docker details decide whether this protection go 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. 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, CF-Connecting-IP behind Cloudflare). Since 1.37.4, when you use X-Forwarded-For, Vaultwarden go take the rightmost address wey no dey inside IP_HEADER_TRUSTED_PROXIES (default na local, wey cover proxy for the same host or Docker network). So, if CDN dey before your proxy, add its addresses there too. Second, the correct iptables chain depend on your proxy. If Traefik dey run as container with published ports, traffic dey pass through Docker FORWARD path. So the ban must dey for DOCKER-USER as above. But if you choose the host-nginx option from Step 1, connections dey end for nginx on the host's INPUT chain, and ban for DOCKER-USER no go see dem. 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 all-ports ban for DOCKER-USER go block the offender cleanly from every published service for 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, you fit capture file wey never finish writing and don corrupt. 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 vaultwarden

Run am from cron every night, then 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 go 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 go show whether the backup real or na hope: 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/server

From your laptop, use ssh -L 8888:127.0.0.1:8888 you@your-vps tunnel reach am and open http://localhost:8888 (or http://127.0.0.1:8888). Because loopback addresses count as secure context, crypto.subtle dey available, and the vault fit decrypt over plain http here. Na the only kind address wey allow that. 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 on 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 suppose never face internet. Na so you fit reach self-hosted open-kritt security scanner for port 5173 too.

Failure modes, with the strings you go see

Cannot read properties of undefined (reading 'importKey') for browser console. Dem load the vault through http, so crypto.subtle no dey defined; access 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, you mistype am, 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 reject the correct password. The Argon2 hash lose its escaping. Every $ must be $$ for the Compose file (but single $ inside single quotes for .env), or you enter 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 this automatically, but nginx need the two upgrade lines from Step 1. The vault still dey work, but e only sync when you open am. The old dedicated port 3012 don comot since v1.31.0, so you no need separate WebSocket route.

Fail2ban report say e ban person, but the attacker still dey connect. E dey ban 127.0.0.1 because IP_HEADER 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 -d

Vaultwarden 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, 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 much 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 loopback test. Bitwarden web vault and extensions dey use browser Web Crypto API, and e only dey available for secure context. So when you use plain http, client go throw Cannot read properties of undefined and e no go ever log in. The only http addresses wey work na loopback ones like http://localhost or http://127.0.0.1. 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 the Invite User button for /admin. SMTP must dey configured so dem fit receive the invitation link. Check the admin user list from time to time to confirm say no unexpected account don appear.

How I fit back up my Vaultwarden vault?

Stop the container for short time and archive the complete 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 the live SQLite file while server dey run, the snapshot fit corrupt, so take am cold. Most important, restore am once to throwaway container and log in, so you know say the backup dey valid before you depend on am.

E really safe to self-host my passwords?

Yes, when you do the three things wey this guide cover: real HTTPS, closed signups plus strong admin token, and tested backups. Your vault dey encrypted client-side with your master password, so even the server no dey see your passwords in clear text. A stolen db.sqlite3 no go reveal your entries without that password. But e still dey hold account emails and salted hash wey dem derive from each master password. Attacker fit try crack am offline, so protect the database file and every copy of am. The trade-off be say patching and backups don become your responsibility. Na why Fail2ban and the restore routine no be optional here. Once dem dey in place, take closer look at where attacker fit really attack self-hosted vault na the useful next step. Since the entries themselves dey encrypted inside the client, the things wey remain to defend na the admin token and backup archive.