Running Your Own Password Manager, Properly Secured
Vaultwarden is a lightweight, self-hosted alternative to Bitwarden’s official server, written in Rust and designed to run on modest hardware without the resource overhead of the full Bitwarden stack. It supports the complete Bitwarden client ecosystem – browser extensions, mobile apps, desktop clients – while giving you full control over where your vault data lives. For anyone already running a home server or a small VPS, it’s a natural fit.
The catch is that a password manager exposed to the internet without proper HTTPS and two-factor authentication is worse than useless – it’s a liability. This guide walks through the full setup: Vaultwarden running in Docker, Nginx Proxy Manager handling SSL termination, and TOTP-based 2FA locking down every login. No shortcuts, no optional steps.

Prerequisites and Environment
This setup assumes a Linux host – Ubuntu 22.04 or Debian 12 work cleanly – with Docker and Docker Compose already installed. You’ll also need a domain name with DNS pointing to your server’s public IP, since Nginx Proxy Manager needs a valid domain to issue a Let’s Encrypt certificate. A wildcard subdomain works fine; a dedicated subdomain like vault.yourdomain.com is the cleaner choice. Port 80 and 443 must be open on your firewall and forwarded through your router if you’re running this at home.
Nginx Proxy Manager itself should already be running and accessible on your network before you start the Vaultwarden container. If you’re not already using it, stand up NPM first with its own Docker Compose stack, log into the admin UI, and confirm it can issue a test certificate. Trying to debug Vaultwarden and NPM simultaneously when neither is working yet is a time sink you don’t need.
One decision to make upfront: where does your Vaultwarden data actually live? The default Docker volume works, but mapping a bind mount to a specific directory on the host – say, /opt/vaultwarden/data – makes backups straightforward and keeps your data visible without digging through Docker internals. Use the bind mount approach. You’ll thank yourself when it’s time to migrate or restore.
Deploying Vaultwarden with Docker Compose
Create a working directory and drop in a docker-compose.yml file. The core service definition is minimal:
- Image: vaultwarden/server:latest
- Restart policy: unless-stopped
- Volume: bind mount from /opt/vaultwarden/data to /data
- Environment variable: DOMAIN=https://vault.yourdomain.com
- Environment variable: SIGNUPS_ALLOWED=true (set to false after creating your account)
- Environment variable: WEBSOCKET_ENABLED=true (required for real-time sync across clients)
- Port: map host port 8080 to container port 80, and host port 3012 to container port 3012 for WebSocket
Run docker compose up -d and confirm the container starts cleanly with docker compose logs -f. The service won’t be usable yet without the proxy layer in front of it, but you can confirm it’s listening by curling localhost:8080 from the host machine. If you see an HTML response, the container is healthy.

Configuring Nginx Proxy Manager for SSL
Open the NPM admin interface and navigate to Proxy Hosts, then create a new entry. Set the domain name to vault.yourdomain.com, the scheme to http, the forward hostname to the Docker host’s internal IP (or the container name if both stacks share a Docker network), and the forward port to 8080. Enable “Block Common Exploits” and “Websockets Support” – the WebSocket toggle is non-negotiable for Vaultwarden, since without it the browser extension and desktop app won’t sync in real time.
Switch to the SSL tab within the same proxy host configuration. Request a new Let’s Encrypt certificate for the domain, enable “Force SSL” and “HTTP/2 Support,” and save. NPM handles the ACME challenge automatically as long as port 80 is reachable. Within a minute, the certificate should issue and the green padlock should appear. At this point, navigating to your vault URL in a browser should show the Vaultwarden login screen over HTTPS. If it doesn’t, check that your DNS record has propagated and that no upstream firewall is blocking the challenge request.
Vaultwarden also uses a separate WebSocket connection on port 3012. In NPM, you can handle this by adding a custom Nginx configuration block under the “Advanced” tab of the proxy host. The block should proxy /notifications/hub to the container’s port 3012 with appropriate WebSocket upgrade headers. Without this, the client will fall back to polling, which works but drains battery on mobile devices and slows down vault updates across sessions.
One additional setting worth enabling in Vaultwarden’s environment variables: ROCKET_LOG=critical to cut down on noise in the logs, and LOG_LEVEL=warn for the same reason. Vaultwarden is chatty by default, and once everything is running correctly you don’t need to see every successful authentication event filling your log files. Set these before you go live with real credentials in the vault.
Enabling Two-Factor Authentication
Once your account is created and the SSL connection is confirmed, 2FA setup happens entirely inside the Vaultwarden web vault. Log in, navigate to Account Settings, then Security, then Two-step Login. Vaultwarden supports TOTP via authenticator apps, email codes, and – with a Bitwarden Premium equivalent license key in the config – FIDO2 hardware keys. For most self-hosters, TOTP is the right choice: it works offline, doesn’t depend on email delivery, and pairs with any standard authenticator app.
Select the Authenticator App option, scan the QR code with your app of choice, and enter the six-digit code to confirm. Vaultwarden will display a set of recovery codes – save these somewhere that isn’t inside the vault you’re currently trying to protect. A printed copy in a secure location, or an encrypted note in a separate system, is the standard approach. Storing your 2FA recovery codes inside Vaultwarden itself is the digital equivalent of keeping your spare house key inside the locked house.
After confirming 2FA is active, go back to your Docker Compose file and set SIGNUPS_ALLOWED=false. Run docker compose up -d again to apply the change. This disables open registration on your instance – anyone who finds your URL can no longer self-register an account. If you’re running this for a small family or team, the admin token environment variable (ADMIN_TOKEN) lets you access a backend panel at /admin to invite users manually without opening registration to everyone.
The admin panel itself should be treated with the same caution as the vault. Set a long, random ADMIN_TOKEN value – generate one with openssl rand -base64 48 – and consider restricting access to /admin at the proxy level by IP allowlist if NPM’s advanced configuration supports it in your setup. The admin panel bypasses normal user authentication, so an exposed admin token with a weak value is a direct path to your entire vault database.

With HTTPS enforced, 2FA active, and open registration disabled, the attack surface on a Vaultwarden instance shrinks to essentially brute-force attempts against a login form that requires both a strong password and a rotating TOTP code. Pair that with fail2ban watching the Vaultwarden logs for repeated failed logins – which Vaultwarden logs clearly enough to parse – and you have a self-hosted password manager that holds up to real-world threat conditions rather than just looking secure on paper.





