Vaultwarden password server running with Docker behind HTTPS on a VPS
Self-hosting

Vaultwarden Docker Compose: Self-Host Your Password Manager

Every password manager asks you for the same thing: trust. You hand over the keys to your email, your bank, your hosting panel and your customers’ admin areas, and then you hope the company storing that vault never gets breached, never doubles its prices and never moves the feature you rely on to a more expensive plan. There is another option. You can run the server yourself, on a VPS you control, and keep using the polished Bitwarden apps and browser extensions you already know. That is exactly what Vaultwarden makes possible. In this guide we will set it up from scratch with Docker Compose, put it behind HTTPS, close the doors that should stay closed and make sure a dead disk never costs you your passwords.

Vaultwarden is an open source project hosted at github.com/dani-garcia/vaultwarden , written in Rust and released under the AGPL-3.0 license. It is an independent implementation of the Bitwarden server API, which means the official Bitwarden clients talk to it as if it were the real thing. It is not affiliated with Bitwarden Inc., so if something misbehaves on your server, report it to the Vaultwarden project, not to Bitwarden support.

What Vaultwarden is and why it fits on a small VPS

A password manager has two halves. The client (browser extension, phone app, desktop app or the web vault in your browser) is where you type your master password, and it is where all encryption and decryption happens. The server only stores encrypted data and keeps your devices in sync. This model is called end-to-end encryption, sometimes “zero-knowledge”: the server never sees your master password and never sees your passwords in clear text. That is the reason self-hosting a password manager is a reasonable idea in the first place. Your server is a sync hub for data it cannot read.

The official Bitwarden server is built for companies: around a dozen containers, a Microsoft SQL Server database and a documented minimum of 2 GB of RAM. Vaultwarden reimplements the same API in one small program that keeps its data in SQLite, a database that lives in a single file on disk. The result idles at a few tens of megabytes of RAM and is perfectly happy on the smallest VPS you can buy.

You also get features that cost money on the official cloud service: organizations for sharing passwords with family or a small team, file attachments, the built-in TOTP code generator, Bitwarden Send and emergency access. No license key needed.

What Docker and Docker Compose do for us

A container is an isolated package that holds an application together with everything it needs to run (libraries, runtime, default config), sharing the host’s Linux kernel instead of booting a full virtual machine. Docker is the tool that downloads these packages (called images) and runs them. Docker Compose lets us describe several containers, their settings and their storage in one YAML file, then start or update the whole set with a single command. For us that means two containers: Vaultwarden itself and Caddy, a web server that handles HTTPS for it.

Approximate RAM use at idleVaultwarden~50 MBCaddy~35 MBVaultwarden + Caddy~85 MBBitwarden official~2 GB min.05001000150020002500RAM in MB (approximate)SetupTypical idle values on a small VPS; the Bitwarden bar is its documented minimum requirement.
Approximate idle memory: Vaultwarden plus Caddy need well under 100 MB, while the official Bitwarden server documents 2 GB as its minimum.

What you need

  • A VPS running Debian 13 or Ubuntu 24.04, with a user that can run sudo. One vCPU and 1 GB of RAM is more than enough for a family or a small team; 512 MB works too.
  • A KVM VPS is the easy path. Docker also runs in an LXC container, but only if nesting is enabled on the host. In an unprivileged LXC container without nesting (and often keyctl), Docker fails with permission errors.
  • A domain name where you can add DNS records. We will use vault.example.com everywhere; replace it with your own.
  • SSH access to the server and about 30 to 45 minutes.
  • An email address for Let’s Encrypt expiry notices (optional, recommended).
  • A place for off-site backups: a second VPS, a NAS at home or any storage that is not the same server.

How the pieces fit together

Before typing commands, it helps to know what each layer does. A port is a numbered door on the server’s network address: SSH listens on port 22, plain HTTP on 80, HTTPS on 443. A firewall decides which doors are open. A reverse proxy is a web server that receives every request from the internet, handles HTTPS encryption and forwards the request to the right application behind it. TLS is the encryption behind the padlock in your browser, and Let’s Encrypt is the free authority that issues the certificate. A volume (here simply a folder on the host) is where a container keeps data that must survive restarts and upgrades.

Vaultwarden itself will not be reachable from the internet at all. Only Caddy publishes ports, and it talks to Vaultwarden over a private network that Docker Compose creates automatically.

How requests reach your vaultYour devicesBrowserMobile appDesktop appWeb vault443Firewallufw22, 80, 443openDocker Compose stack443Caddyreverse proxyTLS certificateLet's Encrypt:80Vaultwardenweb vault + APIport 80not published/data./vw-datadb.sqlite3attachments/rsa_key.pemNightly backupcron + sqlite3Off-site copysecond serverOnly Caddy publishes ports 80 and 443.Vaultwarden is reachable only insidethe private Docker network.
The full path: your devices reach Caddy over HTTPS, Caddy forwards to Vaultwarden on the private Docker network, and the data folder is backed up every night to another machine.

The plan: nine steps from a blank VPS to a working vault

Here is the whole procedure at a glance. Each card matches one “Step” heading below, in the same order.

Vaultwarden setup in 9 steps1Step 1: DNSA record to the VPS IP2Step 2: DockerEngine + Compose v23Step 3: Firewallallow 22, 80, 4434Step 4: Admin tokenArgon2 hash5Step 5: ComposeVaultwarden + Caddy6Step 6: First startcreate your account7Step 7: Lock downsignups off, /admin8Step 8: Appsself-hosted server URL9Step 9: Backupnightly + off-site
The nine steps of this guide: Step 1 to Step 9 match the numbered section headings below, in the same order.

Step 1: Point a subdomain at your VPS

Caddy will request a certificate from Let’s Encrypt, and Let’s Encrypt will only issue it if the name really points to your server. So DNS comes first.

In your domain’s DNS panel, create an A record named vault (giving vault.example.com) with your VPS’s IPv4 address as its value. Add an AAAA record only if your VPS has working IPv6; a wrong AAAA record is one of the most common reasons certificate issuance fails.

Check from the VPS that the name resolves:

getent hosts vault.example.com

You should see your VPS IP followed by the name. If you get nothing back, wait a few minutes for DNS propagation and try again. Do not move on until this works.

Step 2: Install Docker Engine and the Compose plugin

Debian and Ubuntu ship their own Docker packages, but we use Docker’s official repository so we get a current Engine and the Compose v2 plugin (the docker compose command, with a space). The commands below work on both Debian 13 and Ubuntu 24.04, because $ID and $VERSION_CODENAME are read from /etc/os-release and resolve to debian/trixie or ubuntu/noble.

sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
. /etc/os-release
sudo curl -fsSL "https://download.docker.com/linux/$ID/gpg" -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/$ID $VERSION_CODENAME stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Verify the installation:

sudo docker run --rm hello-world
sudo docker compose version

The first command prints a message starting with “Hello from Docker!”, the second prints something like Docker Compose version v2.x.x. In this guide we prefix Docker commands with sudo. You could add your user to the docker group instead, but be aware that membership in that group is effectively root access.

Step 3: Configure the firewall

We want exactly three things reachable from outside: SSH (22) so you can manage the server, HTTP (80) so Let’s Encrypt can validate and Caddy can redirect to HTTPS, and HTTPS (443, TCP and UDP for HTTP/3). Ubuntu 24.04 ships ufw installed but inactive; Debian 13 does not install it by default, so the apt install line below is needed there and harmless on Ubuntu.

sudo apt install -y ufw
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw enable
sudo ufw status verbose

If your SSH daemon listens on a different port, allow that port instead of 22 before running ufw enable, or you will lock yourself out.

One important detail: ports published by Docker bypass ufw, because Docker writes its own firewall rules. That is why, in Step 5, the Vaultwarden container publishes no port at all. Only Caddy publishes 80 and 443, which we want open anyway.

Step 4: Generate a hashed admin token

Vaultwarden has an admin panel at /admin where you can see users, invite people, delete accounts and read diagnostics. It is enabled by setting the ADMIN_TOKEN variable. You could put a plain password there, but Vaultwarden supports (and recommends) an Argon2 hash instead, so the real password never sits in a config file.

First, generate a strong random password for the admin panel and store it somewhere safe (on paper, or in an existing password manager until your new one is ready):

openssl rand -base64 32

Then let Vaultwarden hash it. We pin a specific image version here and use the same tag in the Compose file. 1.34.3 was current when we tested this guide; check the releases page and use the newest stable tag.

sudo docker run --rm -it vaultwarden/server:1.34.3 /vaultwarden hash

Type the password twice when asked. The output ends with a line like this:

ADMIN_TOKEN='$argon2id$v=19$m=65540,t=3,p=4$Wk1x...long...string'

Docker Compose treats every $ in the YAML file as the start of a variable, so each $ in the hash must be doubled. Let sed do it for you (paste only the part between the single quotes):

echo '$argon2id$v=19$m=65540,t=3,p=4$Wk1x...long...string' | sed 's/\$/$$/g'

The result starts with $$argon2id$$v=19.... Keep it handy for the next step.

Step 5: Write the Docker Compose file and Caddyfile

We keep everything in one folder, /opt/vaultwarden:

sudo mkdir -p /opt/vaultwarden
cd /opt/vaultwarden
sudo nano compose.yaml

Paste the following, replacing the domain and the escaped hash:

services:
  vaultwarden:
    image: vaultwarden/server:1.34.3
    container_name: vaultwarden
    restart: unless-stopped
    environment:
      DOMAIN: "https://vault.example.com"
      SIGNUPS_ALLOWED: "true"
      ADMIN_TOKEN: "$$argon2id$$v=19$$m=65540,t=3,p=4$$REPLACE_WITH_YOUR_ESCAPED_HASH"
      TZ: "Europe/Bucharest"
    volumes:
      - ./vw-data:/data

  caddy:
    image: caddy:2
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - ./caddy-data:/data
      - ./caddy-config:/config
    depends_on:
      - vaultwarden

What each part does:

  • image pins Vaultwarden to an exact version, so an update only happens when you decide. For Caddy we use the caddy:2 tag, which follows the 2.x line and stays config-compatible; you can pin an exact version too if you prefer. Using :latest everywhere is convenient but means a random restart may pull a new major version.
  • DOMAIN must be the full public URL with https://. Vaultwarden uses it to build links and for features like WebAuthn; a wrong value causes strange login errors.
  • SIGNUPS_ALLOWED: "true" is temporary, just long enough to create your own account. We close it in Step 7.
  • ./vw-data:/data stores the database, keys and attachments in /opt/vaultwarden/vw-data on the host. This folder is your vault: back it up.
  • No ports: under vaultwarden. It listens on port 80 inside the private network, where Caddy reaches it by name.

Now create the Caddyfile:

sudo nano Caddyfile
{
    email you@example.com
}

vault.example.com {
    encode zstd gzip

    reverse_proxy vaultwarden:80 {
        header_up X-Real-IP {remote_host}
    }
}

The global block gives Let’s Encrypt an address for expiry notices. The site block tells Caddy to obtain a certificate for vault.example.com, compress responses, and forward everything to the vaultwarden container, passing the visitor’s real IP so Vaultwarden logs it correctly. Live sync between devices uses WebSockets, which Caddy forwards automatically; since Vaultwarden 1.29 they share port 80, so the old separate port 3012 you may see in older guides is no longer needed.

Step 6: Start the stack and create your account

cd /opt/vaultwarden
sudo docker compose up -d
sudo docker compose ps

Docker downloads both images and reports Container vaultwarden Started and Container caddy Started. After about a minute, docker compose ps should show vaultwarden as Up (healthy) and caddy as Up. Watch Caddy fetch the certificate:

sudo docker compose logs -f caddy

Look for a line containing certificate obtained successfully, then press Ctrl+C. A quick test from the server:

curl -I https://vault.example.com

The first line should read HTTP/2 200.

Open https://vault.example.com in your browser, choose Create account, enter your email, a name and your master password. The master password is the one thing nobody can recover for you: not Vaultwarden, not us, not you with root access to the server, because the vault is encrypted with it on your device. Pick a long passphrase and write it down somewhere physically safe. Once logged in, go to Settings, Security, Two-step login and enable an authenticator app.

Step 7: Close signups and lock down the admin panel

As long as signups are open, anyone who finds your URL can create an account on your server. Edit compose.yaml and change one line:

      SIGNUPS_ALLOWED: "false"

Apply it:

sudo docker compose up -d

Compose notices the change and recreates only the vaultwarden container. Test in a private browser window: trying to create a new account now fails with an error saying registration is not allowed.

Need to add family or colleagues later? Open https://vault.example.com/admin, log in with the plain admin password from Step 4 (not the hash), and use Invite User. Without email (SMTP) configured, the invited person simply goes to the web vault and creates an account with exactly that address. Organization owners can also invite members; set INVITATIONS_ALLOWED: "false" if you want only the admin panel to do that.

Two things to know about the admin panel:

  • Settings saved in the panel win. When you click Save in /admin, Vaultwarden writes a config.json file in vw-data, and its values override the environment variables in compose.yaml. If a variable change “does nothing”, check that file.
  • Reduce who can even see /admin. If you manage the server from a fixed IP, let Caddy refuse everyone else. Update the site block in your Caddyfile:
vault.example.com {
    encode zstd gzip

    @blockedadmin {
        path /admin*
        not remote_ip 203.0.113.10
    }
    respond @blockedadmin 403

    reverse_proxy vaultwarden:80 {
        header_up X-Real-IP {remote_host}
    }
}

Replace 203.0.113.10 with your own public IP, then apply it with sudo docker compose up -d --force-recreate caddy. If you do not have a fixed IP, a simpler option is to remove the ADMIN_TOKEN line entirely once setup is done: without it, the admin panel is disabled. Add it back when you need it.

Step 8: Connect the Bitwarden apps and extensions

Install the official Bitwarden browser extension, mobile app or desktop app from the usual stores. On the login screen, look for the server or region selector (it usually shows bitwarden.com by default) and choose Self-hosted. In Server URL, enter https://vault.example.com, without any path at the end, and save. Then log in with your email and master password.

That is all. The extension autofills, the phone app offers to save passwords, and changes made on one device appear on the others within seconds thanks to the WebSocket connection. To bring passwords over from another manager, export them there and import the file in the web vault under Tools, Import data. Delete the unencrypted export file afterwards.

Step 9: Back up the vault (and test the restore)

A self-hosted password manager without backups is a disaster waiting for a date. Everything lives in vw-data: db.sqlite3 (the database), rsa_key.pem (the key that signs login tokens), attachments/, sends/ and, if you used the admin panel, config.json. The icon_cache folder can be skipped; it refills itself.

Copying a live SQLite file with cp can produce a corrupt copy, so we use SQLite’s own online backup command. Install the tools (cron is preinstalled on Ubuntu; on minimal Debian images it may be missing):

sudo apt install -y sqlite3 cron

Create the backup script:

sudo tee /usr/local/bin/vaultwarden-backup.sh > /dev/null <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

SRC="/opt/vaultwarden/vw-data"
DEST="/var/backups/vaultwarden"
STAMP="$(date +%F-%H%M)"
WORK="$DEST/$STAMP"

mkdir -p "$WORK"
chmod 700 "$DEST"

# Consistent copy of the database, safe while Vaultwarden is running
sqlite3 "$SRC/db.sqlite3" ".backup '$WORK/db.sqlite3'"

# Keys, attachments, sends and admin panel settings
cp -a "$SRC"/rsa_key* "$WORK/"
for item in attachments sends config.json; do
  if [ -e "$SRC/$item" ]; then
    cp -a "$SRC/$item" "$WORK/"
  fi
done

tar -czf "$DEST/vaultwarden-$STAMP.tar.gz" -C "$DEST" "$STAMP"
rm -rf "$WORK"

# Keep 14 days of local archives
find "$DEST" -maxdepth 1 -name 'vaultwarden-*.tar.gz' -mtime +14 -delete
EOF
sudo chmod 700 /usr/local/bin/vaultwarden-backup.sh

Run it once by hand and look at the result:

sudo /usr/local/bin/vaultwarden-backup.sh
sudo ls -lh /var/backups/vaultwarden

You should see a file like vaultwarden-2026-11-10-0330.tar.gz. Now schedule it every night at 03:30:

echo '30 3 * * * root /usr/local/bin/vaultwarden-backup.sh' | sudo tee /etc/cron.d/vaultwarden-backup

A backup on the same disk does not survive the loss of that server, so copy the archives elsewhere. The safest pattern is to let a second machine pull them over SSH with rsync, so a compromised VPS cannot delete its own off-site copies. Treat the archives as sensitive: vault items are encrypted, but the database still holds account emails and metadata.

Finally, test a restore at least once, ideally on a second VPS with the same Compose files:

cd /opt/vaultwarden
sudo docker compose down
sudo mv vw-data vw-data.old
sudo mkdir vw-data
sudo tar -xzf /var/backups/vaultwarden/vaultwarden-2026-11-10-0330.tar.gz -C /tmp
sudo cp -a /tmp/2026-11-10-0330/. vw-data/
sudo sqlite3 vw-data/db.sqlite3 "PRAGMA integrity_check;"
sudo docker compose up -d

The integrity check should print ok. Log in and confirm your items are there. As an extra safety net, export an encrypted copy of your vault from the web vault (Tools, Export vault, encrypted JSON) every few months and keep it offline.

Updating Vaultwarden later

Bitwarden releases new client versions often, and occasionally a new client needs a newer server. Updating is quick: take a backup, change the image tag in compose.yaml to the new release, then pull and recreate.

cd /opt/vaultwarden
sudo /usr/local/bin/vaultwarden-backup.sh
sudo nano compose.yaml
sudo docker compose pull
sudo docker compose up -d
sudo docker image prune -f

Read the release notes before jumping several versions at once. For the operating system, Ubuntu enables unattended-upgrades by default; on Debian 13 install it with sudo apt install unattended-upgrades. After kernel updates Ubuntu creates /var/run/reboot-required; Debian does not create that file reliably, so install needrestart there to see when services or a reboot are needed.

Troubleshooting and common mistakes

  • Caddy logs show certificate errors. DNS does not point to the VPS yet, port 80 is blocked, or a wrong AAAA record sends Let’s Encrypt to the wrong place. Fix the cause and run sudo docker compose restart caddy. Do not restart in a tight loop; Let’s Encrypt has rate limits.
  • Compose prints warnings like “The “argon2id” variable is not set”. You forgot to double the $ signs in ADMIN_TOKEN. Repeat the sed command from Step 4.
  • The admin panel rejects your token. You must type the plain password, not the hash. If you changed the token after saving settings in the panel, check vw-data/config.json, which overrides the environment.
  • Changing SIGNUPS_ALLOWED has no effect. Same cause: a value saved from /admin lives in config.json and wins.
  • Apps fail to log in, but the web vault works. Check that DOMAIN is exactly https://vault.example.com and that the app’s Server URL has no trailing path. Also make sure Vaultwarden is not too old for a freshly updated client.
  • Port 80 or 443 already in use. Another web server (nginx, Apache) is running on the host. Stop it, or proxy Vaultwarden through that server instead of Caddy.
  • Docker fails with permission errors inside LXC. Nesting is not enabled for the container. Ask support to enable it, or use a KVM VPS.

Security notes

  • Your master password is the real lock. The server stores only encrypted data, so a strong master password plus two-step login protects you even if the server were compromised.
  • Vaultwarden is never exposed directly. Only Caddy publishes ports, and Docker’s rules bypass ufw, so keep it that way: do not add a ports: section to the vaultwarden service.
  • Admin access is hashed and limited. Argon2 token, optional IP restriction in Caddy, or no admin panel at all when you do not need it.
  • Harden SSH. Use key-based login, disable password authentication and consider fail2ban.
  • Patch regularly. Keep both the OS and the Vaultwarden image updated.
  • Backups are sensitive data. Store them off-site, restrict who can read them and test restores.

So, is self-hosting your passwords worth it?

If you want the convenience of Bitwarden’s apps without trusting a third-party cloud with your vault, Vaultwarden is the most practical way to get there. It runs in well under 100 MB of RAM, the setup above takes less than an hour, and the result is a private password server with HTTPS, closed signups, a protected admin panel and nightly backups. The responsibility moves to you, which is exactly why backups and updates are part of the procedure rather than an afterthought.

You can run this exact stack on a VPS from our own data center in Romania, or let our server administration team install, harden, back up and keep your Vaultwarden instance updated for you.

FAQ

Is Vaultwarden an official Bitwarden product?

No. It is an independent open source server that implements the Bitwarden API. The official Bitwarden apps work with it, but bugs on your server should be reported to the Vaultwarden project.

Do I need to pay for Bitwarden Premium to use attachments or TOTP?

No. Vaultwarden unlocks these features for every account on your server. The client apps are free.

What happens if my VPS goes down?

Devices that are already logged in keep a cached, encrypted copy of the vault, so you can still read your passwords. Syncing and saving changes resume when the server is back.

How many users can a small VPS handle?

A family or a team of a few dozen people is comfortable on a 1 GB VPS. Vaultwarden’s memory use grows slowly, and SQLite handles this scale without problems.

Can I move from Bitwarden cloud to my own server?

Yes. Export your vault from the cloud web vault, import the file in your Vaultwarden web vault, then switch the Server URL in your apps to your own domain.

Trusted By & Member Of

We are proud members of leading internet infrastructure organizations.

RIPE NCC MANRS PeeringDB RoTLD DSIX SBIX 4IXP LOCIX Euro-IX RIPE NCC MANRS PeeringDB RoTLD DSIX SBIX 4IXP LOCIX Euro-IX