Vaultwarden is a lightweight, Bitwarden-compatible password manager that you can self-host on your own server. On Ubuntu 24.04, a practical deployment is Docker Compose with Vaultwarden on a private Docker network and Caddy as the only public-facing container for HTTPS.
Raff Technologies is the VM platform used by the original tutorial. The saved tested environment remains Raff VM with 2 vCPU, 4 GB DDR5 RAM, 40 GB NVMe storage, Ubuntu 24.04 LTS, kernel 6.8.x. This revision re-verifies the deployment guidance against current Vaultwarden documentation and releases on September 5, 2026 without claiming a new end-to-end machine test.
This guide pins Vaultwarden 1.37.2, the current stable release at verification time. Vaultwarden's release notes specifically require 1.37.2 for compatibility with Bitwarden clients version 2026.8.0 and newer. The deployment also uses the modern integrated WebSocket path: notifications share Vaultwarden's normal HTTP port, so the old dedicated 3012 WebSocket port and WEBSOCKET_ENABLED setting are no longer part of the recommended setup.
A password manager is security-critical infrastructure. Do not put real credentials into the vault until HTTPS works, the first account can sign in, public registration is disabled, the admin page is disabled unless intentionally required, and you have created an off-server backup.
Prerequisites:
- Ubuntu 24.04 with SSH and sudo access
- A domain such as
vault.example.compointing to the VM - Public TCP 80 and 443 available for HTTPS
- A tested recovery path before firewall changes
- A secure off-server destination for Vaultwarden backups
Step 1 — Verify Ubuntu, DNS, resources, and existing listeners
Confirm the operating system and architecture:
cat /etc/os-release uname -m
Check memory and free storage:
free -h df -h /
Set the hostname you plan to use and verify DNS:
export DOMAIN=vault.example.com dig +short A "$DOMAIN" dig +short AAAA "$DOMAIN"
Publish an AAAA record only when IPv6 really reaches this VM and is protected consistently.
Inspect current listeners:
sudo ss -tulpn
Ports 80 and 443 should be available for the Caddy reverse proxy. Vaultwarden itself will not publish a host port in this deployment.
Verify: Ubuntu should report 24.04, DNS should point to this server, available memory/disk should be understood, and no unexpected service should already own TCP 80 or 443.
Step 2 — Update Ubuntu and install Docker Engine with Compose v2
Update package metadata and review upgrades:
sudo apt update apt list --upgradable 2>/dev/null
Apply updates according to your maintenance policy:
sudo apt upgrade
Install the Docker repository prerequisites:
sudo apt install -y ca-certificates curl gnupg sudo install -m 0755 -d /etc/apt/keyrings sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \ -o /etc/apt/keyrings/docker.asc sudo chmod a+r /etc/apt/keyrings/docker.asc
Add Docker's official repository:
sudo tee /etc/apt/sources.list.d/docker.sources >/dev/null <<EOF Types: deb URIs: https://download.docker.com/linux/ubuntu Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") Components: stable Architectures: $(dpkg --print-architecture) Signed-By: /etc/apt/keyrings/docker.asc EOF
Install Docker Engine and Compose:
sudo apt update sudo apt install -y \ docker-ce docker-ce-cli containerd.io \ docker-buildx-plugin docker-compose-plugin sudo systemctl enable --now docker
Verify:
sudo docker version docker compose version sudo docker run --rm hello-world
For a standalone walkthrough of this step, see Install Docker on Ubuntu 24.04.
Verify: Docker should be active, Compose v2 should respond, and the hello-world container should run successfully.
Step 3 — Prepare the firewall without locking out SSH
Open a second SSH session before changing firewall state.
If UFW is already active, confirm the actual SSH port is allowed and permit HTTP/HTTPS:
sudo ufw status numbered sudo ufw allow 80/tcp comment 'Vaultwarden HTTP/ACME' sudo ufw allow 443/tcp comment 'Vaultwarden HTTPS'
If UFW is currently inactive, do not blindly enable it from a single remote shell. Follow the lockout-safe flow in Set Up UFW Firewall on Ubuntu 24.04, including allowing your real SSH port before enabling the firewall.
Review the final rules:
sudo ufw status numbered
Do not open Vaultwarden port 80 separately on the host and do not open the retired WebSocket port 3012. Only Caddy will publish TCP 80 and 443.
Verify: SSH should remain reachable from the second session, 80/443 should be allowed when UFW is active, and no rule should expose port 3012 or a direct Vaultwarden application port.
Step 4 — Create the Vaultwarden deployment directory and private environment file
Create a dedicated stack directory:
sudo mkdir -p /opt/vaultwarden/{vw-data,backups} sudo chown -R "$USER":"$USER" /opt/vaultwarden cd /opt/vaultwarden chmod 700 vw-data backups
Create the environment file:
cat > .env <<'EOF' DOMAIN=vault.example.com [email protected] EOF chmod 600 .env
Replace both values with your real domain and certificate contact email.
This main deployment intentionally does not define ADMIN_TOKEN; that keeps /admin disabled. If you later need the admin interface, use Vaultwarden's Argon2-hashed-token procedure rather than a plaintext token.
Verify: /opt/vaultwarden should exist, vw-data and backups should not be group/world accessible, and .env should have mode 600.
Step 5 — Create a pinned Vaultwarden 1.37.2 Docker Compose stack
Create compose.yaml:
cat > compose.yaml <<'EOF' services: vaultwarden: image: vaultwarden/server:1.37.2 container_name: vaultwarden restart: unless-stopped environment: DOMAIN: "https://${DOMAIN}" SIGNUPS_ALLOWED: "true" SHOW_PASSWORD_HINT: "false" IP_HEADER: "X-Real-IP" LOG_LEVEL: "warn" TZ: "Etc/UTC" volumes: - ./vw-data:/data networks: - vaultwarden_net caddy: image: caddy:2 container_name: vaultwarden-caddy restart: unless-stopped depends_on: - vaultwarden ports: - "80:80" - "443:443" environment: DOMAIN: "${DOMAIN}" ACME_EMAIL: "${ACME_EMAIL}" volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - caddy_data:/data - caddy_config:/config networks: - vaultwarden_net networks: vaultwarden_net: driver: bridge volumes: caddy_data: caddy_config: EOF
Why pin Vaultwarden instead of using latest? A password-manager server update can include database migrations and client-compatibility changes. An exact stable tag makes the deployed version auditable and lets you review each future update deliberately.
The Vaultwarden service has no ports: block, so it is reachable only from the Compose network.
Verify: grep -n 'vaultwarden/server' compose.yaml should show 1.37.2, and the Vaultwarden service should not publish any host port.
Step 6 — Configure Caddy HTTPS and the modern WebSocket path
Create Caddyfile:
cat > Caddyfile <<'EOF' { email {$ACME_EMAIL} } {$DOMAIN} { encode zstd gzip header { Strict-Transport-Security "max-age=31536000" X-Content-Type-Options "nosniff" Referrer-Policy "same-origin" -Server } reverse_proxy vaultwarden:80 { header_up X-Real-IP {remote_host} } } EOF
Modern Vaultwarden WebSocket notifications are integrated into the normal HTTP server. Caddy's reverse_proxy supports WebSocket upgrades automatically, so do not add an old rule such as:
/notifications/hub -> vaultwarden:3012
and do not add WEBSOCKET_ENABLED=true. Those belong to older Vaultwarden deployments.
Validate the Compose model before starting it:
sudo docker compose config >/dev/null && echo 'Compose config is valid'
Verify: The Caddyfile should proxy only to vaultwarden:80, Compose validation should pass, and there should be no reference to port 3012.
Step 7 — Pull, start, and verify Vaultwarden 1.37.2 over HTTPS
Pull the pinned images:
cd /opt/vaultwarden sudo docker compose pull
Start the stack:
sudo docker compose up -d
Inspect status and recent logs:
sudo docker compose ps sudo docker compose logs --tail=100 vaultwarden sudo docker compose logs --tail=100 caddy
Verify the running Vaultwarden version:
sudo docker compose exec vaultwarden /vaultwarden --version
Expected version:
vaultwarden 1.37.2
Test HTTPS:
DOMAIN=$(sed -n 's/^DOMAIN=//p' .env) curl -I "https://$DOMAIN"
Vaultwarden 1.37.2 is the current stable release at this revision and upstream specifically calls it required for Bitwarden clients 2026.8.0 and newer.
Verify: Both containers should stay running, /vaultwarden --version should report 1.37.2, Caddy should obtain a trusted certificate, and the HTTPS endpoint should respond successfully.
Step 8 — Create the first account and verify client login before storing real credentials
Open:
https://vault.example.com
Create the first Vaultwarden account while SIGNUPS_ALLOWED=true.
Use a unique, high-entropy master password. Do not assume the server can recover a forgotten master password; the password-manager design depends on client-side encryption. Establish your own recovery process before putting irreplaceable credentials in the vault.
After account creation:
- Sign out of the web vault.
- Sign back in.
- Connect one Bitwarden-compatible browser extension or desktop/mobile client to your self-hosted server URL.
- Create a temporary test item.
- Force/trigger a sync if needed.
- Confirm the item appears in both the web vault and the client.
Current Vaultwarden 1.37.2 release notes mention client compatibility changes around Bitwarden 2026.8.0+, so testing at least one real client is more meaningful than checking the browser page alone.
Verify: The account should log in through HTTPS, at least one supported client should connect to the self-hosted URL, and a temporary item should synchronize successfully.
Step 9 — Disable public registration immediately after onboarding
Once the intended first account exists, change:
SIGNUPS_ALLOWED: "true"
to:
SIGNUPS_ALLOWED: "false"
using a controlled edit:
cd /opt/vaultwarden sed -i 's/SIGNUPS_ALLOWED: "true"/SIGNUPS_ALLOWED: "false"/' compose.yaml sudo docker compose up -d
Verify the running environment:
sudo docker compose exec vaultwarden printenv SIGNUPS_ALLOWED
Expected output:
false
Then open the registration page in a private/incognito browser and confirm an unknown visitor cannot create a new account.
Do not repeatedly toggle public signups for routine user onboarding. For multi-user deployments, review Vaultwarden's invitation and organization controls rather than leaving anonymous registration enabled indefinitely.
Verify: SIGNUPS_ALLOWED should report false, normal existing-user login should still work, and unauthenticated public signup should no longer be available.
Step 10 — Keep the Vaultwarden admin page disabled unless you explicitly need it
Because ADMIN_TOKEN is absent from this stack, the Vaultwarden admin interface should be disabled.
Check it:
DOMAIN=$(sed -n 's/^DOMAIN=//p' /opt/vaultwarden/.env) curl -sS "https://$DOMAIN/admin" | head
A disabled-admin response is the intended state for this guide.
If you genuinely need /admin, do not use a plaintext token. Generate an Argon2 PHC hash with the exact Vaultwarden version you run:
sudo docker run --rm -it vaultwarden/server:1.37.2 /vaultwarden hash
Vaultwarden's admin documentation recommends storing the resulting Argon2 hash as the ADMIN_TOKEN configuration value. How $ characters must be quoted/escaped depends on whether you place the value directly in Compose YAML, a Compose .env file, another environment file, or a stack manager, so follow Vaultwarden's current Enabling admin page documentation for your exact configuration method.
Also be aware that values saved through /admin are written to /data/config.json and can override environment variables on later starts. Do not mix GUI-managed configuration and Compose-managed configuration without understanding that precedence.
If you no longer need the admin page, remove both the environment token and any persisted admin_token setting in config.json according to the official documentation, then restart Vaultwarden.
Verify: For this tutorial's default architecture, /admin should stay disabled and the container logs should not report a plaintext ADMIN_TOKEN warning.
Step 11 — Create a consistent Vaultwarden backup with the built-in SQLite backup command
Vaultwarden's /data directory contains more than the database: attachments, Send data, RSA keys, configuration, and other persistent application state may also be present. Back up the full persistent dataset, but do not rely on a raw copy of an actively written SQLite database.
First create Vaultwarden's built-in consistent SQLite snapshot:
cd /opt/vaultwarden sudo docker compose exec vaultwarden /vaultwarden backup
This creates a timestamped file such as:
/data/db_20260905_120000.sqlite3
Then stop Vaultwarden briefly so the non-database files are copied from a quiet state:
sudo docker compose stop vaultwarden
Create an archive that includes the generated SQLite snapshot and the rest of /data, but excludes the live SQLite database/WAL/SHM files:
BACKUP="backups/vaultwarden-$(date -u +%Y%m%d-%H%M%S).tar.gz" sudo tar \ --exclude='vw-data/db.sqlite3' \ --exclude='vw-data/db.sqlite3-wal' \ --exclude='vw-data/db.sqlite3-shm' \ -czf "$BACKUP" \ vw-data compose.yaml Caddyfile .env
Start Vaultwarden again:
sudo docker compose start vaultwarden
Inspect the archive:
sudo tar -tzf "$BACKUP" | head -n 40 sudo ls -lh "$BACKUP"
Copy the archive to a protected off-server location. Treat it as sensitive infrastructure data even though vault contents are encrypted.
