Build a Vaultwarden Lab with HTTPS, Backups, and Recovery Validation
Deploy Vaultwarden behind an HTTPS reverse proxy, keep the application bound to localhost, disable open signup after bootstrap, and create a consistent backup of the vault data...
Expected Outcome
A Vaultwarden Lab reachable only through HTTPS, with persistent data isolated from the container, a documented recovery artifact, and validation that distinguishes 'a backup file exists' from 'the vault can be recovered.'
Assumptions
A Linux host with Docker Engine and the Docker Compose plugin installed.
A DNS name that resolves to the reverse proxy for the Lab, or an internal DNS name with a trusted certificate path.
An HTTPS-capable reverse proxy such as Caddy, Nginx, or Nginx Proxy Manager.
A maintenance window for the short container stop used to create a consistent Lab backup.
A backup destination that is not inside the live Vaultwarden data directory.
Bill of Materials
The official Vaultwarden container image.
A reviewed Vaultwarden image tag or digest for the Lab.
A local persistent directory such as `/opt/vaultwarden/vw-data`.
A secure, offline place for recovery instructions and any emergency-access material.
Build Steps
- Verify the container runtime and create the Lab directory
Use Compose v2 and keep the service definition, persistent data, and backups in separate paths.
Read-only command: verify target and scope
docker --version docker compose version sudo install -d -m 0750 /opt/vaultwarden/vw-data /opt/vaultwarden/backups cd /opt/vaultwarden
- Define Vaultwarden on a localhost-only application port
Vaultwarden's web vault requires HTTPS for a secure browser context. Bind the application to loopback and let the reverse proxy own external HTTPS rather than exposing Vaultwarden's HTTP port directly to the LAN. Replace the example hostname and reviewed image tag before deployment.
Changes system state: review before running
sudo tee /opt/vaultwarden/compose.yaml > /dev/null <<'EOF' services: vaultwarden: image: vaultwarden/server:<reviewed-tag> container_name: vaultwarden restart: unless-stopped environment: DOMAIN: "https://vault.example.internal" SIGNUPS_ALLOWED: "true" volumes: - ./vw-data:/data ports: - "127.0.0.1:8000:80" EOF docker compose -f /opt/vaultwarden/compose.yaml config - Start Vaultwarden and configure HTTPS at the reverse proxy
Start the container, then configure the reverse proxy to terminate TLS for the exact DOMAIN value and forward to http://127.0.0.1:8000. Modern Vaultwarden handles notification endpoints on the main HTTP service; do not reintroduce old separate websocket-port recipes unless current project guidance for your proxy requires it.
Changes system state: review before running
cd /opt/vaultwarden docker compose up -d docker compose ps curl -I http://127.0.0.1:8000/
- Bootstrap the first account and close open signup
Use the HTTPS URL to create the intended first account. After bootstrap, change SIGNUPS_ALLOWED to false, validate the Compose file, and recreate the container. This prevents the Lab from remaining in an unnecessary open-registration state.
Changes system state: review before running
sudo sed -i 's/SIGNUPS_ALLOWED: "true"/SIGNUPS_ALLOWED: "false"/' /opt/vaultwarden/compose.yaml docker compose -f /opt/vaultwarden/compose.yaml config docker compose -f /opt/vaultwarden/compose.yaml up -d --force-recreate
- Validate the HTTPS boundary
Confirm that the public Lab URL presents the expected certificate and that the application port itself is bound only to loopback on the host.
Manual or UI step
ss -ltnp | grep ':8000'
Read-only command: verify target and scope
curl -I https://vault.example.internal/
- Create a consistent stopped-container backup
For this Lab, use a short maintenance stop so SQLite and the rest of /data are quiescent together. Archive the complete persistent directory to a separate backup path, restart Vaultwarden immediately, and record a checksum. This avoids assuming a copy of only db.sqlite3 also protects attachments, keys, and other persisted state.
Read-only command: verify target and scope
cd /opt/vaultwarden STAMP=$(date +%Y%m%d-%H%M%S) docker compose stop vaultwarden sudo tar -C /opt/vaultwarden -cpf "/opt/vaultwarden/backups/vaultwarden-$STAMP.tar" vw-data docker compose start vaultwarden sudo sha256sum "/opt/vaultwarden/backups/vaultwarden-$STAMP.tar" | sudo tee "/opt/vaultwarden/backups/vaultwarden-$STAMP.sha256"
- Validate the backup artifact without overwriting the live vault
Verify the checksum and extract the archive into a disposable directory. Confirm that the SQLite database and expected persisted directories/files are present. If sqlite3 is installed on the host, run an integrity check against the extracted database rather than the live file.
Destructive: review before running
cd /opt/vaultwarden/backups sudo sha256sum -c vaultwarden-$STAMP.sha256 sudo rm -rf /tmp/vaultwarden-restore-check && sudo mkdir -p /tmp/vaultwarden-restore-check sudo tar -C /tmp/vaultwarden-restore-check -xpf "vaultwarden-$STAMP.tar" sudo test -f /tmp/vaultwarden-restore-check/vw-data/db.sqlite3 && echo 'Database present'
Manual or UI step
command -v sqlite3 >/dev/null && sudo sqlite3 /tmp/vaultwarden-restore-check/vw-data/db.sqlite3 'PRAGMA integrity_check;' || echo 'sqlite3 not installed; skip DB integrity check'
- Write recovery instructions outside the vault
Document the DNS name, reverse-proxy dependency, Compose path, backup location, restore order, and who is authorized to perform recovery. Store that document in an appropriately protected offline or independent location—not as plaintext beside the live vault and not only inside Vaultwarden itself.
Validation
Vaultwarden is reachable through the intended HTTPS URL and the client trusts the certificate.
Port 8000 is bound only to `127.0.0.1` on the Vaultwarden host.
Open signup is disabled after the intended bootstrap account is created.
A stopped-container backup archive verifies by SHA-256 and contains the persistent Vaultwarden data, not just a container image.
The extracted backup contains the SQLite database and passes `PRAGMA integrity_check` when SQLite tooling is available.
Recovery instructions are stored independently of the live vault.
Troubleshooting
If the web vault fails in a browser while localhost HTTP works, verify the external `DOMAIN`, TLS certificate, reverse-proxy target, forwarded headers, and HTTPS access first. The Vaultwarden project explicitly requires a secure browser context for the web vault.
If the container restarts but data appears missing, stop and inspect the bind mount before creating new accounts. A wrong host path can make a fresh empty `/data` look like data loss.
Read-only command: verify target and scope
docker inspect vaultwarden --format '{{json .Mounts}}' sudo ls -la /opt/vaultwarden/vw-dataIf a restore is required, stop Vaultwarden and preserve the current data directory before replacing it. Do not mix a restored SQLite database with stale WAL files from a different database generation.
Cleanup or Rollback
Remove `/tmp/vaultwarden-restore-check` after backup validation; it contains sensitive copied vault data.
For a disposable Lab, stop the stack with `docker compose down` but preserve any backup you intend to use before deleting `/opt/vaultwarden/vw-data`.
Do not use `docker compose down -v` or broad filesystem deletion as generic cleanup when you have not confirmed where persistent vault data lives.
Next Improvements
Copy encrypted backup artifacts off the Vaultwarden host and set a retention policy.
Perform a full restore rehearsal on an isolated host before treating the backup process as production-ready.
Add MFA and emergency-access practices appropriate to the people who will depend on the vault.
