Docker & DevOps

Self-host Vaultwarden with Docker Compose and HTTPS

Run a Bitwarden-compatible Vaultwarden server in Docker Compose on a Linux VPS, put HTTPS in front of it, lock down signups and set up a backup you can restore.

IntermediateLinux server7 min read

To self-host Vaultwarden with Docker Compose you run a single container that speaks the Bitwarden API, then put a TLS reverse proxy in front of it so official Bitwarden apps and extensions can sync over HTTPS. This guide builds the stack, creates your first account, secures the admin page, and sets up a backup you have actually tested. The whole setup fits on a small Linux VPS with Docker installed.

Vaultwarden keeps its state in one volume: the SQLite database, your attachments, and the RSA keys clients use to encrypt vault data. Everything below is built around protecting that directory.

Before you start

You need:

  • A Linux VPS (Ubuntu 22.04/24.04, Debian 12, AlmaLinux or Rocky 8–9) with root or sudo access.
  • Docker Engine and the Compose plugin installed. If not, follow install Docker and Docker Compose on a Linux VPS.
  • A DNS record for the hostname you will use, for example vault.example.com pointing at 203.0.113.10. HTTPS certificates cannot be issued until this resolves.
  • Ports 80 and 443 open in your firewall, and port 8080 kept closed externally.

Log in with SSH and confirm Docker works:

Bash
docker --version
docker compose version

Warning: Vaultwarden holds the keys to your password vault. Treat the server as sensitive from the first minute: no plain HTTP exposure, no shared root logins, and no public admin page without a token.

Write the Compose file

Create the project directory and the data folder. Vaultwarden runs as a non-root user inside the container, so the mounted directory must be writable by it.

Bash
sudo mkdir -p /opt/vaultwarden/vw-data
cd /opt/vaultwarden

Create compose.yaml:

YAML
services:
  vaultwarden:
    image: vaultwarden/server:latest
    container_name: vaultwarden
    restart: unless-stopped
    env_file:
      - vw.env
    volumes:
      - ./vw-data:/data
    ports:
      - "127.0.0.1:8080:80"

The 127.0.0.1: prefix is deliberate. The container is reachable only from the VPS itself, and the reverse proxy talks to it over the loopback address. Nothing on the internet can reach port 8080 directly.

Set restart: unless-stopped if you want the container to come back after a reboot. The restart policy guide explains the differences between always, on-failure and unless-stopped.

Create the environment file

Vaultwarden reads settings from environment variables. Put them in vw.env rather than inside the Compose file, so secrets stay in one place:

INI
DOMAIN=https://vault.example.com
SIGNUPS_ALLOWED=true
ADMIN_TOKEN=replace-me

DOMAIN must match the public URL exactly, including https:// and no trailing slash. If it does not, logins fail with redirect errors.

Now generate a hashed admin token. Run the hashing subcommand inside the image:

Bash
cd /opt/vaultwarden
docker compose run --rm --entrypoint /vaultwarden vaultwarden hash

It asks for a token twice and prints an Argon2 hash. Paste that hash as the value of ADMIN_TOKEN, keeping it on one line. If the value contains $ characters and Compose complains about interpolation, wrap it in single quotes. Check the Vaultwarden project documentation if the subcommand path changes in a future release.

Tip: The admin page at /admin can only be reached with this token. Never reuse a password you use elsewhere, and do not store the plain token in the same file.

Start the container

Bash
docker compose up -d
docker compose ps
docker compose logs --tail 50 vaultwarden

Expect a running container and log lines about migrations and the Rocket server listening. If it exits immediately, read the log: a permission error on /data is the usual cause, fixed with sudo chown -R 1000:1000 /opt/vaultwarden/vw-data (the container's default UID in the official image; verify with docker compose exec vaultwarden id).

Put HTTPS in front of Vaultwarden

Clients refuse to talk to a plain HTTP Bitwarden server, so TLS is not optional. Caddy is the shortest path because it requests and renews certificates itself. Install it as described in install Caddy with automatic HTTPS, then create /etc/caddy/Caddyfile:

vault.example.com {
    reverse_proxy 127.0.0.1:8080
    encode gzip
}

Reload and watch the certificate being issued:

Bash
sudo systemctl reload caddy
sudo journalctl -u caddy -f

If you already run a container-based proxy, Nginx Proxy Manager in Docker is a good fit. In that case the proxy cannot reach 127.0.0.1 inside the Vaultwarden container namespace, so attach both containers to a shared Docker network and remove the 127.0.0.1: prefix. The Docker networking troubleshooting guide covers that pattern in detail.

Warning: Do not publish port 8080 to the internet as a shortcut. Vaultwarden sends no HSTS header itself and clients would fall back to an address you cannot later change without breaking them.

Finish the account and admin setup

  1. Open https://vault.example.com in a browser and create your account. This first account becomes the owner.
  2. Sign in, create a test item, and install the browser extension against the same URL under the self-hosted server setting.
  3. Open https://vault.example.com/admin, paste the plain token you hashed earlier, and confirm the settings page loads.
  4. Close registration: change SIGNUPS_ALLOWED=false in vw.env and apply it with docker compose up -d. The container is recreated with the new environment.
  5. From the admin page you can still invite specific users by email, which is safer than leaving open signups on.

If you plan to run this alongside other stacks, the Compose file explained article covers services, volumes and networks line by line.

Back up Vaultwarden

Your backup must include the whole vw-data directory, not just the database. rsa_key.pem and rsa_key.pub.pem are what let existing clients keep decrypting after a restore; lose them and every device has to be logged in again.

Warning: Copying db.sqlite3 while the container writes to it can produce a corrupt file. The script below stops the container first, so the service is offline for a few seconds. Run it at a quiet time.

Bash
#!/usr/bin/env bash
set -euo pipefail
cd /opt/vaultwarden
docker compose stop vaultwarden
tar -czf "/root/vaultwarden-$(date +%F).tar.gz" vw-data
docker compose start vaultwarden

Test the archive before you trust it: extract it into a scratch directory on another machine and check that db.sqlite3, rsa_key.pem and any attachments folder are present. Copy archives off the VPS regularly — a backup stored only on the server it protects is not a backup.

Check it works

Run these from your own machine, not the VPS:

Bash
curl -sS -o /dev/null -w "%{http_code}\n" https://vault.example.com/alive
curl -sSI https://vault.example.com | head -n 1

The first command prints 200 and the second shows HTTP/2 200 plus a valid certificate. Then create an item on the desktop app and confirm it appears in the mobile app after a sync. That round trip proves HTTPS, the database and the WebSocket notifications path are all working.

Troubleshooting

  • 502 Bad Gateway from the proxy. The proxy cannot reach the container. With Caddy on the host, check that the upstream is 127.0.0.1:8080 and that docker compose ps shows the port mapping. With a container proxy, use a shared Docker network instead of loopback.
  • Login loops or "invalid redirect URI" errors. DOMAIN does not match the address in the browser. Fix it to https://vault.example.com, no trailing slash, then run docker compose up -d.
  • Admin page rejects the token. The value in vw.env is the plain token instead of the Argon2 hash, or Compose mangled a $ in the hash. Regenerate with the hash subcommand and quote the value.
  • Clients stop syncing after a restore. The RSA key files were left out of the backup. Restore the full vw-data directory, not only db.sqlite3.
  • New items do not appear until you refresh. WebSocket traffic is being dropped. Caddy handles the upgrade automatically; behind Nginx you need proxy_set_header Upgrade $http_upgrade; and proxy_set_header Connection "upgrade"; in the location block.

Next steps

Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.