在群晖 NAS 上部署 Vaultwarden 自建密码管理器,比云服务器或官方服务端更可控:数据存本地硬盘,FRP 按需穿透,云服务器只转发不存数据,容器仅绑回环并经 DSM nginx 反代。整套约 40MB 内存,一下午跑通;结论是个人与家庭自建更安全,服务关停也只是开关。
密码管理器是那种「平时完全想不起来,出事就是灭顶之灾」的东西。主流方案让你把全家的密码托付给云厂商的服务器——这没什么错,Bitwarden 云端免费版甚至足够好。但我最终还是选择在自己家里搭了一套,不是为了省钱,也不是为了折腾,而是因为密码库这种数据,容不得它存在于别人的服务器上。
我的做法是:把 Vaultwarden 部署在家里的群晖 NAS(Synology DS1621+)上,数据永远躺在自己的硬盘里;对外只留一个 FRPC 内网穿透,需要的时候用、不需要随手停掉。整套东西从零到能用,一个下午搭完,资源占用不到 40 MB 内存。这篇文章完整记录这个过程,包括踩过的坑——你照着做应该能一次跑通。
如果你也在折腾家里的服务,可以先看看这篇把 Nginx 访问日志做成可视化仪表盘,和本文用的是同一台 NAS。
一、先说结论:为什么不搭在云服务器上
网上搜「Bitwarden 自建」,出来的东西几乎清一色是「买台云服务器,装 Docker,公网 IP 直接访问」。方案本身能跑通,但有两个问题被系统性忽略了。
1. 你会不续费,然后数据就没了
云服务器有生命周期。轻量应用服务器有到期时间,活动机型有续费价,促销机型第二年可能直接翻倍。Bitwarden 的密码库不是「随便重新注册一下就行」的东西——里面是你所有账号的唯一凭据,服务停一天你就一天登不进去。更现实的情况是:某天服务商跑路、某个端口被封、或者你就是不想再续费了,此时数据在人家硬盘上,取回来的难度取决于人家心情。
NAS 不一样。它本来就在你家,因为你买它是为了存照片、存文件。它不会因为「续费到期」而停止工作——只要你还用电,它就一直在。你的数据生命周期和你的数据重要性是绑定的,不和某个第三方的商业决策绑定。
2. 密码库值得放在你信任的物理位置
Bitwarden 官方服务器流量是加密的(客户端到服务端全程 TLS,服务端到服务端也加密),这点没问题。但服务端仍然是第三方在运维,它能看到你的元数据:什么时候登录、从哪个 IP 登录、有几个条目、条目标题在客户端加密前可能经过缓存。这不是「Bitwarden 不可信」,而是你把它放到了一个你无法审计的位置。
二、选型:Vaultwarden,不是官方 bitwarden/server
Bitwarden 有两条自建路线,差别大到不需要纠结。两条路线用的都是同一个官方客户端——浏览器扩展、桌面端、手机 App、CLI 全都通用,区别只在服务端。
| 对比项 | 官方 bitwarden/server | Vaultwarden(本文采用) |
|---|---|---|
| 技术栈 | C# / .NET | Rust 单二进制 |
| 容器数量 | 约 12 个 + 独立数据库 | 1 个 |
| 最低内存 | 4 GB(推荐 8 GB) | 128 MB(实测空闲约 40 MB) |
| 磁盘要求 | 30 GB+ | 5~ 10 GB |
| 数据库 | MSSQL / PostgreSQL | SQLite(可换 PG) |
| 高级功能 | 需付费 license | 全部免费 |
| TOTP / Send / 组织 | 付费版才有 | 免费解锁 |
| 厂商支持 / 审计 | 有,含 SOC 2 | 社区维护,无正式审计 |
| 缺失的企业功能 | — | SSO(SAML/OIDC)、SCIM、Duo、部分管理策略 |
| 冷启动时间 | 约 38 秒 | 约 1.2 秒 |
结论很清楚:个人 / 家庭 / 小团队自建,Vaultwarden 是唯一理性选择。官方那套的 footprint(占地)只有在「企业要 SSO、要审计报告、要厂商担责」时才值得。资源占用差 30 倍,而两者用的是同一套官方客户端。
Vaultwarden 用 Rust 重实现了 Bitwarden 的服务端 API,所以官方客户端完全兼容。迁移到官方云端或反过来,只需改登录页的服务器地址,密码条目不需要重新分发。
三、最终架构:NAS + DSM 反代 + FRP 按需开
先看最终长什么样。这是我实际跑通的链路:
手机 / 电脑(官方 Bitwarden 客户端)
│ https://ssl.997wan.com
▼
FRPC 内网穿透(按需启动,不需要就停)
▼
云服务器 frps(只做转发,不存任何数据)
▼
爱快 iKuai 端口转发 443 → 192.168.11.224
▼
NAS:DSM 自带 nginx (443)
│ http://127.0.0.1:8080
▼
vaultwarden 容器 (80)
▼
SQLite /volume1/vaultwarden/vw-data/
关键设计有三条:
1. 云服务器只做转发,一个字节的数据都不经过
FRP 的定位是「打通内外网的网络通路」,不是「把服务搬到云上」。密码库在跑完整条链路时,数据是从你的 NAS 直连出去的,云服务器只是让客户端能连到 NAS 的443 端口。这意味着云服务器被停止服务、欠费、跑路,都只影响你「在外网能不能访问」,不影响数据本身。数据在 NAS 上,NAS 一直在。
2. 容器只绑回环地址,公网流量一律经 nginx
compose 里端口只写 127.0.0.1:8080:80,不写 8080:80。这样即使内网有人扫到 NAS 的 8080,也无法绕过反代直接访问 API。所有对外流量必须经过 nginx(也就是能强制 HTTPS、能加安全头的那一层)。
3.放弃 Caddy,改用 DSM 自带 nginx
我最初的设计是像大多数人一样:Caddy + Let’s Encrypt 自动签证书。探测 NAS 之后发现一个硬约束——NAS 的 80 和 443 已经被 DSM 自带的 nginx 占满了(进程 /usr/bin/nginx -c /etc/nginx/nginx.conf.run)。Caddy 根本抢不到这两个端口。
所以改为复用 DSM 的反向代理能力:少一个容器,TLS 也直接走 DSM 现有的证书体系。这是「先探测再设计」省下来的事——如果不先看 NAS 上有什么就动手,多半要返工。
四、部署过程实录
| 项目 | 实测值 |
|---|---|
| NAS 型号 | Synology DS1621+ |
| DSM 版本 | 7.4.1 |
| 实际 LAN IP | 192.168.11.224(不是 .220,那是另一个共享地址) |
| 内存 / 磁盘 | 62 GiB 总量,可用 59 GiB;volume1 剩余 22 TB |
| 容器运行时 | Container Manager,Docker 24.0.2,Compose v2.20.1 |
| Vaultwarden 版本 | 1.37.4 |
| 资源占用 | 空闲约 40 MB 内存 |
| 冷启动 | 约 1.2 秒 |
1. 建目录并写好 docker-compose.yml
先建工作目录,再放 compose 文件。整个项目放在 /volume1/vaultwarden:
mkdir -p /volume1/vaultwarden
cd /volume1/vaultwarden
然后是核心的 docker-compose.yml。我把它完整贴出来,每一行后面都解释了为什么这么写:
services:
vaultwarden:
image: vaultwarden/server:latest
container_name: vaultwarden
restart: unless-stopped
env_file: .env
environment:
DOMAIN: ${DOMAIN}
ADMIN_TOKEN: ${ADMIN_TOKEN}
SIGNUPS_ALLOWED: ${SIGNUPS_ALLOWED}
INVITATIONS_ALLOWED: "false"
ORG_CREATION_ALLOWED: "false"
WEBSOCKET_ENABLED: "true"
LOG_LEVEL: info
# 必须保持 false:true 表示数据只在内存,容器一重启全部密码消失
I_REALLY_WANT_VOLATILE_STORAGE: "false"
SEND_ENABLED: "true"
MAX_ATTACHMENT_SIZE: 10
# 前面有 nginx 时必填,否则审计日志里全是反代 IP
TRUSTED_PROXIES: 127.0.0.1/32,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
volumes:
- ./vw-data:/data
# 只绑回环。NAS 的 80/443 已被 DSM 自带 nginx 占用,
# 绝不能在这里抢,公网流量统一走 nginx 反代。
ports:
- "127.0.0.1:8080:80"
healthcheck:
test: ["CMD", "/healthcheck.sh"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
三个必须解释的参数:
I_REALLY_WANT_VOLATILE_STORAGE必须保持false。这个变量名字起得很直白——设为true意味着数据只在内存里,容器一重启所有密码都没了。日志里那句警告就是它。WEBSOCKET_ENABLED必须true。实时同步(多端改密码立刻互相同步)走的是 WebSocket,关掉的话能用但各端数据会不一致。TRUSTED_PROXIES前面有 nginx 反代时必填。否则审计日志里记录的全是你自己 nginx 的 IP,而不是真实客户端 IP,日志价值归零。
2. 生成 .env(ADMIN_TOKEN 用随机数)
DSM 上可以用一条命令生成随机 ADMIN_TOKEN 并写入 .env:
ADMIN_TOKEN=$(head -c 48 /dev/urandom | base64 | tr -d '\n=/+' | head -c 48)
cat > /volume1/vaultwarden/.env <<EOF
DOMAIN=https://ssl.997wan.com
ADMIN_TOKEN=$ADMIN_TOKEN
SIGNUPS_ALLOWED=true
INVITATIONS_ALLOWED=false
ORG_CREATION_ALLOWED=false
EOF
chmod 600 /volume1/vaultwarden/.env
SIGNUPS_ALLOWED=true 是为了让你能创建第一个账号。建完账号第一件事就是改成 false,否则任何人访问你的域名都能注册一个新账号进你的库。这一步别忘。
3. 拉镜像并启动
DK=/var/packages/ContainerManager/target/usr/bin/docker
cd /volume1/vaultwarden
$DK compose pull
$DK compose up -d
/var/packages/ContainerManager/target/usr/bin/docker。原因是 sudo 会重置 PATH,直接敲 docker 会得到 command not found。这个坑我在部署过程中撞了两次。启动后验证:
$DK compose ps
$DK exec vaultwarden /healthcheck.sh
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/alive
/alive 返回 200 且容器状态是 healthy,就算部署成功。真实启动日志长这样:
| Starting Vaultwarden |
| Version 1.37.4 |
[NOTICE] You are using a plain text `ADMIN_TOKEN` which is insecure.
Please generate a secure Argon2 PHC string by using `vaultwarden hash`.
[2026-10-07 11:08:05.556][vaultwarden::auth][INFO] Private key 'data/rsa_key.pem' created correctly
[2026-10-07 11:08:06.353][start][INFO] Rocket has launched from http://0.0.0.0:80
看到 Rocket has launched 和 rsa_key.pem 生成成功,就成了。
4. DSM 反向代理(这一步最容易漏)
进「控制面板 → 登录门户 → 高级 → 反向代理」,新建规则:
| 项目 | 值 |
|---|---|
| 名称 | vaultwarden |
| 来源 | HTTPS / ssl.997wan.com / 443 |
| 目标 | HTTP / 127.0.0.1 / 8080 |
| 自定义标头 | X-Forwarded-Proto: httpsX-Forwarded-For: $remote_addrX-Forwarded-Host: $host |
| WebSocket | 必须勾选 |
/notifications/hub 这个 WebSocket 端点,漏了它表现为「能登录、能存密码,但手机和电脑上的数据不一致」,很容易被误判成数据丢了。五、踩坑记录:DSM 上特有的 11 个问题
Synology DSM 不是标准 Linux,官方 docker-compose 教程在这里基本全线失效。以下每一条都是我实际卡住过的。
| # | 现象 | 原因与解法 |
|---|---|---|
| 1 | SSH 账号密码怎么都登录不上 | 可登录的账号是 wen(不是 admin),属 administrators 组;密码认证失败时改走 DSM 任务计划(root 执行脚本) |
| 2 | sudo: docker: command not found |
sudo 重置 PATH。必须 sudo sh -c '...' 或用绝对路径 |
| 3 | root 也删不掉文件 | volume1 挂载带 synoacl。覆盖写要用 install -m 755 /tmp/x /target/x;SMB 映射盘(Z:)同样不能删/覆盖已有文件 |
| 4 | 没有 crontab 命令 |
DSM 没有 crontab 客户端。定时任务直接写 /etc/crontab(DSM 7.4 用 synoschedtask,但加 root 行同样生效) |
| 5 | /var/log/ 写不进去 |
没有写权限,自建日志目录 |
| 6 | SFTP 连不上 | SFTP subsystem 未启用。改用 hex 管道:printf '%s' <hex> | xxd -r -p > /tmp/f |
| 7 | 连续密码失败后 SSH 突然全断 | 触发 NAS 自动封锁:22 端口开着但握手秒断、5001 也返回 HTTP 000。只能等自动解封,切勿暴力重试 |
| 8 | compose 报变量解析失败 | compose 要求 .env 存在才能解析变量 → 必须先写 .env 再 pull |
| 9 | Bind mount failed: '/volume1/vaultwarden/vw-data' does not exist |
数据目录不存在。先 mkdir 再 up |
| 10 | 备份脚本找不到 sqlite3 | DSM 默认没装 sqlite3 → 备份脚本要走停容器冷备份分支 |
| 11 | 打包备份失败 | 密钥文件名是 rsa_key.pem(不是目录 rsa_keys/),按官方教程写路径会漏掉密钥 |
六、备份:这是整套方案的命门
自建密码库的所有风险,最后都会收敛到一件事:能不能恢复。服务器格式化、勒索加密、手贱删错目录——任一发生,没备份就永久丢失,而且因为密码库本身是加密的,丢了就是彻底丢了。
备份必须包含密钥,这是关键
Vaultwarden 的数据目录里有两样东西:
db.sqlite3—— 密码库数据库rsa_key.pem—— 服务器 RSA 密钥
rsa_key.pem 丢了,库里所有已加密数据永远解不开——包括别人已经保存进去的条目。这个文件必须和数据库一起备份。只备数据库等于没备。我的备份脚本
DSM 没有 sqlite3,所以走「停容器冷备份」分支。大约 15 秒不可用,但绝对不会产生半写状态——对一个密码库来说,保守比可用重要:
#!/bin/bash
# cron: 17 4 * * *(每天 04:17,避开整点高峰)
set -euo pipefail
BASE_DIR="/volume1/vaultwarden"
DATA_DIR="$BASE_DIR/vw-data"
BACKUP_DIR="$BASE_DIR/backups"
KEEP_DAYS=30
mkdir -p "$BACKUP_DIR"
[ -d "$DATA_DIR" ] || { echo "[ERR] 数据目录不存在: $DATA_DIR"; exit 1; }
STAMP="$(date +%F)"
# ---------- SQLite 在线热备份(有 sqlite3 时走这里)----------
if command -v sqlite3 >/dev/null 2>&1; then
sqlite3 "$DATA_DIR/db.sqlite3" ".backup '$BACKUP_DIR/db-$STAMP.sqlite3'"
echo "[ok] 数据库热备份完成"
else
# DSM 默认没装 sqlite3,退化为停容器冷备份
echo "[warn] 无 sqlite3,改用停容器冷备份"
DOCKER=/var/packages/ContainerManager/target/usr/bin/docker
cd "$BASE_DIR"
$DOCKER compose stop vaultwarden
sleep 3
cp "$DATA_DIR/db.sqlite3" "$BACKUP_DIR/db-$STAMP.sqlite3"
$DOCKER compose start vaultwarden
sleep 10
fi
# ---------- 打包:数据库 + RSA 密钥(缺密钥等于没备)----------
TARGET="$BACKUP_DIR/vw-backup-$STAMP.tar.gz"
tar czf "$TARGET" -C "$DATA_DIR" db.sqlite3 rsa_key.pem rsa_key.pub
# ---------- 清理过期备份 ----------
find "$BACKUP_DIR" -name 'vw-backup-*.tar.gz' -mtime +$KEEP_DAYS -delete
echo "[ok] $STAMP 备份完成: $TARGET"
挂进 DSM 的 /etc/crontab:
17 4 * * * root /volume1/vaultwarden/backup.sh >> /volume1/vaultwarden/logs/backup.log 2>&1
用 04:17 而不是 04:00 是有意的——整点是各类定时任务高峰,错开几分钟避免 IO 打架。保留 30 天。
再补一层异地
同一块盘上的备份不算备份。DSM 自带的 Hyper Backup 可以把 /volume1/vaultwarden 整个目录纳入,另一个存储池或另一台机器都行。这是最后一道保险。
七、外网访问:FRP 按需开,不需要就停
回到开头那个问题。云服务器方案的隐含前提是「服务必须一直在线」,而我的做法把这件事变成可选项。
FRP 的拓扑很简单:
# NAS 上跑 frpc(客户端)
[common]
server_addr = 你的云服务器IP
server_port = 7000
token = 一段双方约定的字符串
[ssl-vaultwarden]
type = tcp
local_ip = 127.0.0.1
local_port = 443
remote_port = 443
云服务器上跑 frps,NAS 上跑 frpc,一条隧道把家里 NAS 的 443 端口接到云服务器的公网 IP 上。所有访问流量都是这个转发链路。
「随时可以停掉」意味着什么
| 场景 | 操作 | 结果 |
|---|---|---|
| 平时在家里 / 公司内网 | 什么都不用做 | 客户端直连 https://ssl.997wan.com,走本地解析,不经过云服务器,速度快 |
| 出门要用 | NAS 上启动 frpc | 外网可访问 |
| 不想暴露了 / 云服务器到期 | systemctl stop frpc 或直接关云服务器 |
外网立刻不可达,数据仍在 NAS 上,一个字节都没动 |
| 哪天想续费了 | 重启云服务器 + frpc | 重新可用 |
这就是把服务端放在自己家里的核心价值:「服务是否在线」从一个持续的成本和风险,变成了一次可以随时做的开关操作。云服务器到期那天你不需要做任何抢救动作,因为数据从来就没在那里。
如果你连公网 IP 都不想暴露,还有两条路:Tailscale(组网方案,走 WireGuard 打洞,不经过任何第三方中转)或者 Cloudflare Tunnel(走 Cloudflare 边缘)。但两种都比 FRPC 麻烦一些——FRPC 的优势是标准化配置、不依赖第三方服务、随时可停。
八、日常运维命令速查
DK=/var/packages/ContainerManager/target/usr/bin/docker
$DK ps --filter name=vaultwarden # 状态
$DK logs --tail 100 vaultwarden # 日志
cd /volume1/vaultwarden && $DK compose restart # 重启
cd /volume1/vaultwarden && $DK compose up -d # 启动
cd /volume1/vaultwarden && $DK compose pull && $DK compose up -d # 升级
bash /volume1/vaultwarden/backup.sh # 手动备份
tail -20 /volume1/vaultwarden/logs/backup.log # 备份日志
ls -la /volume1/vaultwarden/backups/ # 备份产物
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/alive # 健康检查
升级流程是固定的:先 pull 拉新镜像,再 up -d 重建。Vaultwarden 的数据库结构在升级时自动迁移,不需要手动处理。升级前确保当天备份已经生成——这是唯一需要记住的纪律。
九、安全清单
部署完逐条核对:
- 容器只绑
127.0.0.1(不绑 0.0.0.0) .env权限 600- ADMIN_TOKEN 用48 位随机字符生成,不是手写
I_REALLY_WANT_VOLATILE_STORAGE=false(数据落盘而非内存)SIGNUPS_ALLOWED=false(建完账号立刻改)- DSM 反向代理勾选 WebSocket
- 备份包含
rsa_key.pem - Hyper Backup 纳入
vw-data(异地备份) - 保管库开启两步验证
一个可以后面再做的改进
启动日志会警告 You are using a plain text ADMIN_TOKEN which is insecure.。改成 Argon2 哈希即可:
cd /volume1/vaultwarden
/var/packages/ContainerManager/target/usr/bin/docker exec vaultwarden \
vaultwarden hash --plaintext -
# 把输出的整行 $argon2id$... 替换 .env 里的 ADMIN_TOKEN,再重启容器
ADMIN_TOKEN 只管 /admin 后台,而 .env 权限已经锁 600,风险有限。可以先把上面 9 条做完再说这个。
十、故障排查
| 现象 | 原因 | 处理 |
|---|---|---|
| 客户端提示「服务器不安全」 | 没有 HTTPS | 走完 DNS + 反代;客户端拒绝明文 HTTP |
| 能登录但多端不同步 | 反代没开 WebSocket | DSM 反代勾选 WebSocket 支持 |
| 打开白屏 | 用 IP + http 访问 | 必须用 https://ssl.997wan.com。web 保管库要求安全上下文,用裸 IP + http 会直接拒绝 |
| 容器起不来,Bind mount failed | vw-data 不存在 |
mkdir -p /volume1/vaultwarden/vw-data |
sudo: xxx: command not found |
sudo 重置 PATH | 用绝对路径或 sudo sh -c '...' |
| 收不到验证邮件 | 没配 SMTP | .env 加 SMTP_HOST / SMTP_PORT / SMTP_FROM / SMTP_USERNAME / SMTP_PASSWORD |
| 忘记主密码 | — | 无法找回。这就是必须开两步验证 + 离线导出加密备份的原因 |
十一、值得不值得
最后说点实在的。把成本摊开看:
| Vaultwarden 自建 | Bitwarden 云端免费版 | 1Password | |
|---|---|---|---|
| 成本 | 免费(已有 NAS) | 免费 | ~$36/年 |
| 数据位置 | 自己的 NAS | Bitwarden 云 | 1Password 云 |
| TOTP / Send / 组织 | 全部免费 | 付费版才有 | 付费 |
| 维护成本 | 自己(每天 1 次 cron) | 免维护 | 免维护 |
| 厂商审计 | 无 | 有第三方审计 | 有 |
| 服务可控性 | 随时可停可重建 | 依赖厂商 | 依赖厂商 |
| 客户端 | 官方 Bitwarden App | 官方 App | 1Password App |
说实话:Bitwarden 云端免费版对多数人已经够用,有审计、有厂商维护、不用操心备份和升级。自建换来的是数据主权 + 免费解锁 TOTP/Send/组织功能 + 服务可停可重建,代价是你得自己保证备份和升级。这个交换是否划算,取决于你有多在意「密码存在别人的服务器上」。
适合谁
推荐:已经有 NAS 或愿意买一台、有基本命令行能力、能接受自己负责备份的人。
不推荐:没有 NAS 只能租云服务器、不想折腾任何运维、或者单纯因为「听说自建更安全」就临时起意——那你用云端免费版,它真的够用。
如果你手上的服务比较多、都在同一台机器上,别忘了这几篇可能对你有用:DoH / DoT 延迟实测对比、Nginx 访问日志可视化仪表盘、WordPress 大图与灯箱放大。
- 文章名称:Bitwarden 自建教程:密码库放自己 NAS 上
- 文章永久网址:https://www.997wan.com/bitwarden-self-host-on-nas/
- 本网站的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,请联系站长 997笔记 进行删除处理。
- 本站一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
- 本站一律禁止以任何方式发布或转载任何违法的相关信息,访客发现请向站长举报
- 本站资源大多存储在云盘,如发现链接失效,请联系我们我们会第一时间更新。
- 如无特别声明本文即为原创文章仅代表个人观点,版权归《997笔记》所有,欢迎转载,转载请保留原文链接。