跬步 On Coding

以Vaultwarden为例使用SQLite Litestream实现无数据库服务

前言

Vaultwarden是一个非官方实现的Bitwarden Server,用于密码管理,支持Web端、桌面端和移动端。它支持多种数据来存储数据,包括SQLite、PostgreSQL和MySQL等。当我们想把它部署在一个容器服务平台时,如果容器服务本身没有提供持久挂载的卷,那我们就只能使用PostgreSQL或者MySQL外部数据来存储数据。

但是我们并不想额外再买资源来运行一个数据库,那我们就可以使用SQLite来存储数据,并且使用Litestream来实现容灾备份。这样虽然容器服务没有持久存储,但是数据还是安全的。下面以Vaultwarden为例,使用SQLite和Litestream来实现无数据库服务。其它可以使用SQLite的程序也可以使用这种方式来实现无数据库服务。

1. litestream.yml

Litesream是一个开源的SQLite数据库备份工具,它使用Go语言开发。它可以监控一个SQLite数据库文件的变化,并且将变化记录到S3、MinIO等对象存储服务上。它巧妙的使用了SQLite的WAL(Write-Ahead Logging)机制,来记录数据库的变化,并流式地将这些变化记录到对象存储服务上,达到实时备份的目的。在服务启前,Litestream可以从对象存储服务上恢复数据到SQLite数据库文件中。它的实现原理可以参考这篇文章

在使用Litestream时,我们需要准备一个S3兼容的对象存储,这里以Cloudflare R2为例,我们需要准备一个litestream.yml的配置文件,内容如下:

dbs:
  - path: /data/db.sqlite3
    replicas:
      - type: s3
        endpoint: ${REPLICA_URL}
        bucket: sqlite

/data/db.sqlite3 是Vaultwarden的SQLite数据库文件路径,REPLICA_URL是对象存储服务的地址,比如 https://<cloudflare-account-id>.r2.cloudflarestorage.com,我们需要在Cloudflare R2上创建一个sqlite的存储桶。

2. entrypoint.sh

我们还需要准备一个entrypoint.sh的脚本文件,用于启动Liestream和Vaultwarden服务。内容如下:

#!/bin/bash
set -e

# Set the directory of the database in a variable
DB_PATH=/data/db.sqlite3

# Restore the database if it does not already exist.
if [ -f $DB_PATH ]; then
	echo "Database already exists, skipping restore"
else
	echo "No database found, restoring from replica if exists"
	litestream restore -if-replica-exists -config /etc/litestream.yml $DB_PATH
fi

# Litestream replicate database and start vaultwarden
echo "exec litestream"
exec litestream replicate -exec "/start.sh" -config /etc/litestream.yml

在启动Vaultwarden服务前,我们首先检查SQLite数据库文件是否存在,如果不存在则从对象存储服务上恢复数据。

3. Dockerfile

FROM litestream/litestream:latest AS litestream

FROM vaultwarden/server:latest

COPY --from=litestream /usr/local/bin/litestream /usr/local/bin/litestream

COPY --chmod=755 entrypoint.sh /usr/bin/entrypoint.sh
COPY litestream.yml /etc/litestream.yml

CMD ["/usr/bin/entrypoint.sh"]

这样会打一个新的镜像把Litestream和Vaultwarden服务一起打包,并且使用entrypoint.sh启动服务。

4. 启动服务

我们还要准备一个.env文件,用于设置环境变量:

LITESTREAM_ACCESS_KEY_ID=<cloudflare-r2-access-key-id>
LITESTREAM_SECRET_ACCESS_KEY=<cloudflare-r2-access-key>
PUSH_ENABLED=true
PUSH_INSTALLATION_ID=<push-installation-id>
PUSH_INSTALLATION_KEY=<push-installation-key>
PUSH_RELAY_BASE_URI=https://push.bitwarden.com
REPLICA_URL=https://<cloudflare-account-id>.r2.cloudflarestorage.com
SIGNUPS_ALLOWED=true

vaulewarden的PUSH配置需要到这里申请。

然后我们就可以使用Docker来启动服务了:

docker run -d \
  --name bitwarden \
  -p 80:80 \
  --restart unless-stopped \
  --env-file .env \
  vaultwarden/server:latest

这样我们就可以在没有持久存储的容器服务上无数据库的运行Vaultwarden服务了,比如GCP Code Run,我们可以把实例数量scale到0,只有流量进来时才启动服务,真正做到只有使用时才付费。

总结

我们使用Litestream实现了Vaultwarden的无数据库服务,并且使用Cloudflare R2作为对象存储服务。这样即使容器服务没有持久存储卷,数据也是安全的。类似的思路还可以推广到其它使用SQLite的程序上。比如我们自己开发了一个Django的程序,不依赖外部的数据库服务,也可以使用Litestream来实现容灾备份。