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.compointing at203.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:
docker --version
docker compose versionWarning: 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.
sudo mkdir -p /opt/vaultwarden/vw-data
cd /opt/vaultwardenCreate compose.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:
DOMAIN=https://vault.example.com
SIGNUPS_ALLOWED=true
ADMIN_TOKEN=replace-meDOMAIN 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:
cd /opt/vaultwarden
docker compose run --rm --entrypoint /vaultwarden vaultwarden hashIt 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
docker compose up -d
docker compose ps
docker compose logs --tail 50 vaultwardenExpect 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:
sudo systemctl reload caddy
sudo journalctl -u caddy -fIf 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
- Open
https://vault.example.comin a browser and create your account. This first account becomes the owner. - Sign in, create a test item, and install the browser extension against the same URL under the self-hosted server setting.
- Open
https://vault.example.com/admin, paste the plain token you hashed earlier, and confirm the settings page loads. - Close registration: change
SIGNUPS_ALLOWED=falseinvw.envand apply it withdocker compose up -d. The container is recreated with the new environment. - 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.
#!/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 vaultwardenTest 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:
curl -sS -o /dev/null -w "%{http_code}\n" https://vault.example.com/alive
curl -sSI https://vault.example.com | head -n 1The 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:8080and thatdocker compose psshows the port mapping. With a container proxy, use a shared Docker network instead of loopback. - Login loops or "invalid redirect URI" errors.
DOMAINdoes not match the address in the browser. Fix it tohttps://vault.example.com, no trailing slash, then rundocker compose up -d. - Admin page rejects the token. The value in
vw.envis the plain token instead of the Argon2 hash, or Compose mangled a$in the hash. Regenerate with thehashsubcommand and quote the value. - Clients stop syncing after a restore. The RSA key files were left out of the backup. Restore the full
vw-datadirectory, not onlydb.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;andproxy_set_header Connection "upgrade";in the location block.
Next steps
- Add a second layer in front of the admin page with Nginx Proxy Manager in Docker if you prefer a UI for certificates.
- Learn how the data volume behaves under a non-root user in Docker persistent storage for non-root users.
- Review Docker volumes vs bind mounts before you move
vw-datato another disk.
