Vaultwarden 自建密码管理器实战:Docker 部署、Nginx 反代与数据备份全流程

为什么个人站长应该自建密码管理器

做站时间一长,手里攒下的密码会多到失控:服务器 SSH、宝塔面板、MySQL 数据库、CDN 后台、域名注册商、搜索引擎站长平台、邮件服务商、各种 API Key,还有自己博客的后台。这些密码如果全部用同一套,那等于一个被拖库全盘沦陷;如果每个都不同,靠脑子根本记不住。用浏览器自带的密码同步?数据在别人手里,而且跨设备、跨浏览器一塌糊涂。用在线密码管理器?免费版功能阉割,付费版又是一笔长期订阅费。

Vaultwarden 是 Bitwarden 服务端的一个轻量级 Rust 开源实现。Bitwarden 官方服务端是 .NET + 一堆微服务,跑起来要吃掉 2GB 内存;而 Vaultwarden 单进程只占几十 MB,一台 1 核 1G 的小鸡就能稳稳跑住。最大的好处是:你用的还是 Bitwarden 官方客户端(浏览器扩展、手机 App、桌面端全都有),但数据全部存在你自己的服务器上,端到端加密,服务端拿到的也只是密文。这篇就完整走一遍从 Docker 部署、Nginx 反代、开启注册保护到数据备份的全流程,个人站长照着做半小时能上线。

准备工作:域名、证书与 Docker

Vaultwarden 强烈建议走 HTTPS,因为浏览器扩展和手机客户端对非 HTTPS 的自建服务限制很多(Web Crypto API 在非安全上下文里会被禁用,导致根本没法注册和登录)。所以先准备一个子域名,比如 vault.example.com,解析到你的服务器,并且申请好证书。用 acme.sh 或 certbot 都行,下面假设证书已经放在 /etc/letsencrypt/live/vault.example.com/。

然后确认 Docker 和 Docker Compose 已经装好。绝大多数 VPS 用官方脚本一条命令即可。这里我们把数据目录规划到 /opt/vaultwarden,所有持久化数据都放这个目录,方便备份,也方便以后迁移。

mkdir -p /opt/vaultwarden/data
cd /opt/vaultwarden

编写 docker-compose.yml

直接上 Compose 文件,比一堆 docker run 参数好维护。注意几个关键点:数据卷挂载到宿主机的 ./data;用 environment 传环境变量;把容器端口只绑定到 127.0.0.1,外网访问一律经 Nginx 反代,绝不直接暴露容器端口。

services:
  vaultwarden:
    image: vaultwarden/server:latest
    container_name: vaultwarden
    restart: unless-stopped
    environment:
      DOMAIN: "https://vault.example.com"
      SIGNUPS_ALLOWED: "true"
      INVITATIONS_ALLOWED: "false"
      WEBSOCKET_ENABLED: "true"
      ADMIN_TOKEN: "请替换成你自己的强随机串"
      SIGNUPS_VERIFY: "true"
      SMTP_HOST: "smtp.example.com"
      SMTP_FROM: "vault@example.com"
      SMTP_PORT: "465"
      SMTP_SECURITY: "force_tls"
      SMTP_USERNAME: "vault@example.com"
      SMTP_PASSWORD: "你的邮箱密码或授权码"
    volumes:
      - ./data:/data
    ports:
      - "127.0.0.1:8088:80"

这里解释几个最容易踩坑的变量。第一,DOMAIN 必须写成带 https:// 的完整地址,写错会导致客户端登录后附件下载、WebSocket 通知全部异常。第二,SIGNUPS_ALLOWED 先开成 true,是为了让你自己注册第一个账号;注册完立刻改回 false 并重启,否则任何人都能注册进来。第三,ADMIN_TOKEN 是访问 /admin 管理页的口令,一定要用强随机串,可以用 openssl rand -base64 48 生成。第四,WEBSOCKET_ENABLED 打开后 Nginx 反代要额外处理 Upgrade 头,否则实时同步会退化成轮询。

Nginx 反向代理配置

Vaultwarden 的反代有个著名坑:客户端上传大附件走的是分片上传,容易撞上默认的 client_max_body_size 限制;另外 WebSocket 通知需要正确转发 Upgrade 头。下面这份配置把两者都照顾到了。

server {
    listen 80;
    server_name vault.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name vault.example.com;

    ssl_certificate     /etc/letsencrypt/live/vault.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/vault.example.com/privkey.pem;

    client_max_body_size 525M;

    location / {
        proxy_pass http://127.0.0.1:8088;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location /notifications/hub {
        proxy_pass http://127.0.0.1:8088;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    location /notifications/hub/negotiate {
        proxy_pass http://127.0.0.1:8088;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

注意 /notifications/hub 这一段必须单独拎出来转发 Upgrade 头,否则 WebSocket 连不上,客户端会一直在日志里刷重连错误。client_max_body_size 给到 500M 以上,是因为 Bitwarden 客户端默认的分片大小和服务端协商有关,给太小会导致大附件上传到一半失败。

启动与初始化

配置写好后先做一次语法检查,再启动容器,最后 reload Nginx:

nginx -t
cd /opt/vaultwarden && docker compose up -d
docker compose logs -f --tail=50
nginx -s reload

日志里看到 Rocket has launched from https://0.0.0.0:80 之类的字样就算起来了。此时浏览器打开 https://vault.example.com,应该能看到 Bitwarden 的登录界面。点「创建账户」,用你的邮箱注册第一个账号。密码建议至少 14 位,因为这个主密码是解密整个密码库的唯一钥匙——服务端拿到的是密文,一旦主密码忘了,谁都救不回来。

注册成功后立刻回到 docker-compose.yml,把 SIGNUPS_ALLOWED 改成 "false",然后 docker compose up -d 重建。这样外人就无法注册了。如果以后想再拉个人进来,可以用管理员页面发邀请,或者临时把开关打开、拉完人再关掉。

客户端接入

登录 https://vault.example.com 后进入个人设置,里面有一个「自托管环境」的地址,把这个地址填进各个客户端。浏览器扩展(Chrome/Firefox/Edge)和手机 App 在登录界面都有一个齿轮或「设置服务器地址」的入口,把 https://vault.example.com 填进去,再登录即可。桌面端同理。所有设备从此共用同一个端到端加密的密码库,新增密码在一台设备上保存,其他设备几秒内自动同步。

建议顺手开启两步验证。Vaultwarden 支持 TOTP,可以配合任意认证器 App。开启后登录需要主密码加动态码,即便主密码泄露,攻击者也进不来。恢复码一定要抄下来另存,别只存在 Vaultwarden 里,否则哪天手机丢了会把自己锁在门外。

数据备份:自建服务的生命线

自建最大的风险就是数据在自己手里,丢了没人赔。Vaultwarden 的全部状态就在 /opt/vaultwarden/data 这个目录里:db.sqlite3 是数据库,attachments/ 是附件,sends/ 是临时分享,rsa_key* 是签名密钥。备份其实很简单,但要注意一致性。

最稳妥的做法是先在容器里用 sqlite 自带的备份命令导出一份,再连同目录一起打包:

#!/bin/bash
set -e
DATE=$(date +%F)
BACKUP_DIR=/opt/vaultwarden/backup
mkdir -p "$BACKUP_DIR"
# SQLite 在线安全备份,避免直接 cp 拿到损坏文件
docker exec vaultwarden sqlite3 /data/db.sqlite3 ".backup '/data/db.backup.sqlite3'"
cp /opt/vaultwarden/data/db.backup.sqlite3 "$BACKUP_DIR/db-$DATE.sqlite3"
tar czf "$BACKUP_DIR/vaultwarden-$DATE.tar.gz" -C /opt/vaultwarden data
# 只保留最近 14 天
find "$BACKUP_DIR" -name "*.tar.gz" -mtime +14 -delete
echo "backup done: $DATE"

把这段存成 /opt/vaultwarden/backup.sh,加执行权限,然后挂个 crontab 每天凌晨跑一次。有一点务必提醒:备份文件里包含你的加密密码库,绝不能对外可读。目录权限设成 700,用 chmod 600 锁住 tar 包。更进一步,可以把这个备份再通过 rclone 同步到对象存储或异地机器(前提是传输通道加密、存储桶私有),形成 3-2-1 备份。

顺手可以做的一件事:导入浏览器里已有的密码

刚部署完密码库最烦的就是「空库」——几十上百个账号等着你手动录入,谁也没这个耐心。这里有个省事的办法:先从浏览器(Chrome、Edge、Firefox 都支持)把现有密码导出成一个 CSV 文件,然后登录 Vaultwarden 网页端,在「工具」里选择「导入数据」,指定格式为 Bitwarden(或 Chrome),上传 CSV,一次就把所有条目导进来。

导入之后有三件事必须马上做。第一,把那份 CSV 原件彻底删除。CSV 是未加密的明文,你的所有密码在其中一览无余,留在下载目录或云盘里等于白送。Windows 上删除记得清回收站,macOS 上如果用了时间机器还要考虑本地快照,最稳妥是用 shred 之类的工具覆写一遍再删。第二,导入完成后,去浏览器设置里关掉「保存密码」并清空已保存的密码,避免两套数据并存、日后分不清哪边更新。第三,利用这个机会把弱密码逐个换成随机强密码——既然密码库已经能自动填充,「记得住」这个约束就彻底消失了,每个站点都该用独立的十六位随机串。

导出的 CSV 列结构不同的浏览器略有差异,导入时如果出现字段错位,通常是格式选错了,换一个格式重新导一次即可。Vaultwarden 支持导入 Bitwarden、Chrome、LastPass、1Password 等多种格式,绝大多数情况都能直接吃下,不必手工改表头。

常见问题排查

客户端登录后无法同步,日志报 WebSocket 错误。 九成是 Nginx 没转发 Upgrade 头,回头检查 /notifications/hub 那段配置,确认 proxy_set_header Upgrade 和 Connection "upgrade" 都写了,并且容器端口用的是 HTTP(内网)而不是 HTTPS(Vaultwarden 容器内默认不监听 TLS)。

注册时报「注册未开放」。 说明 SIGNUPS_ALLOWED 是 false。临时改成 true,重建容器,注册完再关掉。

附件上传失败或中断。 大概率是 client_max_body_size 太小,或者 CDN 层有独立的体积限制。如果套了 Cloudflare 免费版,注意单次上传有 100MB 的上限,超大附件会失败,这是 CDN 层的硬限制,只能改走不经过 CDN 的直连子域。

忘记主密码。 很遗憾,没有服务端重置按钮能救回数据,因为服务端根本没有你的明文。唯一的出路是用此前导出的加密备份配合恢复码,或者登录已经登录着的客户端把密码库整体导出为未加密的 JSON 再重新导入。所以请务必把恢复码离线保存、定期导出。

小结

Vaultwarden 是个人站长自建服务里性价比最高的一个:占用小、维护简单、客户端体验和官方几乎一样,还能顺带练一遍 Docker + Nginx 反代 + HTTPS + 备份的完整运维链路。部署完之后你会发现自己再也不用记密码,随机生成的强密码想用就用,各个后台的安全性直接上一个台阶。真正要花心思的不是部署,而是上线后那三件小事:关掉公开注册、开启两步验证、把备份跑起来并定期验证能恢复。做到这三点,这套自建密码库就能安心长期用下去。

Last modification:October 7th, 2026 at 07:24 pm

Leave a Comment