Last updated: November 2026 — pin images you tested; upstream tags move.
For: One person or a small household who wants a Bitwarden-compatible vault on a cheap Linux VPS (Hetzner CX22 / DigitalOcean $6 / similar), with automatic HTTPS and a backup path they can restore.
Do not use this guide when:
You need official Bitwarden Business features (SSO, SCIM, org policies Vaultwarden does not fully match)
The VPS has no public IPv4 and you cannot do HTTP-01 (use a different ACME path — not covered here)
You plan to expose Vaultwarden on plain HTTP — the web vault will break in modern browsers
You want a managed SaaS password manager with zero ops
A true "$5" tier works if it has 1 GB RAM and a stable public IP. Avoid shared hosts that block outbound 80/443 ACME.
vaultwarden listens only on the Compose network (:80 inside)
caddy publishes 80, 443, 443/udp and terminates TLS (Let's Encrypt HTTP-01)
Data lives in ./vw-data (SQLite + attachments + keys)
Directory layout:
~/vaultwarden/
docker-compose.yml
Caddyfile
.env
vw-data/ # created on first start
caddy-data/
caddy-config/
DOMAIN=vault.example.com
ACME_EMAIL=you@example.com
# Generate AFTER first container pull — see admin token section
# ADMIN_TOKEN=
SIGNUPS_ALLOWED=true
{$DOMAIN} {
encode gzip
reverse_proxy vaultwarden:80 {
header_up X-Real-IP {remote_host}
}
}
services:
vaultwarden:
image: vaultwarden/server:1.37.5
container_name: vaultwarden
restart: unless-stopped
environment:
DOMAIN: "https://${DOMAIN}"
SIGNUPS_ALLOWED: "${SIGNUPS_ALLOWED:-true}"
ADMIN_TOKEN: "${ADMIN_TOKEN:-}"
# Optional hardening after day one:
# INVITATIONS_ALLOWED: "false"
# SHOW_PASSWORD_HINT: "false"
volumes:
- ./vw-data:/data
healthcheck:
test: ["CMD", "/healthcheck.sh"]
interval: 30s
timeout: 10s
retries: 3
start_period: 20s
caddy:
image: caddy:2.11.7
container_name: caddy
restart: unless-stopped
depends_on:
vaultwarden:
condition: service_healthy
ports:
- "80:80"
- "443:443"
- "443:443/udp"
environment:
DOMAIN: "${DOMAIN}"
EMAIL: "${ACME_EMAIL}"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy-data:/data
- ./caddy-config:/config
Pin tags (1.37.5, 2.11.7) for reproducibility; bump deliberately with docker compose pull && docker compose up -d.
docker compose exec vaultwarden /vaultwarden hash
# paste the Argon2 PHC string into .env as ADMIN_TOKEN='...'
# quote it — `$` in the hash breaks unquoted Compose interpolation
docker compose up -d
After your account exists:
SIGNUPS_ALLOWED=false
Then docker compose up -d.
A/AAAA for vault.example.com → VPS IP
Wait until dig +short vault.example.com returns the right address
Allow TCP 80, 443 (and UDP 443 if you keep HTTP/3)
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw enable
mkdir -p ~/vaultwarden && cd ~/vaultwarden
# create the three files above
docker compose up -d
docker compose logs -f caddy
Open https://vault.example.com, create the owner account, then disable signups.
Caddy obtains and renews certificates automatically when DNS points here and ports 80/443 are reachable.
Vaultwarden must see DOMAIN=https://… or websocket sync / attachments misbehave behind TLS.
Do not publish vaultwarden ports on the host; only Caddy should be public.
If you later add more apps, put additional site blocks in the same Caddyfile or use a shared Caddy stack — keep one ACME client per host.
Backup (while running is OK for SQLite if you copy consistently; prefer stop for cold copy):
cd ~/vaultwarden
Optional cold backup: docker compose stop vaultwarden
tar -czf vw-backup-$(date +%F).tar.gz vw-data
Copy the tarball off-box (another VPS, S3, laptop)
docker compose start vaultwarden if you stopped it
Also keep a copy of .env and Caddyfile (no secrets in git)
Restore:
Fresh host: install Docker, recreate compose files
docker compose down if a stack exists
Extract: tar -xzf vw-backup-YYYY-MM-DD.tar.gz
Confirm ./vw-data ownership/mode is readable by the container
docker compose up -d
Log in with an existing account; confirm vault items and attachments
What matters most: ./vw-data (database db.sqlite3, attachments/, rsa_key*, config.json). Losing this loses the vault.
[ ] curl -I https://vault.example.com → HTTP/2 200 (or 3xx to app) with a valid cert
[ ] Web vault loads over HTTPS; you can register/login
[ ] Official Bitwarden mobile/desktop client connects using the custom server URL
[ ] Create a login item + attachment; refresh another client — item syncs
[ ] SIGNUPS_ALLOWED=false — second browser cannot self-register
[ ] /admin opens only with your token
[ ] Backup tarball restores on a second folder/stack and vault opens
Move up when: many org users, large file attachments, or you want Postgres instead of SQLite. For a personal vault, 1–2 GB RAM is usually enough.
Images referenced: vaultwarden/server:1.37.5, caddy:2.11.7 (Nov 2026). Confirm current tags on Docker Hub / GitHub before production pin bumps.