DEV Community

Cover image for Self-Hosting a Password Manager on a Pi Zero 2W
Priyanshu Verma
Priyanshu Verma

Posted on

Self-Hosting a Password Manager on a Pi Zero 2W

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!
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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:

  1. 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.
  2. 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.
  3. 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
}
Enter fullscreen mode Exit fullscreen mode

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}
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. No persistent volume: Fixed by mapping ./vw-data:/data in Docker Compose.
  2. Subpath asset/WebSocket issues: Resolved with handle_path /pass/* in Caddy.
  3. SSL warnings: Generated local CA with mkcert, imported into desktop and mobile devices.
  4. Trailing slash problems: Also handled by handle_path.
  5. Remote access quirks: Fixed via local DNS entries and correct certificate paths.
  6. 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)