在VPS上使用Docker Compose自建Vaultwarden

在VPS上部署加固的Vaultwarden密码管理器。涵盖Docker Compose只读容器、fail2ban、用于双因素认证的SMTP、备份与恢复以及紧急访问功能。

Vaultwarden存储着你的所有密码。草率的部署比不部署更糟糕。本教程在VPS上搭建Vaultwarden,使用加固的Docker容器、fail2ban、用于双因素认证的SMTP,以及备份和恢复流程。

我们假设Docker Engine和反向代理(Nginx或Caddy)已在你的服务器上运行。如果还没有,请先参阅VPS上的Docker生产环境:会出什么问题以及如何解决Nginx反向代理配置教程

Vaultwarden是什么?与Bitwarden有何不同?

Vaultwarden是用Rust重新实现的Bitwarden服务器API。它以单个Docker容器运行,默认使用SQLite,空闲时内存占用约50 MB,与所有Bitwarden官方客户端(浏览器扩展、移动应用、桌面客户端、CLI)完全兼容。Bitwarden官方自托管部署需要11个以上容器和至少4 GB内存。

Vaultwarden Bitwarden自托管
语言 Rust C#(.NET)
容器数量 1 11+
内存(空闲) ~50 MB ~2-4 GB
数据库 SQLite(默认)、MySQL、PostgreSQL MSSQL(必需)
Bitwarden客户端支持 完全兼容 完全兼容
SSO(OpenID Connect) 自1.35.0版本起 仅企业版
官方支持 社区 Bitwarden Inc.
许可证 AGPL-3.0 专有(服务端为SSPL)

Vaultwarden曾用名bitwarden_rs。如果你在旧教程中看到这个名称,指的是同一个项目。

如何在VPS上用Docker Compose部署Vaultwarden?

创建Vaultwarden数据和配置的目录结构。所有持久化数据都存放在一个目录中,后续将对其进行备份。

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

生成argon2哈希的管理员令牌。永远不要以明文存储管理员令牌。

docker run --rm -it vaultwarden/server:1.35.4 /vaultwarden hash

该命令会要求输入两次密码,然后输出一个argon2id PHC字符串:

Generate an Argon2id PHC string using the 'bitwarden' preset:

Password:
Confirm Password:
ADMIN_TOKEN='$argon2id$v=19$m=65540,t=3,p=4$S2mMOA8VnTtIOb3J8Gj9Jw$9cZ0YIKmGxfWEqSMKFMbORkBiW7hMGCls3SXAFXSIVE'

保存该字符串,后面在环境文件中需要用到。

创建权限受限的密钥文件:

touch /opt/vaultwarden/.env
chmod 600 /opt/vaultwarden/.env

编辑/opt/vaultwarden/.env,填入你的值:

DOMAIN=https://vault.example.com
ADMIN_TOKEN='$argon2id$v=19$m=65540,t=3,p=4$your-hash-here'

SIGNUPS_ALLOWED=true
INVITATIONS_ALLOWED=true
EMERGENCY_ACCESS_ALLOWED=true
ORG_CREATION_USERS=all

SHOW_PASSWORD_HINT=false
IP_HEADER=X-Real-IP
LOG_FILE=/data/vaultwarden.log
LOG_LEVEL=warn

SMTP_HOST=smtp.example.com
SMTP_FROM=vault@example.com
SMTP_PORT=587
SMTP_SECURITY=starttls
SMTP_USERNAME=vault@example.com
SMTP_PASSWORD=your-smtp-password

SHOW_PASSWORD_HINT=false防止向知道用户名的人泄露密码提示。IP_HEADER=X-Real-IP告诉Vaultwarden从反向代理头中读取客户端IP,这是fail2ban封禁正确地址所必需的。

现在创建Compose文件。

如何加固Vaultwarden Docker容器?

以下Compose文件锁定镜像版本、移除所有Linux capabilities、启用只读根文件系统、阻止权限提升并设置内存限制。这些是本教程的默认配置,不是可选附加项。

创建/opt/vaultwarden/compose.yaml

services:
  vaultwarden:
    image: vaultwarden/server:1.35.4
    container_name: vaultwarden
    restart: unless-stopped
    env_file: .env
    user: "1000:1000"
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    read_only: true
    tmpfs:
      - /tmp
    volumes:
      - ./data:/data
    ports:
      - "127.0.0.1:8080:80"
    deploy:
      resources:
        limits:
          memory: 512M
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:80/alive"]
      interval: 30s
      timeout: 5s
      retries: 3
  • **user: "1000:1000"**以非root用户身份在容器内运行进程。你需要将数据目录的所有权设置为对应值:chown -R 1000:1000 /opt/vaultwarden/data
  • **cap_drop: ALL**移除所有Linux capabilities。Vaultwarden不需要任何capabilities,因为它在容器内监听80端口(在Docker映射的上下文中属于非特权端口)。
  • **read_only: true**阻止容器向除显式挂载的卷和tmpfs以外的任何位置写入。如果Vaultwarden被攻破,攻击者无法写入容器文件系统。
  • **ports: "127.0.0.1:8080:80"**仅绑定到localhost。反向代理处理外部流量。永远不要将Vaultwarden直接暴露到互联网。
  • **deploy.resources.limits.memory: 512M**防止失控进程耗尽服务器所有内存。Vaultwarden空闲时使用约50 MB,负载下约100-150 MB。512 MB留有充足余量。

修正数据目录所有权并启动容器:

chown -R 1000:1000 /opt/vaultwarden/data
docker compose up -d
[+] Running 1/1Container vaultwarden  Started

检查容器状态:

docker compose ps
NAME          IMAGE                       COMMAND       SERVICE       CREATED          STATUS                    PORTS
vaultwarden   vaultwarden/server:1.35.4   "/start.sh"   vaultwarden   Up 30 seconds (healthy)   127.0.0.1:8080->80/tcp

(healthy)状态表示健康检查已通过。查看日志检查启动错误:

docker compose logs --tail 20

日志显示Vaultwarden在容器内80端口启动,无错误。

反向代理配置

反向代理将HTTPS流量转发到127.0.0.1:8080。以下是一个最小的Nginx server块:

server {
    listen 443 ssl;
    http2 on;
    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;
    server_tokens off;

    location / {
        proxy_pass http://127.0.0.1:8080;
        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;
    }
}

client_max_body_size 525M允许上传文件附件至Bitwarden的限制上限。server_tokens off隐藏Nginx版本信息,因为版本泄露有助于攻击者针对已知漏洞发起攻击。

自Vaultwarden 1.29.0起,WebSocket流量与HTTP共用同一端口。不再需要单独的/notifications/hub代理路径。如果你在旧教程中看到WEBSOCKET_ENABLED,请忽略。该变量在1.29.0中被弃用,在1.31.0中被移除。

Nginx配置详情请参阅Nginx反向代理配置教程在Debian 12和Ubuntu 24.04上为Nginx配置Let's Encrypt SSL/TLS证书

如何为Vaultwarden配置SMTP邮件?

Vaultwarden需要SMTP来发送邮件验证码、2FA设置邮件、组织邀请和紧急访问通知。没有SMTP,邀请注册、邮件2FA和紧急访问等功能无法使用。

主要环境变量为SMTP_HOSTSMTP_PORTSMTP_SECURITYSMTP_USERNAMESMTP_PASSWORDSMTP_FROM。对大多数服务商使用SMTP_PORT=587配合SMTP_SECURITY=starttls,或使用SMTP_PORT=465配合SMTP_SECURITY=force_tls实现隐式TLS。

这些变量已包含在上面的.env文件中。启动容器后,通过以下方式测试邮件发送:登录https://vault.example.com的Web保险库,创建账户,进入Settings > Security > Two-step Login,启用邮件2FA。如果收到验证码,说明SMTP配置正确。

如果邮件发送失败,检查日志:

docker compose logs | grep -i smtp

常见问题:

  • 认证失败:检查SMTP_USERNAMESMTP_PASSWORD。密码中的特殊字符可能需要在.env文件中使用单引号。
  • 587端口连接被拒:部分VPS服务商默认封锁25和587出站端口。联系你的服务商确认,或尝试465端口配合force_tls
  • 证书错误:SMTP服务器的TLS证书必须有效。自签名证书需要SMTP_ACCEPT_INVALID_CERTS=true(不建议在生产环境使用)。
变量 用途 示例
SMTP_HOST 邮件服务器主机名 smtp.example.com
SMTP_PORT 连接端口 587
SMTP_SECURITY TLS方式 starttlsforce_tls
SMTP_FROM 发件人地址 vault@example.com
SMTP_USERNAME 认证用户名 vault@example.com
SMTP_PASSWORD 认证密码 (存储在.env中,chmod 600
SMTP_AUTH_MECHANISM 认证类型(可选) Login

如何保护Vaultwarden管理面板?

位于/admin的管理面板可用于管理用户、查看配置和修改设置。它由之前生成的ADMIN_TOKEN保护。argon2哈希意味着原始令牌永远不会以明文存储在.env文件中。即使有人读取了文件,得到的也只是哈希值而非密码。

完成初始设置(创建账户、配置SMTP、禁用注册)后,有两个选择:

**选项1:完全禁用管理面板。**从.env中删除或注释掉ADMIN_TOKEN并重启:

docker compose down && docker compose up -d

未设置ADMIN_TOKEN时,/admin端点返回404。这是单用户部署最安全的选择。

**选项2:通过反向代理限制访问。**保留管理面板但阻止外部访问:

location /admin {
    allow 127.0.0.1;
    deny all;

    proxy_pass http://127.0.0.1:8080;
    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;
}

需要时通过SSH隧道访问管理面板:

ssh -L 8888:127.0.0.1:443 user@your-vps

然后在浏览器中打开https://127.0.0.1:8888/admin

禁用公开注册

账户创建完成后,禁用开放注册。编辑/opt/vaultwarden/.env

SIGNUPS_ALLOWED=false

重启容器:

cd /opt/vaultwarden && docker compose down && docker compose up -d

如果INVITATIONS_ALLOWED=true,新用户仍可通过组织邀请加入。这样你可以添加团队成员而不必向所有人开放注册。

如何在Docker中为Vaultwarden设置fail2ban?

Fail2ban监控Vaultwarden的日志文件,检测失败的登录尝试并封禁违规IP。由于Vaultwarden运行在Docker中,封禁规则必须针对iptables的DOCKER-USER链。标准INPUT链规则对Docker路由的流量不起作用。

Vaultwarden容器将日志写入/opt/vaultwarden/data/vaultwarden.log(从容器内的/data/vaultwarden.log映射),因为我们在环境变量中设置了LOG_FILE=/data/vaultwarden.log

fail2ban安装详细指南请参阅在Linux VPS上安装和配置Fail2Ban

登录过滤器

创建/etc/fail2ban/filter.d/vaultwarden.local

[INCLUDES]
before = common.conf

[Definition]
failregex = ^.*?Username or password is incorrect\. Try again\. IP: <ADDR>\. Username:.*$
ignoreregex =

管理面板过滤器

创建/etc/fail2ban/filter.d/vaultwarden-admin.local

[INCLUDES]
before = common.conf

[Definition]
failregex = ^.*?Invalid admin token\. IP: <ADDR>.*$
ignoreregex =

Jail配置

创建/etc/fail2ban/jail.d/vaultwarden.local

[vaultwarden]
enabled = true
port = http,https
filter = vaultwarden
action = iptables-allports[name=vaultwarden, chain=DOCKER-USER]
logpath = /opt/vaultwarden/data/vaultwarden.log
maxretry = 3
bantime = 14400
findtime = 14400
backend = pyinotify

[vaultwarden-admin]
enabled = true
port = http,https
filter = vaultwarden-admin
action = iptables-allports[name=vaultwarden-admin, chain=DOCKER-USER]
logpath = /opt/vaultwarden/data/vaultwarden.log
maxretry = 3
bantime = 14400
findtime = 14400
backend = pyinotify
参数 原因
chain DOCKER-USER Docker绕过INPUT链。规则必须放在DOCKER-USER中才能影响容器流量。
maxretry 3 三次失败触发封禁。合法用户很少失败超过两次。
bantime 14400 四小时封禁。足以阻止暴力破解,又不会永久锁定输错密码的合法用户。
findtime 14400 在四小时窗口内计算失败次数。
backend pyinotify 基于文件的日志监控。默认的systemd后端无法读取Docker容器日志文件。

重启fail2ban并检查jail:

systemctl restart fail2ban
fail2ban-client status vaultwarden
Status for the jail: vaultwarden
|- Filter
|  |- Currently failed: 0
|  |- Total failed:     0
|  `- File list:        /opt/vaultwarden/data/vaultwarden.log
`- Actions
   |- Currently banned: 0
   |- Total banned:     0
   `- Banned IP list:

对照实际日志格式测试正则表达式:

fail2ban-regex /opt/vaultwarden/data/vaultwarden.log /etc/fail2ban/filter.d/vaultwarden.local

如果还没有失败的登录记录,输出会显示0 matched。这是正常的。通过Web保险库触发一次错误登录,然后重新运行以确认正则匹配。

如何将浏览器扩展和移动应用连接到Vaultwarden?

所有Bitwarden官方客户端都可以与Vaultwarden配合使用。唯一的改动是将服务器URL指向你的地址而非bitwarden.com

浏览器扩展(Chrome、Firefox、Safari):

  1. 从浏览器的扩展商店安装Bitwarden扩展
  2. 在登录界面点击齿轮图标(或新版本中的"Self-hosted")
  3. Server URL设为https://vault.example.com
  4. 保存并用你的凭据登录

移动应用(iOS、Android):

  1. 从App Store或Google Play安装Bitwarden应用
  2. 登录前在登录界面点击齿轮图标
  3. Server URL设为https://vault.example.com
  4. 保存并登录

桌面应用:

  1. 登录前点击齿轮图标
  2. Server URL设为https://vault.example.com

CLI:

bw config server https://vault.example.com
bw login

所有客户端通过你的Vaultwarden实例同步。保险库数据永远不会经过Bitwarden的服务器。

如何在Vaultwarden中设置紧急访问?

紧急访问允许受信任的联系人在你无法使用时访问或接管你的保险库。它需要配置SMTP,因为Vaultwarden在此过程中会发送通知邮件。

确认.env中已启用该功能:

EMERGENCY_ACCESS_ALLOWED=true

设置受信任联系人:

  1. https://vault.example.com登录Web保险库
  2. 进入Settings > Emergency Access
  3. 点击Add emergency contact
  4. 输入受信任人员的邮箱(该人必须在你的Vaultwarden实例上有账户)
  5. 选择访问级别:
    • View:联系人可以查看你的保险库项目(只读)
    • Takeover:联系人可以重置你的主密码并完全接管
  6. 设置等待时间(1到90天)。这是联系人请求访问到实际获得访问之间的延迟。你会收到邮件通知,并可在此期间拒绝请求。
  7. 点击Save

受信任联系人会收到邀请邮件。接受后,他们会显示为待处理的紧急联系人。等待时间相当于一个安全机制:如果你在配置的天数内没有响应,访问权限将自动授予。

对于个人保险库,7天等待时间配合View访问是合理的。对于团队共享基础设施,建议设置更短的等待时间。

如何备份和恢复Vaultwarden实例?

Vaultwarden将所有内容存储在/data目录中:SQLite数据库(db.sqlite3)、文件附件、图标缓存和配置。正确的备份需要一致地捕获所有这些内容。

SQLite数据库必须使用sqlite3 .backup备份,而不是直接复制文件。复制正在使用的SQLite文件时如果发生写入操作,可能产生损坏的备份。.backup命令创建一致的快照。

备份内容 位置 方法 频率
SQLite数据库 /opt/vaultwarden/data/db.sqlite3 sqlite3 .backup 每天
附件 /opt/vaultwarden/data/attachments/ rsynccp -a 每天
图标缓存 /opt/vaultwarden/data/icon_cache/ 跳过(自动重新生成) -
环境文件 /opt/vaultwarden/.env cp 修改时
Compose文件 /opt/vaultwarden/compose.yaml cp 修改时

备份脚本

对于自动化备份,脚本使用age配合收件人密钥文件。这避免了交互式密码提示,使脚本可以从cron运行。

生成密钥对(仅一次):

apt install age
age-keygen -o /opt/vaultwarden/backup-key.txt
chmod 600 /opt/vaultwarden/backup-key.txt

命令会将公钥输出到stdout。将其保存到收件人文件:

age-keygen -y /opt/vaultwarden/backup-key.txt > /opt/vaultwarden/backup-key.pub

backup-key.txt(私钥)的副本存放在服务器之外。解密备份时需要它。如果丢失此密钥,你的加密备份将无法恢复。

创建/opt/vaultwarden/backup.sh

#!/bin/bash
set -euo pipefail

BACKUP_DIR="/opt/vaultwarden/backups"
DATA_DIR="/opt/vaultwarden/data"
DATE=$(date +%Y-%m-%d_%H%M)
BACKUP_FILE="${BACKUP_DIR}/vaultwarden-${DATE}.tar"
RECIPIENT="/opt/vaultwarden/backup-key.pub"

mkdir -p "${BACKUP_DIR}"

# Back up SQLite database consistently
sqlite3 "${DATA_DIR}/db.sqlite3" ".backup '${BACKUP_DIR}/db-${DATE}.sqlite3'"

# Package database + attachments + config
tar cf "${BACKUP_FILE}" \
  -C "${BACKUP_DIR}" "db-${DATE}.sqlite3" \
  -C "${DATA_DIR}" attachments/ \
  -C /opt/vaultwarden .env compose.yaml 2>/dev/null || true

# Encrypt with age recipient key (non-interactive)
age -R "${RECIPIENT}" -o "${BACKUP_FILE}.age" "${BACKUP_FILE}"

# Clean up unencrypted files
rm -f "${BACKUP_FILE}" "${BACKUP_DIR}/db-${DATE}.sqlite3"

# Remove backups older than 30 days
find "${BACKUP_DIR}" -name "*.age" -mtime +30 -delete

echo "Backup complete: ${BACKUP_FILE}.age"
chmod 700 /opt/vaultwarden/backup.sh

设置每日自动备份的cron任务:

echo "0 3 * * * root /opt/vaultwarden/backup.sh >> /var/log/vaultwarden-backup.log 2>&1" > /etc/cron.d/vaultwarden-backup
chmod 644 /etc/cron.d/vaultwarden-backup

将加密备份推送到远程服务器或对象存储实现异地备份:

rsync -az /opt/vaultwarden/backups/*.age backup-user@remote-server:/backups/vaultwarden/

恢复流程

从未恢复过的备份是不可信的备份。完整流程如下:

# Stop the container
cd /opt/vaultwarden && docker compose down

# Decrypt the backup
age -d -i /opt/vaultwarden/backup-key.txt -o /tmp/vaultwarden-restore.tar /opt/vaultwarden/backups/vaultwarden-2026-03-20_0300.tar.age

# Extract
mkdir -p /tmp/vaultwarden-restore
tar xf /tmp/vaultwarden-restore.tar -C /tmp/vaultwarden-restore

# Replace the database (delete WAL files first)
rm -f /opt/vaultwarden/data/db.sqlite3-wal /opt/vaultwarden/data/db.sqlite3-shm
cp /tmp/vaultwarden-restore/db-2026-03-20_0300.sqlite3 /opt/vaultwarden/data/db.sqlite3
chown 1000:1000 /opt/vaultwarden/data/db.sqlite3

# Restore attachments
rsync -a /tmp/vaultwarden-restore/attachments/ /opt/vaultwarden/data/attachments/
chown -R 1000:1000 /opt/vaultwarden/data/attachments/

# Start the container
docker compose up -d

# Clean up
rm -rf /tmp/vaultwarden-restore /tmp/vaultwarden-restore.tar

恢复前必须删除WAL(Write-Ahead Log)和SHM文件。这些文件属于旧的数据库状态。使用恢复的数据库但保留过期的WAL文件启动Vaultwarden会导致数据损坏。

恢复后登录Web保险库,确认你的条目都在。

如何安全地更新Vaultwarden?

将镜像锁定到特定版本标签(我们使用的是1.35.4)。在生产环境中永远不要使用:latest,因为你无法追踪变更内容,也无法可靠地回滚。

更新步骤:

cd /opt/vaultwarden

# Back up first
./backup.sh

# Pull the new version
docker compose pull

# Recreate the container
docker compose up -d

拉取前先编辑compose.yaml,将镜像标签改为新版本。查看发行说明了解破坏性变更。

docker compose logs --tail 30

如果更新出现问题,从备份恢复:

# Revert compose.yaml to the old version tag
docker compose down
# Follow the restore procedure above
docker compose up -d

遇到问题了?

容器立即退出:

docker compose logs

查找权限错误。user: 1000:1000指令要求/opt/vaultwarden/data具有匹配的所有权。运行chown -R 1000:1000 /opt/vaultwarden/data后重试。

无法访问Web保险库:

检查反向代理是否正在转发到127.0.0.1:8080

curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8080/alive

收到200响应表示Vaultwarden正在运行,问题出在反向代理配置上。

Fail2ban未封禁:

fail2ban-regex /opt/vaultwarden/data/vaultwarden.log /etc/fail2ban/filter.d/vaultwarden.local

如果正则匹配零行,检查.env中是否设置了LOG_FILE=/data/vaultwarden.log,以及日志文件是否存在于/opt/vaultwarden/data/vaultwarden.log

邮件发送失败:

docker compose logs | grep -i "smtp\|mail\|email"

验证你的VPS是否允许SMTP端口的出站连接:

nc -zv smtp.example.com 587

数据库锁定错误:

这可能发生在Vaultwarden运行时复制数据库文件的情况下。备份时始终使用sqlite3 .backup,恢复前始终停止容器。

日志位置:

# Application logs
docker compose logs -f

# Log file (if LOG_FILE is set)
tail -f /opt/vaultwarden/data/vaultwarden.log

# Fail2ban logs
journalctl -u fail2ban -f

关于本教程未涵盖的Docker容器安全内容,请参阅Docker安全加固:VPS上的Rootless模式、Seccomp和AppArmor。关于所有Docker服务的备份策略,请参阅在VPS上备份和恢复Docker卷


版权所有 2026 Virtua.Cloud。保留所有权利。 本内容为 Virtua.Cloud 团队原创作品。 未经书面许可,禁止复制、转载或再分发。

准备好亲自尝试了吗?

几秒内部署您自己的服务器。支持 Linux、Windows 或 FreeBSD。

查看 VPS 方案