Self-host Vaultwarden on a VPS: the secure setup
Vaultwarden runs your passwords, so the tutorial is the security, not the install. Anyone can start the container. The parts that decide whether you get owned are the admin token, whether open signups are still on, whether the reverse proxy passes the WebSocket upgrade, whether fail2ban is watching the login endpoint, and whether the backup you took is one you have ever restored.
This walks the full path on Ubuntu 24.04 LTS with Vaultwarden 1.37.3 (released 13 September 2026), the current stable at the time of writing. Every command is here. Where a step commonly breaks, the failure and its cause are next to it.
What is Vaultwarden?
Vaultwarden is a password server that speaks the Bitwarden client protocol. It is a from-scratch reimplementation in Rust of the Bitwarden server API, so the official Bitwarden apps, browser extensions and CLI all talk to it unchanged. You point your clients at your own domain instead of bitwarden.com and keep the whole vault on hardware you control.
People self-host it because it is tiny. The upstream Bitwarden stack wants several containers and a couple of gigabytes of RAM; Vaultwarden is a single binary with SQLite and idles in tens of megabytes. It replaces a paid Bitwarden organisation while giving you the premium features (2FA, attachments, Sends) for the cost of a small VPS.
What you need
The honest minimum, with SQLite and a handful of users:
- RAM: 512 MB is plenty. It idles well under 100 MB; the memory floor is your OS, not Vaultwarden.
- vCPU: 1 shared vCPU. Login and sync are bursty and short.
- Disk: 10 GB covers the OS, the image, the database and years of attachments for a small team.
- OS: Ubuntu 24.04 LTS, root or a sudo user.
- A domain you can add an A/AAAA record to, e.g.
vault.example.com.
This is a genuine smallest-plan workload. It fits comfortably on the entry UK VPS in Coventry, which gives you a fixed IPv4 and IPv6, always-on DDoS filtering and one-reboot upgrades if your team grows later. Do not size up for this. A password vault for a household or a small company will never touch the ceiling of the cheapest plan.
Bitwarden client nginx TLS :443 certbot Vaultwarden container :80 via 127.0.0.1:8080 SQLite db.sqlite3 https http wss /notifications/hub (Upgrade) The request path. Only 443 is public; the container listens on loopback and is never reached directly.
Point the domain at the server
Create the DNS record before anything else, because the TLS certificate later depends on it resolving. Use your VPS's real addresses.
Confirm it has propagated to where you are working from:
An empty answer means the record has not propagated yet. Wait; do not skip ahead to certbot, which will fail its HTTP-01 challenge if the name does not resolve to this box.
Update the system and install Docker
Vaultwarden ships an official Docker image; the project does not publish a standalone binary, so the container is the supported path. We manage it with systemd rather than docker compose so restarts, logging and boot order behave like any other service.
Create the directory layout
Keep the persistent data and the environment file together under /srv.
The data directory is the whole state of the server: db.sqlite3 plus its -wal and -shm companions, rsa_key.* (the token-signing keys), attachments/, sends/, config.json and icon_cache/. Lose the rsa keys and every client is logged out; lose the database and the vault is gone.
Generate the admin token
The admin page is disabled unless ADMIN_TOKEN is set. Never put a plaintext token there. Vaultwarden hashes one for you with Argon2, so a leak of the config file does not leak the token:
Copy that whole single-quoted string. The single quotes matter: the PHC string contains $ signs that the shell and Docker will otherwise try to expand.
Configure Vaultwarden
Write the environment file. Note SIGNUPS_ALLOWED=true for now, so you can create the first account. We close it in a moment.
Since v1.31.0 the WebSocket path is served on the main HTTP port; the old separate port 3012 was removed. You do not open any extra port, but the reverse proxy still has to forward the upgrade. That comes next.
Write the systemd unit
We bind the container to loopback only, so nothing reaches Vaultwarden except through nginx.
Pin the tag. Using :latest means a restart can silently jump a major version and change the config format under you.
Start and verify it works
Now hit the health endpoint on loopback. /alive returns the server time with a 200:
A connection refused here means the container did not come up. Check journalctl -u vaultwarden -n 50. The usual cause is a stray $ in the admin token because the here-doc was not quoted, which makes Vaultwarden refuse to parse the config.
Put nginx in front
The last three proxy_set_header lines are the WebSocket-critical ones. X-Forwarded-For also matters beyond routing: it is the IP fail2ban will ban, so if you forget it every failed login looks like it came from 127.0.0.1 and the jail bans your own proxy.
Issue the TLS certificate
Certbot rewrites the nginx block to listen on 443, installs the certificate and adds the 80→443 redirect. Confirm the public endpoint answers over HTTPS:
First login, then close the door
Open https://vault.example.com, click Create account, and register the one account you actually want. Do it now, while signups are open.
Immediately disable open registration so nobody else can create an account on your server:
To add trusted users later, invite them from the admin page instead of reopening signups. Set INVITATIONS_ALLOWED=true and invite by email; that keeps the door shut to the internet while letting you onboard people one at a time.
Management commands
Before an upgrade, read the release notes for the version you are jumping to. Config keys occasionally get renamed between minor releases, and a removed key logs a warning rather than stopping the server, so it is easy to miss.
Production hardening
Firewall
Only SSH and the web ports should be reachable. The container is already on loopback, so nothing exposes 8080.
fail2ban on the login endpoint
Vaultwarden logs failed logins to the file we set as LOG_FILE. Two filters cover the two attack surfaces: the vault login and the admin page.
The IP in those log lines is whatever nginx passed in X-Forwarded-For. If your bans are all hitting 127.0.0.1, the proxy header is missing, not fail2ban.
Restrict the admin page
Even Argon2-hashed, the admin page is a brute-force target and a phishing surface. If you administer from a fixed address, lock /admin to it in nginx:
If you have no fixed address, the honest alternative is to leave ADMIN_TOKEN unset most of the time and only set it, restart, do your admin, then unset it again. An always-on admin page on a password server is a liability you are choosing to accept.
A backup you have restored
The database is a live SQLite file with a write-ahead log. Copy db.sqlite3 on its own while a write is in flight and you capture a torn state: the -wal holds committed changes the main file does not have yet, and your "backup" restores to a corrupt or stale vault. Use SQLite's online backup API, which is safe against concurrent writes:
Vaultwarden 1.37.3 also has a built-in command that does the same consistently: docker exec vaultwarden /vaultwarden backup (available since v1.32.1). Either way, schedule it and then restore it once, because a backup you have never restored is a hypothesis:
An ok from integrity_check means the snapshot is a consistent database. Copy the data folder off the box as well; a backup that lives only on the server it protects is not a backup. Karizanta offers scheduled off-site backups as a paid add-on, or you can push the tarballs to any object store from cron.
Where this can bite you
Live sync silently stops. Vaultwarden works fine in a browser with a broken WebSocket, so you only notice that a change on your phone never appears on your laptop. The cause is almost always the proxy dropping the upgrade. Test it directly:
A 101 is the handshake succeeding. A 400 or 200 means nginx swallowed the Upgrade and Connection headers, so recheck those three proxy_set_header lines.
The admin page left open to the world. The default posture once you set a token is that anyone who finds /admin gets a login form to hammer. The IP allow-list above, or unsetting the token between sessions, is the fix. Do not rely on the token alone.
A backup taken mid-write. Covered above, and it is the failure that hurts most because you discover it during a restore, when you have no good copy left. If you take only one thing from this guide, use the .backup command, never cp db.sqlite3.
Port 8080 already bound. If another service holds 8080 the container start fails with address already in use. Change the host side of the mapping in the unit (for example 127.0.0.1:8081:80) and match it in proxy_pass.
Where to run this
Vaultwarden is a low-resource, always-on service, which makes it a textbook fit for the cheapest plan rather than an argument for a bigger one. For a password server the deciding factor is usually jurisdiction, because this is the one box on your account holding every credential you own. If your users and legal footprint are in the UK, the UK VPS in Coventry keeps the vault under UK data protection on Karizanta's own hardware. If you want it inside the EU, the Sweden VPS in Stockholm sits in the GleSYS facility; the Netherlands VPS in Naaldwijk is peered into AMS-IX and DE-CIX if low-latency reach across the continent matters more. All three deploy in under 60 seconds, include DDoS filtering and IPv6, and let you add RAM later with one reboot. If your container start fails on a bound port, the walk-through in your container runs but the port is unreachable covers the four places that breaks.
Sources
- Vaultwarden Wiki (official) (unknown)
- Enabling WebSocket notifications - Vaultwarden Wiki (unknown)
- Backing up your vault - Vaultwarden Wiki (unknown)
- Releases - dani-garcia/vaultwarden (2026-09)
- Vaultwarden 1.37.3 - Freedom.Tech (2026-09)
Frequently asked questions
How much RAM does Vaultwarden need on a VPS?
512 MB is enough for a small team using SQLite. Vaultwarden idles well under 100 MB; the practical floor is your operating system, not the application. It runs comfortably on the smallest VPS plan, so there is no need to size up for it.
Do I still need port 3012 for Vaultwarden WebSocket sync?
No. Since v1.31.0 the separate WebSocket port 3012 was removed and live sync is served on the main HTTP port at /notifications/hub. You do not open an extra port, but your reverse proxy must forward the Upgrade and Connection headers or sync silently fails.
How do I stop other people creating accounts on my Vaultwarden server?
Register your own account first with SIGNUPS_ALLOWED=true, then set it to false and restart. To add trusted users afterwards, set INVITATIONS_ALLOWED=true and invite them by email from the admin page instead of reopening public registration.
What is the safe way to back up the Vaultwarden SQLite database?
Use SQLite's online backup API: sqlite3 db.sqlite3 ".backup 'out.sqlite3'", or the built-in /vaultwarden backup command (since v1.32.1). Copying db.sqlite3 directly while it is being written can capture a torn state, because committed changes may still be in the -wal file.
Is the Vaultwarden admin token safe to store in plain text?
No. Generate an Argon2 hash with 'vaultwarden hash' and store that PHC string as ADMIN_TOKEN, so a leaked config file does not leak the token. Also restrict the /admin page by IP or leave the token unset except when you are actively administering.


