Hosting Vaultwarden on fly.io for free
After Lastpass suffered a massive data breach last year, I began looking for an alternative password manager. Bitwarden is another centralized password manager like LastPass, but with the added benefit of being completely open source! They also allow you to spin up your own Bitwarden server and connect to them with all of their clients.
Fly.io has a generous free tier, allowing up to 3 virtual machines with 256 MB of RAM, and 3 GB of persistent volume. 256 MB of ram isn’t a lot, but it should be enough, right? As it turns out, Bitwarden’s official server requires a minimum of 2 GIGABYTES of RAM. That is way more than we can afford. This is where Vaultwarden comes in. It’s an unofficial server for Bitwarden, which is written in Rust, runs in a single process with minimal RAM usage, and uses SQLite as the database. They even have a simple Docker image, which we will be using to spin up the server on fly.io.
Getting the config #
The first step is to make an account on fly.io, and then install the CLI. Once you have the CLI installed, you can create a new app:
$ fly launch ? Choose an app name (leave blank to generate one): vault-aarol ? Choose a region for deployment: Stockholm, Sweden (arn) Created app 'vault-aarol' in organization 'personal' Admin URL: https://fly.io/apps/vault-aarol Hostname: vault-aarol.fly.dev Wrote config file fly.toml
Choose an appropriate name for the app, and the location closest to you.
fly launch will generate a
fly.toml, which should look something like this:
app = "vault-aarol" kill_signal = "SIGINT" kill_timeout = 5 primary_region = "arn" processes =  [env] [experimental] auto_rollback = true [[services]] http_checks =  internal_port = 8080 processes = ["app"] protocol = "tcp" script_checks =  [services.concurrency] hard_limit = 25 soft_limit = 20 type = "connections" [[services.ports]] force_https = true handlers = ["http"] port = 80 [[services.ports]] handlers = ["tls", "http"] port = 443 [[services.tcp_checks]] grace_period = "1s" interval = "15s" restart_limit = 0 timeout = "2s"
The generated file consists mostly of networking stuff. We don’t need to worry about enabling HTTPS, because fly.io already manages that for us. We can instead focus on the actual application, which is just a couple lines more.
Copy these lines to the config, replacing the empty “[env]” block with the following:
[env] SIGNUPS_ALLOWED = "true" [build] image = "vaultwarden/server:latest" [mounts] source = "vw_data" destination = "/data"
There are a bunch of environment variables to configure Vaultwarden, but I’ve singled out the most important one. Since our server will be open to the whole internet, security is going to be very important. Disabling signups means that the potential attack surface is reduced to a single login page. You should set “SIGNUPS_ALLOWED” to false after creating your account(s), just to be sure.
We also need to change
internal_port to 80, since that is what the Vaultwarden Docker image uses.
[[services]] http_checks =  internal_port = 80
Creating the volume #
If we would try to deploy now, we would get an error, because the mount point called “wv_data” doesn’t exist yet. We can create it with the following:
$ fly volumes create vw_data --size 1
Here we create a volume
vw_data with a size of 1GB (vs. the default 3GB), since it’s more than enough for a password manager, and also because we’re nice.
Running the command will probably give you a huge red warning like this:
It’s complaining because we’re only creating a single volume for our app. Fly.io volumes are NVME drives on the physical server that your app runs on. If the NVME drive fails, it will bring the whole app down, and might even cause permanent data loss :(
The docs also say the following:
- You can’t share a volume between apps, nor can two VMs mount the same volume at the same time. A single VM can only mount one volume at a time.
- If you need the volumes to sync up, your app has to make that happen.
- If you only have a single copy of your data on a single Fly Volume, and that drive fails, data is lost. Fly.io takes daily snapshots, retained for 5 days, but these are meant as a backstop, not your primary backup method.
If we wanted to use two volumes, we would need two separate apps using two different volumes, and would need to somehow keep them in sync. That sounds way too complicated. I’d rather keep it simple and use one volume/app, even at the cost of some reliability.
Encrypted copies of personal vaults are kept on logged-in devices for offline use. Even in the worst possible scenario, where the whole server is gone, one could still export the vault as JSON from a device that has a copy of the vault.
I’ve been using a single volume for about three months, and so far my app has stayed up without any downtime. Three months isn’t a long time though – I’ll update this page if something happens.
Another alternative to volumes is Fly Postgres, which can be clustered, and thus doesn’t rely on a single NVME drive. Vaultwarden has support for Postgres as well. I might look into it in the future.
Anyway, enter ‘Y’ to skip the warning. Now we have a volume and a
fly.toml. We can finally deploy:
$ fly deploy ==> Verifying app config Validating ~/vault/fly.toml Platform: machines ✓ Configuration is valid --> Verified app config ==> Building image Searching for image 'vaultwarden/server:latest' remotely... image found: img_3mno4wmd3kmvk18q Deploying vault-aarol app with rolling strategy Machine e784ee72cdd783 [app] update finished: success Finished deploying
If it deploys successfully, the provided URL should show..
Boom! Now we have a fully working Vaultwarden instance! Whenever you want to upgrade the Vaultwarden version, simply run
fly deploy again.
Using your own domain #
The provided domain (https://[whatever]-vault.fly.dev) is pretty nice, but even nicer is to use your own domain. Lets say your domain is
example.com. Wouldn’t it be nice to access the vault from
First, you need to set a CNAME (alias) record that points to the fly domain. There are a billion DNS management systems, but it should look something like this:
This sets a CNAME record for a subdomain, so that the URL will be
Notice the dot after
fly.dev. If you don’t have the trailing dot, it’s going to alias it to
[whatever]-vault.fly.dev.example.com , which is just wrong.
Next, you need to add a certificate for that subdomain on fly.io’s side:
$ fly certs add vault.example.com
vault.example.com should show the same login page. All the actual traffic is going to the fly.dev domain, but the address in the URL bar looks nice and clean!
Backing up the data #
As you know from before, Fly.io takes daily snapshots of your volumes, and keeps those snapshots for 5 days. They also recommend another primary backup method. Let’s make one!
The vaultwarden Docker image stores the data in the /data directory. Let’s SSH into the app:
$ fly ssh console Connecting to 7b36:2bcb:e402:4de9:dbb2:2... complete # ls bin dev home media proc sbin sys var boot etc lib mnt root srv tmp vaultwarden data healthcheck.sh lib64 opt run start.sh usr web-vault # ls /data attachments db.sqlite3-shm icon_cache rsa_key.pem sends db.sqlite3 db.sqlite3-wal lost+found rsa_key.pub.pem tmp
As you can see, the data directory contains a SQLite database and other important files.
db.sqlite3 contains all of the user data, and is fully encrypted. To automate the backup process, I’ve written the PowerShell script below.
Create a new file
backup.ps1 in the same directory as the
$DATE = get-date -Format "yyyy-MM-dd" $INSTALL_SQLITE = "apt-get update && apt-get install sqlite3 -y" $BACKUP_DB = "sqlite3 /data/db.sqlite3 '.backup /data/db.bak'" $CREATE_ARCHIVE = "tar -czf $DATE.tar.gz data" fly ssh console -q -C "bash -c ""$INSTALL_SQLITE && $BACKUP_DB && $CREATE_ARCHIVE"" " fly sftp get "$DATE.tar.gz"
Basically, it will SSH into the app, create a backup of the database using the
sqlite3 CLI, and then create a gzipped tar archive of the entire data directory. Finally, it transfers the archive to the current working directory using SFTP.
The script ended up being more work than anticipated, but I’ve tried to make it a bit more readable. You can run the script using Powershell:
powershell ./backup.ps1. A file that looks like
2023-04-21.tar.gz should now appear in the directory.
The file is very small, about 2 MB for my vault. Even that size is inflated because of the
icon_cache directory, which contains icons for every website in the vault.
The naive way to backup a sqlite database is just to
cp it. However, if the database happens to be modified during the backup, it may lead to corruption.
.backup command will prevent this by locking the database for the duration of the copy, which should only be a fraction of a second. Restoring the database should be as simple as running
rm db.sqlite3 && mv db.bak db.sqlite3
If you don’t want to do run the command manually, you can register a schedule to execute it automatically.
Websocket support #
As of Vaultwarden version 1.29, Websocket support is enabled by default 🎉. This means that any changes you make to the vault should be instantly visible on all devices. Previously, this required a reverse proxy (NGINX, Caddy) because the Websocket server was running on a different port.