As a developer and tinkerer, I love taking control of my digital environment. This weekend, I decided to host my own password manager. My goal was simple: keep all my sensitive data private, accessible from multiple devices, while learning more about self-hosting, TLS, reverse proxies, and Docker on a low-power device. Here’s how the journey unfolded, along with problems I faced and solutions I discovered.
Choosing the Right Tool
I started by exploring the options. Cloud-based managers like 1Password or Bitwarden are convenient, but I wanted full control. Among self-hosted options, Vaultwarden stood out. It’s a lightweight, unofficial Bitwarden implementation, supports TOTP, and can run on low-resource devices like a Pi Zero 2W.
For a solo developer like me, Vaultwarden was ideal: small footprint, actively maintained, and feature-rich.
Step 1: Preparing the Pi
I installed Ubuntu on the Pi Zero 2W and set up SSH access to manage it remotely. Since I planned to use Docker, I made sure the system was updated and installed docker
and docker-compose
.
After verifying Docker installation with docker --version
, I was ready to pull Vaultwarden.
Step 2: Running Vaultwarden in Docker
My first attempt to run Vaultwarden showed an important warning:
No persistent volume!
This meant that if the container stopped or was updated, all data would be lost. Lesson one: always map persistent volumes. I created a vw-data
directory and updated docker-compose.yml
:
vaultwarden:
image: vaultwarden/server:latest
container_name: vaultwarden
restart: unless-stopped
environment:
ADMIN_TOKEN: "changeme"
ROCKET_WORKERS: 1
ROCKET_PROXY_PATH: "/pass"
volumes:
- ./vw-data:/data
expose:
- "80"
The ROCKET_PROXY_PATH="/pass"
was critical.
Step 3: Subpath vs Subdomain
At this stage, I faced a design choice:
-
Option 1: Subdomain (
vault.mydomain.com
) -
Option 2: Subpath (
mydomain.com/pass
)
Initially, subdomains seemed neat and clean. But there are trade-offs for self-hosted services:
-
Certificate complexity: With a subdomain, I would need a separate certificate for
vault.mydomain.com
. With a local or internal CA, managing multiple subdomains could get messy. - Future services: I wanted to host other tools (like a personal wiki or file server). Using a subdomain for every service could clutter DNS and complicate VPN/local routing.
- Tailscale & mobile access: VPN solutions sometimes resolve only the main hostname. Subdomains occasionally fail or require additional DNS tweaks.
A subpath approach (/pass
) solved these issues:
- One certificate covers the main domain.
- Reverse proxy handles routing internally to different services.
- Makes scaling to multiple services simpler without touching DNS or VPN settings.
Hence, I chose /pass
for Vaultwarden and left /
free for other services. This required careful reverse proxy configuration, which I’ll describe next.
Step 4: Reverse Proxy with Caddy
I used Caddy as a reverse proxy to handle HTTPS and route /pass
to Vaultwarden. My first attempt was basic:
panda.local {
tls internal
encode zstd gzip
reverse_proxy vaultwarden:80
}
Problems immediately appeared: assets broke, WebSockets failed, and trailing slashes caused the interface to misbehave. Vaultwarden expects requests at the root, so a simple proxy was insufficient.
After research and testing, I implemented:
handle_path /pass/* {
reverse_proxy vaultwarden:80 {
header_up X-Real-IP {remote_host}
header_up X-Forwarded-Proto {scheme}
header_up X-Forwarded-Host {host}
header_up X-Forwarded-For {remote_host}
}
}
handle_path /pass/*
strips the /pass
prefix before forwarding requests to Vaultwarden. This solved:
- Asset loading issues
- WebSocket failures
- Trailing slash inconsistencies
Step 5: TLS and Certificates
Since I wanted both desktop and mobile apps to trust my self-signed setup, I used mkcert to generate a local CA and certificates:
mkcert panda.local
mkcert pass.local
Copying the root CA to my desktop required careful path handling due to spaces in usernames, and mobile installation required converting .pem
to .crt
and manually trusting the certificate. Once done, browsers and apps stopped showing SSL warnings.
Lesson learned: TLS is critical, but self-hosting means you need to manage trust explicitly on each device.
Step 6: Remote Access with VPN
I didn’t want to open my home network to the internet. Using a VPN-like solution, I could access Vaultwarden remotely without exposing ports. Initially, there were DNS resolution issues with subdomains, but after adding entries to local hosts and configuring certificates correctly, remote access worked smoothly.
This setup also allowed me to think ahead: by using /pass
, I left the root path free for future services, creating a small personal service ecosystem.
Step 7: Troubleshooting Common Issues
Here’s a list of the main problems I faced and how I solved them:
-
No persistent volume: Fixed by mapping
./vw-data:/data
in Docker Compose. -
Subpath asset/WebSocket issues: Resolved with
handle_path /pass/*
in Caddy. - SSL warnings: Generated local CA with mkcert, imported into desktop and mobile devices.
-
Trailing slash problems: Also handled by
handle_path
. - Remote access quirks: Fixed via local DNS entries and correct certificate paths.
- Subdomain vs subpath confusion: Choosing subpath simplified certificates and routing for multiple services.
Step 8: Final Setup
By the end of the day, I had:
- Vaultwarden running under
/pass
on the Pi Zero 2W - Full HTTPS support trusted on desktop and mobile
- Remote access without exposing home network ports
- Persistent storage for all passwords
Testing everything — creating accounts, logging in from mobile apps, syncing TOTP codes — everything worked flawlessly.
Reflections and Lessons Learned
Self-hosting is rewarding, but it comes with challenges:
- Docker volumes prevent accidental data loss.
- Reverse proxies can make or break subpath hosting.
- TLS and certificates are critical; trust must be manually managed for internal or mobile clients.
- Subpath routing simplifies multi-service hosting, while subdomains require additional DNS and cert management.
Small fixes like handle_path
or importing a CA may seem trivial, but they make the difference between a broken interface and a smooth user experience.
Conclusion
Self-hosting Vaultwarden on a Pi Zero 2W might seem overkill for a single user, but for me, it was about control, privacy, and learning. The Pi handled everything surprisingly well, and Vaultwarden proved to be a solid, lightweight solution.
For developers looking to take control of their digital lives: start small, expect roadblocks, and treat each error as a learning opportunity. In the end, there’s immense satisfaction in seeing your own system, from certificates to WebSockets, working exactly the way you designed it.
Top comments (0)