Self-hosting Vaultwarden on a Raspberry Pi

Note: The open source project Vaultwarden that I’m talking about in this post has formerly been named BitwardenRS, it has been renamed at the end of April 2021. I’ve updated this article where necessary.

I’ve been a long-time user of Enpass and while it’s a fine password manager I looked into Bitwarden some months ago and started using it. Bitwarden can be used as a managed service and that’s what I did for some time now. Same as Enpass it’s quite advanced and integrates nicely into my working environment. I’m using it on a Mac, in different browsers and on my mobile Android phone. There are some limitations in the free tier (no file attachments, no 2FA authenticator, …) but the premium account for individuals is fairly cheap, you only pay $10 per year. You’re still subject to limited functionality, though, for example with regards to using organization features.

Apart from using a managed service, you can run Bitwarden on your own hardware. With the official server software you’re still applicable to licence fees, however. There’s also a Rust implementation of the Bitwarden server, it’s named Vaultwarden (formerly BitwardenRS). It’s less resource hungry than the original server software, uses different database technology and you are not applicable to the licencing model mentioned above. All the official Bitwarden client applications still work, so the user experience doesn’t change at all. This sounded interesting so I decided to give it a try.

Docker Compose setup

Vaultwarden can be run using Docker/Docker Compose and it was less then a day to get the basic setup running on a Raspberry PI 3 Model B. The official wiki is comprehensive and easy to consume, so that helped a lot. Here’s my docker-compose file that will start Vaultwarden alongside Caddy as a reverse proxy.

version: '3'
services:
bitwarden:
image: vaultwarden/server:latest
container_name: vaultwarden
restart: always
environment:
- WEBSOCKET_ENABLED=true
- SIGNUPS_ALLOWED=false
- DOMAIN=<domain_name>
- SMTP_HOST=<smtp_host>
- SMTP_FROM=<sender_address>
- SMTP_PORT=587
- SMTP_SSL=true
- SMTP_USERNAME=<sender_username>
- SMTP_PASSWORD=<sender_password>
- ADMIN_TOKEN=<admin token>
volumes:
- ./bw-data:/data
caddy:
image: caddy:2
container_name: caddy
restart: always
ports:
- 80:80 # Needed for the ACME HTTP-01 challenge.
- 443:443
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy-config:/config
- ./caddy-data:/data
environment:
- DOMAIN=<domain_name>
- EMAIL=<email@example.com> # The email address to use for ACME registration.
- LOG_FILE=/data/access.log

You’ll also need to update the Caddyfile with the container name:

{$DOMAIN}:443 {
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 gzip
# Notifications redirected to the WebSocket server
reverse_proxy /notifications/hub vaultwarden:3012
# Proxy everything else to Rocket
reverse_proxy bitwarden:80 {
# Send the true remote IP to Rocket, so that bitwarden_rs can put this in the
# log, so that fail2ban can ban the correct IP.
header_up X-Real-IP {remote_host}
}
}
view raw Caddyfile hosted with ❤ by GitHub

You’ll have to fill out the blanks, once you’ve done this Caddy will automatically setup Letsencrypt certificates for you and you are good to go. In order to be reachable over the public internet using a fixed hostname, I have configured a dynamic hostname with ddnss.de, it is updated by my internet router. To make it a bit nicer I have a DNS record configured on a domain I own that forwards to that dynamic hostname using a CNAME DNS entry.

The docker-compose file contains a number of environment variables, some of them are needed so that Vaultwarden can send emails (e.g. for invitations). You can use your own email provider or setup a dedicated smtp service. The wiki recommends SendGrid or MailJet for this, you can get away with their free plans which allow you to send at least 100 mails per day. I went with MailJet, as SendGrid didn’t send their verification emails out – not a smart thing to fail at, as a mail service.

Another environment variable (“ADMIN_TOKEN”) settings to enable the admin page. You can use it to manage settings, users, organisations and more. The wiki again does a good job to explain why and how to set this up.

Creating a system service

Docker-Compose is great for running containers manually, but I wanted to integrate Vaultwarden into the operating system, so that it could be started automatically upon reboot. Here’s my systemd service file (vaultwarden.service) that can be used to start and stop Vaultwarden.

[Unit]
Description=Vaultwarden
After=docker.service network.target
Requires=docker.service network-online.target
[Service]
RemainAfterExit=true
WorkingDirectory=/home/pi
ExecStartPre=/usr/local/bin/docker-compose pull --quiet
ExecStart=/usr/local/bin/docker-compose up
ExecReload=/usr/local/bin/docker-compose pull --quiet
ExecReload=/usr/local/bin/docker-compose up
ExecStop=/usr/local/bin/docker-compose down
Restart=always
RestartSec=30s
[Install]
WantedBy=multi-user.target

The service needs to be enabled, so that Vaultwaren automatically starts on reboots:

$ sudo systemctl enable vaultwarden.service

Backups

If you run a service like this, you better think about a backup strategy. Vaultwarden helps you, though, as the wiki has a section on this, as well. There’s different aspects to take into account: You’ll definitely want your vault to be backed up, but you also most definitly want attachments to be taken into account. Here’s my systemd service (vaultwarden-backup.service) and timer (vaultwarden-backup.timer) that will export the database once a day and will also tarzip the attachments directory.

[Unit]
Description=backup the vaultwarden sqlite database and attachments
[Service]
Type=oneshot
WorkingDirectory=/media/backup
ExecStart=/usr/bin/env sh -c 'sqlite3 /home/pi/bw-data//db.sqlite3 ".backup backup-$(date -Is | tr : _).sq3"'
ExecStart=/usr/bin/env sh -c 'tar cvfz bw-attachments-$(date -Is | tr : _).tar.gz /home/pi/bw-data/attachments/'
ExecStart=/usr/bin/find . -type f -mtime +30 -name 'backup*' -delete
ExecStart=/usr/bin/find . -type f -mtime +30 -name 'bw-attachments*' -delete
[Unit]
Description=schedule vaultwarden backups
[Timer]
OnCalendar=04:00
Persistent=true
[Install]
WantedBy=multi-user.target

Don’t forget to enable and start the timer:

$ sudo systemctl enable vaultwarden-backup.timer
$ sudo systemctl start vaultwarden-backup.timer

My backup files are stored on a USB stick, instead of mounting it through /etc/fstab, I’m also utilising systemd‘s mounting facilities. Here’s my configuration (media-backup.mount and media-backup.automount):

[Unit]
Description=Additional drive
[Mount]
What=/dev/disk/by-uuid/6080-1BC0
Where=/media/backup
Type=exfat
Options=defaults
[Install]
WantedBy=multi-user.target
[Unit]
Description=Automount USB Backup
[Automount]
Where=/media/backup
[Install]
WantedBy=multi-user.target

The mount-unit sets up the device for manual mounting, the automount-unit tells systemd to automatically mount the device on startup. It’s important to name the unit after the mount point, i.e. the Where attribute of the unit. And you have to use dashes to separate path segments (here: media-backup.mount).

If you want to restore from a backup, you just have to shut down Vaultwarden and replace the live db.sqlite3 file with the one from your backup. Also replace the live attachement directory with the tar-ed version from the backup.

Conclusion

In order to use your new Vaultwarden server in your client apps, you need to configure the server url before logging in. There’s a small setting icon on the login screen, almost unnoticable. Make sure you setup the connection url there.

It was easy and fun to set up Vaultwarden, I was surprised how fast and without much latency it works on a Raspberry Pi on a home connection. I’ve imported my Enpass vault to Bitwarden before without any problems, and also migrating a LastPass account to another user account was easy and painless. Although I try to refrain from hosting too many services myself – I just don’t have enough time to keep up – I have a good feeling about this. And I’m happy my old Raspberry Pi has a new task.