Skip to content
Get Started

How to Self-Host Vaultwarden with Docker

Vaultwarden is an AGPL-3.0-licensed, community-developed implementation of the Bitwarden Client API. It works with the official Bitwarden clients, but it is not Bitwarden Inc.’s official server and is not associated with Bitwarden Inc. That distinction matters: you operate the server, track compatibility, and handle recovery.

This guide installs Vaultwarden 1.36.0, the current release when this article was checked on July 14, 2026, on a fresh Debian or Ubuntu VPS. Docker publishes the application only on 127.0.0.1:8080; Caddy handles public HTTPS at vault.example.com. The result supports the web vault and official desktop, browser, and mobile clients without exposing the container’s HTTP port.

If you are comparing several services before choosing one, start with our self-hosted apps for a VPS guide. A password vault deserves more care than a weekend test server.

Prerequisites

You need:

  • a Debian or Ubuntu VPS with root or sudo access;
  • an A record for vault.example.com pointing to the VPS IPv4 address, plus an AAAA record only if IPv6 works end to end;
  • TCP 80 and 443 reachable for Caddy and certificate issuance;
  • a second device with an official Bitwarden client for the sync test;
  • encrypted backup storage outside the VPS.

Secure SSH before putting another login page on the Internet. Our SSH hardening guide uses a test-first order that avoids the usual lockout.

Install Docker Engine and the Compose plugin using Docker’s convenience script. If running a downloaded script as root makes you uneasy, read it before executing it, or install from Docker’s apt repository instead:

curl -fsSL https://get.docker.com -o get-docker.sh
sudo bash get-docker.sh
sudo docker compose version

Check DNS before asking Caddy for a certificate. The answer must be this VPS; omit the AAAA record if the server is not ready to serve IPv6:

getent ahostsv4 vault.example.com

Install Caddy from the distribution repository on a fresh Debian or Ubuntu host. Caddy’s official installation page documents its upstream packages if your distribution does not provide a suitable build.

sudo apt update
sudo apt install -y caddy ufw

Allow the administrative SSH port before enabling UFW. This example assumes SSH still uses port 22:

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose

If SSH uses another port, replace 22 before enabling the firewall. Mirror the same inbound policy in the VPS provider’s cloud firewall or security group. Only SSH and TCP 80/443 should be public. Port 8080 stays on loopback.

Create the Compose deployment

Keep the Compose file and persistent data under /opt/vaultwarden:

sudo install -d -m 700 /opt/vaultwarden
sudo install -d -m 700 /opt/vaultwarden/vw-data
cd /opt/vaultwarden

Create /opt/vaultwarden/compose.yml. The file follows Vaultwarden’s official container and proxy guidance; the exact tag, loopback binding, and log rotation are our choices.

sudo tee compose.yml > /dev/null <<'YAML'
services:
  vaultwarden:
    image: vaultwarden/server:1.36.0
    container_name: vaultwarden
    restart: unless-stopped
    environment:
      DOMAIN: "https://vault.example.com"
      SIGNUPS_ALLOWED: "true"
    volumes:
      - ./vw-data:/data
    ports:
      - "127.0.0.1:8080:80"
    logging:
      options:
        max-size: "10m"
        max-file: "3"
YAML

The exact image tag makes this deployment reproducible. It also means updates are deliberate. Read the Vaultwarden release notes, take a backup, and change the tag yourself when a suitable release is available.

The vaultwarden service listens on port 80 inside the container and appears only at 127.0.0.1:8080 on the VPS. WebSocket traffic has shared the main HTTP port since Vaultwarden 1.31.0, which removed the separate WebSocket port, so 1.36.0 needs only this one port. Do not expose the obsolete WebSocket port 3012 found in old tutorials.

The bind mount maps ./vw-data to /data. It holds the SQLite database, attachments, file Sends, configuration written by Vaultwarden, RSA signing keys, and the icon cache. Losing that directory is losing the service state.

Start the service:

cd /opt/vaultwarden
sudo docker compose up -d
sudo docker compose logs --tail=50 vaultwarden

Confirm the host binding. The output should show 127.0.0.1:8080, not 0.0.0.0:8080:

sudo ss -lntp | grep ':8080'
curl -fsS http://127.0.0.1:8080/alive

Create the first account through an SSH tunnel

Do not publish the Caddy route yet. SIGNUPS_ALLOWED=true keeps registration globally open until you change it; Vaultwarden does not close registration automatically after the first account. The loopback-only Docker binding is the control that keeps this bootstrap page off the Internet.

From your workstation, open an SSH tunnel to the VPS and leave that command running:

ssh -N -L 8080:127.0.0.1:8080 YOUR_USER@SERVER_IP

Browse to http://127.0.0.1:8080, choose Create account, and create the intended account with a unique master password. Store the recovery code for two-step login somewhere separate from this server. Only someone who can authenticate to SSH and establish this tunnel can reach the loopback listener.

Close registration before making the service public:

cd /opt/vaultwarden
sudo sed -i 's/SIGNUPS_ALLOWED: "true"/SIGNUPS_ALLOWED: "false"/' compose.yml
sudo docker compose up -d
curl -fsS http://127.0.0.1:8080/alive

While still using the tunnel, sign out, reload the web vault, and attempt to create a second disposable account. Vaultwarden must deny the registration; if it does not, stop and check compose.yml. Once denial is confirmed, enable two-step login on the intended account and close the tunnel.

Put Caddy in front

The Bitwarden web vault uses Web Crypto APIs that browsers expose only in a secure context. Public use therefore requires HTTPS. Vaultwarden’s HTTPS guidance recommends a reverse proxy rather than the application’s built-in TLS support.

Now that public registration is closed, add an imported Caddy site snippet without replacing any existing hosts:

sudo install -d -m 755 /etc/caddy/sites-enabled
sudo tee /etc/caddy/sites-enabled/vaultwarden.caddy > /dev/null <<'CADDY'
vault.example.com {
    reverse_proxy 127.0.0.1:8080 {
        header_up X-Real-IP {remote_host}
    }
}
CADDY

if ! sudo grep -Eq \
  '^[[:space:]]*import[[:space:]]+/etc/caddy/sites-enabled/\*' \
  /etc/caddy/Caddyfile; then
  printf '\nimport /etc/caddy/sites-enabled/*\n' | \
    sudo tee -a /etc/caddy/Caddyfile > /dev/null
fi

sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
curl -fsS https://vault.example.com/alive

Caddy obtains and renews a public certificate when DNS is correct and ports 80 and 443 reach this server. If issuance fails, check the A/AAAA records, UFW, the provider firewall, and sudo journalctl -u caddy -n 100 --no-pager.

Vaultwarden’s hardening guide notes that WebSocket notification URLs can contain an access token in the query string. Protect proxy logs and avoid configurations that publish full request URLs to a third-party log service.

This base deployment deliberately has no /admin panel. The panel is optional and has a separate authentication boundary. If you later need it, follow the official admin guide, generate an Argon2id PHC token with the image’s hash command, keep it in a mode-600 .env, and restrict /admin at the proxy. Never put a plaintext admin token in Compose or shell history.

Vaultwarden administrators cannot recover a forgotten master password or decrypt vault contents. Account backups and recovery planning still belong to each user.

Verify a real Bitwarden client sync

A running container does not prove the client workflow works. Confirm the public endpoint first:

curl -fsS https://vault.example.com/alive

Install an official Bitwarden client on a second device. Before logging in, choose the self-hosted or custom server option and set the server URL to https://vault.example.com. The label varies slightly by client, but the URL must be set before authentication.

Add a disposable login item in the web vault and trigger sync. Log in from the second client, sync, and confirm that the item appears. Edit its notes in the client, sync again, and verify the change in the web vault. Delete the test item when finished. If you depend on attachments, repeat the test with a harmless attachment.

Back up and test a restore

Vaultwarden’s backup documentation recommends the SQLite online backup API. The image exposes a built-in command:

cd /opt/vaultwarden
sudo docker exec vaultwarden /vaultwarden backup
sudo find vw-data -maxdepth 1 -type f -name 'db.sqlite3*' -ls

That protects the database, but a complete backup must also preserve attachments/, sends/ if used, config.json, and rsa_key*. The icon cache is optional. With everything in a single bind mount, a stopped-container archive covers all of it:

sudo install -d -m 700 /var/backups/vaultwarden
cd /opt/vaultwarden
sudo docker compose stop vaultwarden
sudo tar -C /opt/vaultwarden -czf \
  "/var/backups/vaultwarden/vaultwarden-$(date +%F-%H%M%S).tar.gz" \
  vw-data
sudo docker compose start vaultwarden
curl -fsS https://vault.example.com/alive

Encrypt the archive and copy it off the VPS. Daily backups come with every Riven Cloud plan and are a welcome safety net, but they are no substitute for an application-consistent backup and a rehearsed restore.

Test restoration on an isolated VPS, never over the only working copy. Deploy the same image version and transfer one archive to the test host. Set BACKUP to its real path, then restore it while keeping the empty directory as a rollback point:

export BACKUP=/path/to/vaultwarden-YYYY-MM-DD-HHMMSS.tar.gz
cd /opt/vaultwarden
sudo docker compose stop vaultwarden
sudo mv vw-data vw-data.empty
sudo tar -C /opt/vaultwarden -xzf "$BACKUP"
sudo docker compose up -d
curl -fsS http://127.0.0.1:8080/alive

If restoring an online SQLite .backup rather than the stopped archive, remove any stale unmatched db.sqlite3-wal first. Then log in through HTTPS and repeat the two-client sync and attachment test. Until an archive has been restored at least once, you do not have a recovery plan.

Upgrade without changing versions by accident

Review the release notes for compatibility and migration warnings. Take and test a backup, edit only the image tag in compose.yml, then run:

cd /opt/vaultwarden
sudo docker compose pull
sudo docker compose up -d
sudo docker compose logs --tail=50 vaultwarden
curl -fsS https://vault.example.com/alive

Repeat the official-client sync test. Do not switch to latest to avoid future maintenance; Bitwarden clients update frequently, so Vaultwarden compatibility still needs deliberate attention.

Choosing a Riven Cloud VPS for Vaultwarden

For a private vault, our Premium plan (2 vCPU, 4 GB RAM, 40 GB NVMe, 1 TB monthly transfer) is plenty; Vaultwarden itself is light. If the same VPS will also carry monitoring, SMTP, backup tooling, or other containers, our Ultra plan (2 vCPU, 8 GB RAM, 80 GB NVMe, 2 TB) buys comfortable headroom. Either way, keep the vault isolated from untrusted workloads even when spare RAM makes co-hosting look convenient.

Every plan is an unmanaged KVM VPS with full root access, a 1 Gbps port, and automatic daily backups, so you run the stack yourself and keep full control. Choose Tokyo or Singapore based on where your clients connect from. If that is mainland China, a quick check of the Tokyo Looking Glass and Singapore Looking Glass from your own carrier, at the hours you actually use the vault, will show which location fits better. Then compare the Riven Cloud VPS plans.

Security limits

Keep registration closed, require strong master passwords and two-step login, encrypt off-server backups, and patch both the host and container. Do not mount the Docker socket or unrelated host directories into Vaultwarden. Configure SMTP if you rely on invitations, verification, or mail-based workflows, and protect those credentials with the same care as the vault data.

Rate limiting can reduce noisy login attempts, but it depends on correct forwarded-client-IP handling. It cannot compensate for weak passwords or missing 2FA.

When not to self-host

Use Bitwarden’s hosted service or official self-hosted offering if you need vendor support, formal compliance features, managed availability, or someone else to own incident response. Vaultwarden is also a poor fit when you cannot maintain DNS and HTTPS, install security updates promptly, keep encrypted off-site backups, and rehearse restores. Password storage is the wrong place to learn whether your backup process works.

If that responsibility is more than you want to take on, there is no shame in a managed password service; a vault someone maintains well beats a vault nobody maintains.

Share