Skip to content

Command Palette

Search for a command to run...

How to Deploy Vaultwarden on a VPS

Updated Jul 2026

Self-host Vaultwarden — a lightweight, Bitwarden-compatible password manager and a 1Password/Bitwarden-cloud alternative — on your own VPS with Docker and HTTPS.

Before you start
  • A small VPS — 1 vCPU / 1 GB RAM is plenty (Vaultwarden is tiny)
  • A fresh Ubuntu 24.04 server with root/sudo SSH access
  • A domain you can point at the server — HTTPS is mandatory for Bitwarden clients
  • Docker Engine + Compose installed (see the base guide below)

What Vaultwarden is

Vaultwarden is a lightweight, self-hostable server that implements the Bitwarden API — a drop-in alternative to Bitwarden's cloud and to 1Password, running on a box you own. It speaks the same protocol the official Bitwarden apps do, so the browser extensions, desktop apps, and mobile apps all connect to it unchanged. You point a client at your server's URL instead of bitwarden.com, and everything else works the way you already know.

The distinction worth understanding up front: Vaultwarden is not Bitwarden's own self-hosted server. Bitwarden ships an official self-host stack too, but it's a heavy, multi-container deployment (SQL Server, an identity service, several supporting containers) that wants real memory and CPU. Vaultwarden is an unofficial, independent reimplementation of the server in Rust — one small container, a SQLite database by default, and a memory footprint measured in tens of megabytes. It's API-compatible with the official clients, but it's a separate project with its own maintainers. For a personal, family, or small-team vault, Vaultwarden is the pragmatic choice: it does what the official server does at a fraction of the resource cost.

The appeal is ownership. Your passwords live on hardware you control, behind a certificate you issued, in a database you back up — not in a vendor's cloud on a per-seat subscription. And because the server is so light, it's genuinely one of the cheapest things you can self-host.

One thing to be clear-eyed about before you start: this is your entire credential store. The whole point of the exercise is that you're responsible for it now. That means HTTPS is not optional, open signups are a real risk, and your backups are the difference between a lost server and a lost life's worth of logins. Every section below leans on those three facts. None of it is hard — it's just non-negotiable.

Server sizing — why a 1 GB box is plenty

Vaultwarden is one of the lightest things you'll ever self-host. The server is a single Rust binary that idles at a few tens of megabytes of RAM and barely touches the CPU. The SQLite database is small — a vault with hundreds of entries is a few megabytes on disk — and it grows meaningfully only if you and your users store large file attachments.

So the realistic floor is 1 vCPU / 512 MB–1 GB RAM, and a 1 GB box has room to spare. You are not sizing for Vaultwarden's own appetite; you're sizing for a reliable, always-on host and a little headroom for the reverse proxy that sits in front of it. Disk needs are minimal — 10–20 GB is comfortable unless you expect heavy attachment use.

The economics are the fun part: a password manager for your whole family or a small team costs a couple of dollars a month to run this way, flat, no per-seat billing. That said — don't chase the very cheapest, least reliable host just because the workload is tiny. This is your credentials. You want a provider with dependable uptime, real backups of the underlying disk, and a network you trust. A Hetzner CX22 is comfortable overkill at a low price; Kamatera lets you size a small instance precisely and scale only if attachments grow. Both are covered at the end.

Prepare the server

This guide assumes Docker Engine and the Compose plugin are already installed, along with a non-root deploy user, a ufw firewall, and unattended security updates. If that's not done yet, work through Docker & Compose on Ubuntu first — it's the base layer for every app on this site, and it sets up exactly that baseline.

With that in place, confirm the firewall exposes only what it should. Vaultwarden itself must never be published directly to the internet — the reverse proxy terminates HTTPS and is the only thing that talks to the container. So you want SSH plus the HTTP/HTTPS ports open, and nothing else:

sudo ufw status verbose

You should see OpenSSH, 80/tcp, and 443/tcp allowed, everything else denied. Ports 80 and 443 are for the reverse proxy (80 is needed for the Let's Encrypt certificate challenge; 443 serves the vault). The Vaultwarden container's own port stays internal — more on that in the next two sections.

Install Vaultwarden (Docker Compose)

Vaultwarden ships as an official image, vaultwarden/server:latest. It runs from a single persistent volume mounted at /data, and that volume is the whole game: it holds the SQLite database (db.sqlite3), file attachments, Bitwarden "sends", the config.json written by the admin panel, and — critically — the RSA keypair that signs authentication tokens. Lose /data and every client is logged out permanently; lose it without a backup and you can lose the vaults themselves. Treat that directory as the most important thing on the box.

Create a project folder as the deploy user and write the compose file:

mkdir ~/vaultwarden && cd ~/vaultwarden
# docker-compose.yml
services:
  vaultwarden:
    image: vaultwarden/server:latest
    container_name: vaultwarden
    restart: unless-stopped
    environment:
      # Full external URL — required. Used for WebAuthn, attachment links, and
      # every URL the server generates. Must match how clients reach you.
      DOMAIN: "https://vault.example.com"
      # Open registration ONLY long enough to create your own account (see below).
      SIGNUPS_ALLOWED: "true"
      # Gate the /admin panel with an Argon2 hash, not a plaintext token (see below).
      # ADMIN_TOKEN: "$argon2id$v=19$m=..."
    volumes:
      - ./vw-data:/data
    ports:
      # Bind to localhost only — the reverse proxy is the sole thing that reaches it.
      # The container listens on port 80 *inside* the container.
      - "127.0.0.1:8080:80"

Two details matter here. The container listens on port 80 internally — that's Vaultwarden's own HTTP port, and it is plain HTTP, which is exactly why it must never face the internet directly. The 127.0.0.1:8080:80 mapping publishes it only on the loopback interface, so the reverse proxy on the same host can reach it at localhost:8080 while the outside world cannot. Never map it to 0.0.0.0. Bring it up:

docker compose up -d
docker compose logs -f

The logs should show Vaultwarden starting and, on first run, generating the RSA keys into ./vw-data. At this point the server is running but only reachable from the box itself — which is correct. Next, put HTTPS in front of it.

HTTPS + domain (mandatory)

This is the step that makes or breaks the deploy, so it gets its own emphasis: the Bitwarden clients refuse to connect to a server that isn't served over HTTPS. This is a hard requirement in the official apps and extensions, not a warning you can click through — the only exception is localhost during development. A Vaultwarden server on plain http:// is, for practical purposes, a server no real client will talk to. HTTPS is the deploy.

Point DNS first: create an A record for your vault hostname (say vault.example.com) pointing at the server's public IP. Wait for it to resolve before you request a certificate — the ACME challenge can't validate a name that doesn't point at your box yet.

The recommended reverse proxy is Caddy — it fetches and renews Let's Encrypt certificates automatically with almost no configuration, which is exactly what you want in front of something this important. Follow Automatic HTTPS with Caddy for the full setup; the Caddyfile entry for Vaultwarden is a single reverse-proxy block pointing at the localhost port you published above:

vault.example.com {
    reverse_proxy 127.0.0.1:8080
}

That's the whole config. Caddy terminates TLS on 443, obtains the certificate on first request, renews it on its own, and forwards decrypted traffic to Vaultwarden on the loopback port.

One point that trips people up on older guides: live sync works over the same HTTPS port. Vaultwarden delivers real-time notifications (a change on one device pushing to another) over WebSockets, and in current versions those run through the same connection as everything else — no separate port, no extra proxy block, no special WebSocket routing. If you've read tutorials mentioning a dedicated 3012 port, that's obsolete. One hostname, one certificate, done.

First-run setup

With HTTPS live, open https://vault.example.com in a browser. You should see the Bitwarden-style web vault, served with a valid certificate.

Register your own account immediately. Because you set SIGNUPS_ALLOWED=true, the "Create account" flow is open. Create your account now — this is the one and only reason that flag is on.

Then close signups — this is the single most important hardening step in the whole guide. An internet-facing password server with open registration is the classic, serious mistake: anyone who finds your URL can create an account on your server. The moment your own account exists, edit the compose file and turn registration off:

    environment:
      DOMAIN: "https://vault.example.com"
      SIGNUPS_ALLOWED: "false"

Then redeploy so the change takes effect:

docker compose up -d

Now the server is closed. You (and anyone you invite through the admin panel or SMTP invites) can still log in and use it fully; the public can no longer register. Verify by loading the site in a private window — the "Create account" option should be gone.

Finally, connect a real client. In the Bitwarden browser extension, desktop, or mobile app, before logging in, open the settings (often a gear icon on the login screen) and set the Server URL / Self-hosted field to https://vault.example.com. Then log in with the account you just created. The client stores your vault locally and syncs through your server from then on.

Hardening

You've already done the two biggest things — HTTPS in front, signups closed. A few more habits make the deployment genuinely solid:

  • Keep signups closed. It's worth repeating: SIGNUPS_ALLOWED=false is the steady state. If you need to add someone, either send them an invite from the admin panel / via SMTP, or flip signups on briefly, have them register, and flip it off again the same way you did for yourself.

  • Gate the admin panel with an Argon2 hash, not a plaintext token. The /admin panel manages users, diagnostics, and settings, so it must be protected. Rather than putting a plaintext ADMIN_TOKEN in your compose file, generate an Argon2 PHC hash and use that — the server compares against the hash, so the raw secret never sits in your config or environment. Generate one with the image itself:

    docker run --rm -it vaultwarden/server /vaultwarden hash

    It prompts for a password and prints an $argon2id$... string. Put that string in ADMIN_TOKEN (mind the shell escaping — the hash contains $ characters, so single-quote it in YAML or an env file), then redeploy. If you'd rather not expose /admin at all, leave ADMIN_TOKEN unset — with no token configured, the admin panel is disabled entirely, which is a perfectly safe default for a set-and-forget personal server.

  • Rate-limit login attempts. Vaultwarden has built-in login rate limiting, and because a password server is a natural brute-force target, it's worth pairing with fail2ban on the host to ban IPs that hammer the login endpoint. The Vaultwarden wiki documents a ready-made fail2ban filter that reads its logs; at minimum, keep the built-in limits on.

  • Keep it patched. Vaultwarden is actively developed and security fixes land in new image tags. Don't let :latest drift for a year — pull updates deliberately (see Upgrades below) and skim the release notes.

Backups

Say it plainly: /data is everything. Your vaults, attachments, sends, the config.json, and the RSA signing keys all live in that one directory (mounted as ./vw-data on the host). A good backup captures all of it, off the box, and encrypted — because it is your vault, a backup that leaks is as bad as a database that leaks.

The one subtlety is the SQLite database. Do not just copy db.sqlite3 while the container is running — a live SQLite file can be mid-write, and a raw copy can be corrupt or inconsistent. Use SQLite's own online-backup command, which produces a clean, consistent snapshot even while the server is running:

# Consistent snapshot of the live SQLite database:
docker compose exec vaultwarden \
  sqlite3 /data/db.sqlite3 ".backup '/data/db-backup.sqlite3'"

Then archive that snapshot alongside the rest of /data — the attachments/ and sends/ folders, the rsa_key* files, and config.json — and encrypt the whole thing before it leaves the box:

cd ~/vaultwarden
tar czf - \
  vw-data/db-backup.sqlite3 \
  vw-data/attachments \
  vw-data/sends \
  vw-data/rsa_key* \
  vw-data/config.json \
  | gpg --symmetric --cipher-algo AES256 \
      -o "vaultwarden-$(date +%F).tar.gz.gpg"

Copy the resulting .gpg file off the server — an object-storage bucket, another machine, anywhere that survives this box dying. A backup that only lives on the same VPS dies with the VPS. Automate this on a schedule (a cron job that runs the .backup command and ships the encrypted archive), and — the part everyone skips — test a restore at least once. Stand up a throwaway Vaultwarden with the restored /data and confirm you can log in. An untested backup of your password vault is a guess you really don't want to be making on the worst day.

Upgrades

Because Vaultwarden uses the :latest tag, upgrading is a deliberate pull, not an automatic one — you decide when:

cd ~/vaultwarden
docker compose pull
docker compose up -d

pull fetches the newer image; up -d recreates the container against your unchanged /data volume, so your vault survives the upgrade untouched. Run a backup first (previous section), and skim the project's release notes before a version jump — Vaultwarden is stable, but it's an actively moving project and the occasional release changes a default. If you want tighter control, pin to a specific version tag (for example vaultwarden/server:1.32.0) instead of :latest, and bump it on your own schedule.

Troubleshooting

The Bitwarden client won't connect / "Cannot reach server". Almost always HTTPS. The clients refuse a non-HTTPS server, so confirm https://vault.example.com loads in a browser with a valid certificate — a self-signed or expired cert will also be rejected by the apps even though a browser might let you click through. Check that DNS resolves to your box (dig vault.example.com), that ports 80 and 443 are open (including any cloud firewall in front of ufw), and that the Server URL in the client exactly matches your DOMAIN, https:// and all.

Can't register / "Registration not allowed". That's SIGNUPS_ALLOWED=false doing its job — which is what you want in normal operation. To add an account, invite the user from the admin panel or via SMTP, or briefly set SIGNUPS_ALLOWED=true, redeploy, register, then set it back to false and redeploy again.

The admin panel returns a 500 / won't accept the token. Usually a malformed ADMIN_TOKEN. If you're using an Argon2 hash, the $ characters in it must not be mangled by shell or YAML interpolation — single-quote the value (or store it in an env file) so it's passed literally. Regenerate the hash with docker run --rm -it vaultwarden/server /vaultwarden hash if in doubt, and check docker compose logs vaultwarden for the specific parse error.

Everyone got logged out after a restore or move. The RSA keys in /data (rsa_key*) sign auth tokens — if they changed or went missing, existing sessions are invalidated. Restore the complete /data, keys included; users can always log in again with their master password, but their vaults are only recoverable if the database came back with them.

Verification + next steps

You're done when you can: load https://vault.example.com with a valid certificate, log in from a real Bitwarden client pointed at your server URL, confirm public registration is closed (no "Create account" in a private window), and produce an encrypted, off-box backup that you've restored at least once.

From there it's genuinely low-maintenance: patch the image now and then, keep the backups flowing, and enjoy a password manager for your whole household or team that costs a couple of dollars a month and answers to no one but you. The only thing underneath it that matters is a reliable host — this is your credentials, after all. A Hetzner CX22 is the value pick and comfortable overkill for a workload this small; reach for Kamatera when you want to size the instance precisely and scale only if attachments grow. See Best VPS for self-hosting for the ranked picks.

Next steps

We use analytics cookies (Google Analytics, PostHog) to see which guides are useful. No ad networks, no cross-site tracking. See our privacy policy.