← Start of tutorial

WIUNIX ACADEMY

Self-host Vaultwarden with HTTPS, restricted admin access and consistent backups

Page 4 of 5. Review the earlier steps and prerequisites before continuing.

YOUR SYSTEM. YOUR STEPS.

Where are you working?

Choose your OS and version. The prerequisites, commands and troubleshooting follow your environment.

Debian12Reviewed against official documentation ·

USE YOUR OWN VALUES

Configure this tutorial

Set the example values for your environment; related explanations and copy panels update together. Never enter passwords, private keys or tokens. Values stay in this tab and are not sent to the server or share links. Check actual DNS, certificates and network policy before running commands.

Different IP addresses, domains or ports? Open the settings below, change the values and select “Apply to tutorial”, then copy the updated commands.

Set values for my environment7 editable values

A database copy is only part of recovery

SQLite uses db.sqlite3 and may have an active WAL. Copying only the main file during writes can lose committed state. SQLite’s online backup API or the built-in backup command produces a consistent database, but it does not atomically capture attachments, Sends and configuration.

This recipe stops the sole writer and archives the complete matching data/config/secrets/TLS set, including SQLite/WAL if present, attachments, sends and RSA signing keys. Sends may be absent because they are disabled; include them if retained from earlier use. icon_cache is optional, but including the whole directory is simpler. Preserve ownership and modes. RSA keys can forge sessions, and config may contain credentials: encrypt and restrict off-host copies with a separately recoverable key.

An archive on the same disk is only a local checkpoint. Transfer an encrypted copy through your existing backup system, verify retrieval and checksum, monitor freshness and retention, and keep recovery keys outside Vaultwarden. A stopped backup causes deliberate downtime; confirm users have synced first.

11

LET’S BUILD

Take one stopped consistent checkpoint

Run in the maintenance window. The helper refuses symlinks, verifies the named container stopped, creates a new private backup directory and never replaces an archive. Finally starts the same container even if archiving fails. A failed copy remains incomplete and must not be used as a recovery set.

bash

Dedicated Vaultwarden VM

sudo python3 - <<'PY'
import os,stat,subprocess,tarfile,tempfile,hashlib
from pathlib import Path
r=Path('/srv/academy-vaultwarden');name='academy_vaultwarden'
assert r.resolve()==r and not any(p.is_symlink() for p in r.rglob('*')), 'Symlink refused'
state=subprocess.check_output(['docker','inspect','--format','{{.State.Running}}',name],text=True).strip()
assert state=='true', 'Review unexpected stopped state first'
subprocess.run(['docker','stop','--time','60',name],check=True,stdout=subprocess.DEVNULL)
assert subprocess.check_output(['docker','inspect','--format','{{.State.Running}}',name],text=True).strip()=='false'
dest=Path(tempfile.mkdtemp(prefix='stopped-',dir=r/'backups'));os.chmod(dest,0o700)
archive=dest/'vaultwarden.tar'
try:
    fd=os.open(archive,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
    with os.fdopen(fd,'wb') as out,tarfile.open(fileobj=out,mode='w') as tar:
        for item in ('data','config','secrets','tls'): tar.add(r/item,arcname=item,recursive=True)
    with archive.open('rb') as source: digest=hashlib.file_digest(source,'sha256').hexdigest()
    (dest/'SHA256').write_text(digest+'  vaultwarden.tar\n')
    os.chmod(dest/'SHA256',0o600)
    print('Stopped consistent archive:',archive,'SHA256:',digest)
finally:
    subprocess.run(['docker','start',name],check=True,stdout=subprocess.DEVNULL)
PY
How do I know it worked?

New private archive/checksum, container running again; encrypt off-host before relying on it.

Restore into an empty isolated destination

Retrieve and decrypt a chosen checkpoint into a new private recovery workspace on an isolated host. Verify its checksum and inspect tar members before extraction: accept only expected relative data/config/secrets/tls members, regular files/directories, no symlink/hardlink/traversal, no overwrite. Keep the original service and its storage untouched. Restore ownership/modes and use the same image digest first.

Do not publish host ports or connect the rehearsal to users, email, push providers or public DNS. Mount a separate empty data destination, with a private configuration/DOMAIN for the fixture. For a stopped full-file checkpoint restore the matching SQLite and WAL together. For a database produced by SQLite online backup restore only that database into a fresh directory without stale WAL/SHM; never mix the two methods. Preserve corresponding attachments/Sends/config/RSA keys.

Start the isolated copy, run SQLite integrity checks, compare attachment/Send file checksums and verify dummy-account login/decryption/sync before a cutover decision. A checksum or /alive alone is insufficient. If reverting an upgrade, restore the complete pre-upgrade checkpoint to a new destination; a previous image may not understand a migrated database. Never run old and new writers against one directory.

12

LET’S BUILD

Diagnose without dumping credentials

For502, inspect the loopback health and container state, file modes and the locally protected error log. For403 on /admin, compare the real source address with 203.0.113.10/32 rather than allowing all. For cryptography failures, verify trusted HTTPS and DOMAIN. For login/sync failures, compare exact current client versions and release notes; do not downgrade against migrated data. Inspect logs locally and redact before sharing.

bash

Dedicated Vaultwarden VM

curl --fail --silent --show-error 'http://127.0.0.1:8159/alive'
sudo docker inspect --format '{{.State.Status}} {{.State.ExitCode}}' 'academy_vaultwarden'
sudo nginx -t
sudo stat -c '%a %u:%g %n' '/srv/academy-vaultwarden/data' '/srv/academy-vaultwarden/secrets/admin.phc'
How do I know it worked?

Health/state/modes identify the failing boundary without a secret dump.

13

LET’S BUILD

Stop safely when acceptance fails

If HTTPS, enrollment or recovery is not accepted, stop the newly named service and keep its private data for investigation. Do not delete directories or reuse a real vault. A bad new proxy file can be removed from the include directory only after preserving it and confirming its exact ownership; use the existing Nginx rollback procedure, validate, then reload. Do not change unrelated sites.

bash

Dedicated Vaultwarden VM

sudo docker stop --time 60 'academy_vaultwarden'
sudo docker inspect --format '{{.State.Status}}' 'academy_vaultwarden'
How do I know it worked?

Only academy_vaultwarden is stopped; private state remains recoverable.

What the isolated checks establish

28 isolated checks passed in a task-owned Debian13 container using the official Vaultwarden1.37.4 image, bundled Web-Vault2026.7.0, Nginx1.26.3 and Python3.13. No host port was published. The exact directory and Argon2 Python bodies ran with fixture-only hidden-input/TTY shims. The published proxy retained its directives with private-CA TLS, loopback listeners, a fixture management source and a bounded1MiB body cap override. Real HTTP checks proved trusted/untrusted TLS, Host421, admin source denial including spoofed headers/case variants, valid/invalid admin passphrase, secure cookie, authenticated diagnostics, unauthenticated sync denial, closed registration and413.

A stopped archive was actually restored into a separate empty private destination. The real application SQLite/RSA files and synthetic database marker/attachment/Send files matched exactly; integrity_check passed, the restored service started and the original fixture admin passphrase still authenticated. Original storage stayed unchanged. This establishes storage/PHC restoration, not decryption or synchronization of an API-created user vault.

Docker Engine port publication/read-only root behavior, installation on each OS, real vault login/decryption/sync, SMTP/invitations, MFA/SSO/passkeys, browser/mobile/extension clients, authenticated WebSocket101 notifications, public ACME/renewal, off-host encryption/retrieval, systemd/reboot and HA were not executed. Initial fixture-only CA strictness and API-version/registration assumptions were corrected against actual responses before the final28 checks. No production account, service, credential, firewall or network was changed.

Explore all tutorials ↗