How to Self-Host Vaultwarden on a VPS
Install Vaultwarden on Ubuntu 24.04 with Docker Compose and Caddy for HTTPS, then set an Argon2 admin token, close signups and back up the vault.
Vaultwarden is an unofficial server for the Bitwarden password manager, written in Rust. It implements the Bitwarden client API, so the official Bitwarden apps and browser extensions work with it. It is not associated with Bitwarden, Inc.
This guide installs it by hand on Ubuntu 24.04 with Docker Compose, using the project wiki's example: Vaultwarden behind Caddy, which gets a Let's Encrypt certificate on its own.
Prefer one click? Deploy Vaultwarden from the VeloxaHost marketplace and skip the manual steps; this guide is for when you want to run every command yourself.
What you need
- A server running Ubuntu 24.04 LTS (pick it as the image when you create the server) with root access. Use SSH or the browser terminal on the server's page.
- A domain or subdomain, such as
vault.example.com. HTTPS is required: the project's README says the web vault needs HTTPS and a secure context for the browser's Web Crypto API. - A dedicated IPv4 address. Caddy's certificate challenge needs port 80 and the vault is served on 443, and neither port is available on the shared IPv4 address where each server gets its block of ports. The dedicated address is a paid add-on, chosen under "Network access" on the create page or taken later from the IP addresses page; the console shows the current price.
- Optionally, an email provider for invitations.
Vaultwarden's documentation sets no hardware minimum; it is a single binary with a SQLite database. A Veloxa Nano (1 vCPU, 1 GB RAM, 25 GB disk) at $4 a month, billed at $0.01 an hour and capped at the monthly price, runs it with Caddy and Docker.
Install Vaultwarden
Bring up the dedicated address. The server must answer on it: add it to the interface, add it to the network configuration too so it survives a reboot, and point your domain's A record at it. IPv6 is not available in US East yet, so skip the AAAA record.
ip addr add 198.51.100.5/32 dev eth0Install Docker Engine and the Compose plugin from Docker's apt repository. First remove conflicting packages:
sudo apt remove $(dpkg --get-selections docker.io docker-compose docker-compose-v2 docker-doc docker-buildx podman-docker containerd runc | cut -f1)Then add the repository, install, and run the test image:
# Add Docker's official GPG key: sudo apt update sudo apt install ca-certificates curl 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 the repository to Apt sources: sudo tee /etc/apt/sources.list.d/docker.sources <<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 sudo apt update sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin sudo docker run hello-worldCreate a directory for the stack.
mkdir -p /opt/vaultwarden && cd /opt/vaultwardenGenerate the admin token as an Argon2 hash, as the wiki recommends. The command asks for a password twice and prints the hash. Remember the password; you type it on the admin page.
docker run --rm -it vaultwarden/server /vaultwarden hashSave the output in
.envin the same directory, in single quotes. In a.envfile the$signs need no escaping.VAULTWARDEN_ADMIN_TOKEN='$argon2id$v=19$m=65540,t=3,p=4$...'Create
compose.yaml, the wiki's "Caddy with HTTP challenge" example with the admin token added. Replace bothDOMAINvalues andEMAIL, the address for Let's Encrypt registration. KeepSIGNUPS_ALLOWEDat"true"until your own account exists.services: vaultwarden: image: vaultwarden/server:latest container_name: vaultwarden restart: always environment: DOMAIN: "https://vault.example.com" # Your domain; vaultwarden needs to know it's https to work properly with attachments SIGNUPS_ALLOWED: "true" ADMIN_TOKEN: ${VAULTWARDEN_ADMIN_TOKEN} volumes: - ./vw-data:/data caddy: image: caddy:2 container_name: caddy restart: always ports: - 80:80 # Needed for the ACME HTTP-01 challenge. - 443:443 - 443:443/udp # Needed for HTTP/3. volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - ./caddy-config:/config - ./caddy-data:/data environment: DOMAIN: "https://vault.example.com" # Your domain. EMAIL: "admin@example.com" # The email address to use for ACME registration. LOG_FILE: "/data/access.log"Only Caddy publishes ports; it reaches Vaultwarden on the network Compose creates.
Create the
Caddyfilenext to it, unchanged from the wiki:{$DOMAIN} { log { level INFO output file {$LOG_FILE} { roll_size 10MB roll_keep 10 } } # Use the ACME HTTP-01 challenge to get a cert for the configured domain. tls {$EMAIL} # This setting may have compatibility issues with some browsers # (e.g., attachment downloading on Firefox). Try disabling this # if you encounter issues. encode zstd gzip # Proxy everything Rocket reverse_proxy vaultwarden:80 { # Send the true remote IP to Rocket, so that vaultwarden can put this in the # log, so that fail2ban can ban the correct IP. header_up X-Real-IP {remote_host} } }Start the stack and follow the logs while Caddy gets its certificate. Ctrl+C stops following; the containers keep running.
docker compose up -d && docker compose logs -f
First login
Open https://vault.example.com, choose to create an account, and set a long master password. It unlocks your vault, so store it safely.
Then close registration. In compose.yaml, set SIGNUPS_ALLOWED to "false" and recreate the container:
cd /opt/vaultwarden && docker compose up -d
Open https://vault.example.com/admin and log in with the password you hashed. There you can manage users, invite people even with registration closed, and check the diagnostics page. The first time you save settings there, Vaultwarden writes config.json to its data folder, and its values override environment variables from then on.
Secure it
Firewall. VeloxaHost firewalls are network-level and stateful, enforced before traffic reaches the server. Until you attach one, all traffic is allowed (except outbound TCP 25). Go to Firewalls, New Firewall, Add Rule: allow TCP 80 and 443 from 0.0.0.0/0 (and UDP 443 for HTTP/3), and TCP 22 only from YOUR.IP/32. Attach it on the server's page. Docker's documentation warns that ports Docker publishes bypass ufw, so rely on this firewall rather than ufw.
Registration. With signups off, the admin and existing organization owners and admins can still invite users. If you set SIGNUPS_DOMAINS_WHITELIST, SIGNUPS_ALLOWED is ignored, so use it deliberately.
Admin sessions. An admin login lasts 20 minutes by default (ADMIN_SESSION_LIFETIME), and changing the token does not end existing sessions. To end them all, delete vw-data/rsa_key.pem and restart Vaultwarden.
Email. Outbound port 25 is blocked by default, so use your provider's port 587 with starttls or 465 with force_tls. Add to the vaultwarden environment:
SMTP_HOST: "smtp.example.com"
SMTP_FROM: "vaultwarden@example.com"
SMTP_PORT: "587"
SMTP_SECURITY: "starttls"
SMTP_USERNAME: "myusername"
SMTP_PASSWORD: "MyPassw0rd"
Login attempts. For a public vault, the wiki's Fail2Ban page has filters for the web vault and admin page; the Caddyfile above already passes the real client IP. Turn on two-step login for your own account too.
Back it up
Everything is in /opt/vaultwarden/vw-data. The wiki marks db.sqlite3 and attachments as required, config.json and the rsa_key* files as recommended, and sends and icon_cache as optional. config.json and rsa_key.pem are sensitive, so encrypt copies you store elsewhere.
For a consistent database copy while it runs, use SQLite's .backup from the host; the image has no sqlite3 or cron. (Version 1.32.1 and later also has a built-in /vaultwarden backup command.) Save this as /root/vw-backup.sh:
#!/bin/bash
mkdir -p /root/vw-backups
sqlite3 /opt/vaultwarden/vw-data/db.sqlite3 ".backup '/root/vw-backups/db-$(date '+%Y%m%d-%H%M').sqlite3'"
tar czf /root/vw-backups/vw-data-$(date '+%Y%m%d').tar.gz -C /opt/vaultwarden vw-data
Then schedule it daily:
apt install sqlite3
chmod +x /root/vw-backup.sh
crontab -e
# add this line:
0 3 * * * /root/vw-backup.sh
To restore, stop Vaultwarden and replace the files in vw-data. When restoring a .backup copy, delete any existing db.sqlite3-wal first.
The wiki advises against relying only on snapshots of the whole machine, so treat them as an extra layer. Snapshots are instant, free (5 on Starter, 30 on Pay as you go) and stored in the same region. On the server's page, turn on automatic snapshots (hourly to weekly, kept 1 to 30 days). On the Backups page, set a schedule (every 6 hours to weekly, keeping 1 to 10) and download a backup now and then for an off-site copy.
Update it
Take a snapshot first: server page, Snapshots, enter a name, Create. Then follow the wiki's Compose update steps:
cd /opt/vaultwarden
docker compose pull
docker compose up -d
docker compose logs -f
This updates vaultwarden/server:latest and caddy:2. The data in vw-data and the certificates in caddy-data stay.
Troubleshooting
You cannot create an account or log in. The vault is probably reached over plain HTTP or with a wrong DOMAIN. Use the https:// URL, and make sure DOMAIN matches it exactly.
Caddy gets no certificate. Check docker compose logs caddy. Caddy needs the A record pointing at the server before it starts, and ports 80 and 443 open from outside. Confirm the address is on the interface (ip addr) and the firewall allows both ports.
"You are using a plain text ADMIN_TOKEN which is insecure." Either settings were saved on the admin page, so config.json wins, or the token arrived with extra quotes or doubled $ signs. Check docker exec vaultwarden printenv ADMIN_TOKEN or grep admin_token vw-data/config.json.
Emails do not arrive. Use port 587 or 465, not 25. On the admin page, click Save before Send test email; unsaved changes do not apply.
FAQ
Which VeloxaHost size fits Vaultwarden?
A Veloxa Nano: 1 vCPU, 1 GB RAM and 25 GB of disk at $4 a month, billed at $0.01 an hour and capped at the monthly price. This size runs it with Caddy and Docker.
What license is Vaultwarden under?
Vaultwarden is released under the AGPL-3.0 license. It is unofficial and not associated with Bitwarden, Inc., so report problems to the Vaultwarden project, not to Bitwarden.
Do I need a dedicated IPv4 address?
For this guide's setup, yes, because the certificate challenge needs port 80 and the vault uses 443. The alternative is a certificate from a DNS challenge, served on a forwarded port of your shared block, such as https://vault.example.com:20002. The wiki's DNS challenge example needs a custom Caddy build, since the stock image has no DNS modules. Shared port numbers can also change if the server moves to another host.
Does Vaultwarden work with the official Bitwarden apps?
Yes. It implements the Bitwarden client API, so the official apps and browser extensions connect once you enter your server's HTTPS URL as the self-hosted server.
Can I turn off the admin page later?
Yes. Remove ADMIN_TOKEN from compose.yaml, make sure DISABLE_ADMIN_TOKEN is unset and config.json has no admin_token key, then run docker compose up -d.
Commands and settings checked on 2026-10-18 against the official documentation: github.com, docs.docker.com, caddyserver.com. Software changes; if a step no longer matches, the vendor's docs win.
Ready to deploy?
Start a free trial, or compare plans to match your team's scale.