vaultwarden的升级和部署修改为1.37版本
This commit is contained in:
@@ -1,27 +0,0 @@
|
||||
<!-- rtk-instructions v2 -->
|
||||
# RTK — Token-Optimized CLI
|
||||
|
||||
**rtk** is a CLI proxy that filters and compresses command outputs, saving 60-90% tokens.
|
||||
|
||||
## Rule
|
||||
|
||||
Always prefix shell commands with `rtk`:
|
||||
|
||||
```bash
|
||||
# Instead of: Use:
|
||||
git status rtk git status
|
||||
git log -10 rtk git log -10
|
||||
cargo test rtk cargo test
|
||||
docker ps rtk docker ps
|
||||
kubectl get pods rtk kubectl pods
|
||||
```
|
||||
|
||||
## Meta commands (use directly)
|
||||
|
||||
```bash
|
||||
rtk gain # Token savings dashboard
|
||||
rtk gain --history # Per-command savings history
|
||||
rtk discover # Find missed rtk opportunities
|
||||
rtk proxy <cmd> # Run raw (no filtering) but track usage
|
||||
```
|
||||
<!-- /rtk-instructions -->
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "rtk hook copilot",
|
||||
"cwd": ".",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,11 @@ SENDS_ALLOWED=true
|
||||
# ===== 镜像配置 =====
|
||||
|
||||
# Vaultwarden 镜像
|
||||
VAULTWARDEN_IMAGE=vaultwarden/server:latest
|
||||
# 留空或填 latest 时,deploy.sh 会自动查询 GitHub 最新 release 并把具体版本号写回这里。
|
||||
# 不要长期使用 latest:该标签只在 docker compose pull 时才重新解析,容器不重建就会
|
||||
# 一直跑旧版本,而且从配置上看不出实际运行的是哪一版。
|
||||
# 后续升级请用 upgrade.sh,它会自动改写此处的版本号。
|
||||
VAULTWARDEN_IMAGE=
|
||||
|
||||
# ===== 目录与端口 =====
|
||||
|
||||
|
||||
+112
-16
@@ -16,7 +16,7 @@ Bitwarden 兼容的自托管密码管理器,轻量、安全、功能完整。
|
||||
|
||||
| 组件 | 版本 | 说明 |
|
||||
|------|------|------|
|
||||
| Vaultwarden | latest | Bitwarden 兼容服务端(Rust 实现) |
|
||||
| Vaultwarden | 固定版本号 | Bitwarden 兼容服务端(Rust 实现),由 `upgrade.sh` 升级 |
|
||||
| SQLite | 内置 | 轻量数据库,无需额外部署 |
|
||||
| Nginx | 系统包 | 反向代理 + HTTPS(Bitwarden 客户端必须 HTTPS) |
|
||||
| Docker | 最新版 | 容器运行环境 |
|
||||
@@ -35,6 +35,7 @@ vaultwarden/
|
||||
├── docker-compose.yml # 容器编排
|
||||
├── .env.example # 配置模板
|
||||
├── deploy.sh # 一键部署脚本
|
||||
├── upgrade.sh # 安全升级脚本(带备份、校验、自动回滚)
|
||||
├── backup.sh # 备份脚本
|
||||
├── uninstall.sh # 完全卸载脚本
|
||||
├── nginx/
|
||||
@@ -97,9 +98,12 @@ bash deploy.sh
|
||||
cd /opt/vaultwarden
|
||||
# 编辑 .env,将 SIGNUPS_ALLOWED 改为 false
|
||||
vi .env
|
||||
docker compose restart
|
||||
docker compose up -d # 注意:不是 restart
|
||||
```
|
||||
|
||||
> **不要用 `docker compose restart`**。环境变量是在容器**创建**时注入的,`restart` 只是重启
|
||||
> 原容器、不会重建,改了 `.env` 也不会生效。必须用 `up -d` 让 compose 检测到配置变更并重建容器。
|
||||
|
||||
### 第五步:安装客户端
|
||||
|
||||
1. 下载 Bitwarden 客户端:https://bitwarden.com/download/
|
||||
@@ -224,23 +228,65 @@ docker compose up -d
|
||||
|
||||
### 升级
|
||||
|
||||
使用 `upgrade.sh` 升级,**不要**手动 `docker compose pull && up -d`(见下方「为什么不要手动升级」)。
|
||||
|
||||
```bash
|
||||
cd /opt/vaultwarden
|
||||
|
||||
# 1. 备份数据
|
||||
bash backup.sh
|
||||
|
||||
# 2. 拉取新镜像
|
||||
docker compose pull
|
||||
|
||||
# 3. 重启
|
||||
docker compose up -d
|
||||
|
||||
# 4. 检查运行状态
|
||||
docker compose ps
|
||||
docker compose logs --tail 20
|
||||
bash upgrade.sh --check # 先看看会发生什么,不做任何改动
|
||||
bash upgrade.sh # 正式升级到 GitHub 最新 release
|
||||
```
|
||||
|
||||
脚本执行流程:
|
||||
|
||||
| 步骤 | 动作 | 失败时 |
|
||||
|------|------|--------|
|
||||
| 1 | 预检:root、依赖命令、数据目录、磁盘空间 | 直接退出,未动服务 |
|
||||
| 2 | 拉取新镜像 | 直接退出,**服务零影响**,可安全重试 |
|
||||
| 3 | 把当前镜像打上 `pre-upgrade-<时间戳>` 标签作为回滚锚点 | — |
|
||||
| 4 | 停止容器 | 自动拉起原服务 |
|
||||
| 5 | WAL checkpoint + `integrity_check` + 冷备份整个数据目录 | 自动拉起原服务 |
|
||||
| 6 | 切换镜像并 `--force-recreate` 重建容器 | 自动回滚 |
|
||||
| 7 | 校验容器实际镜像 == 目标镜像 | 自动回滚 |
|
||||
| 8 | 校验 `/alive`、新旧 prelogin 路由、Nginx 反代 | 自动回滚 |
|
||||
| 9 | 比对升级前后各表行数,任一表减少即判定数据丢失 | 自动回滚 |
|
||||
|
||||
常用选项:
|
||||
|
||||
```bash
|
||||
bash upgrade.sh --check # 只检查,零改动
|
||||
bash upgrade.sh --version 1.37.0 # 升级到指定版本
|
||||
bash upgrade.sh --yes # 跳过交互确认(自动化场景)
|
||||
bash upgrade.sh --rollback # 回滚到上次升级前的状态
|
||||
```
|
||||
|
||||
升级完成后请**登录 Web 端确认数据无误**,再清理回滚镜像:
|
||||
|
||||
```bash
|
||||
docker image rm vaultwarden/server:pre-upgrade-<时间戳>
|
||||
```
|
||||
|
||||
### 回滚
|
||||
|
||||
```bash
|
||||
cd /opt/vaultwarden
|
||||
bash upgrade.sh --rollback
|
||||
```
|
||||
|
||||
回滚会读取 `.upgrade-state` 里记录的备份和镜像锚点,恢复数据目录并切回旧镜像。
|
||||
|
||||
> **注意**:回滚会丢弃升级后新增/修改的密码条目。
|
||||
> 回滚**不会删除**升级后的数据,而是把它改名保留为 `<数据目录>.failed-<时间戳>`,确认无需后再自行删除。
|
||||
|
||||
### 为什么不要手动升级
|
||||
|
||||
手动 `docker compose pull && docker compose up -d` 有两个坑,都会导致「看起来升级了,其实没有」:
|
||||
|
||||
1. **停止状态的容器不会因镜像变更而重建**。如果先 `stop` 再 `up -d`,compose 只是把原容器重新 `start`,仍然跑旧镜像。必须 `--force-recreate`。
|
||||
2. **环境变量优先级高于 `.env` 文件**。如果当前 shell 里已经 `export` 过 `VAULTWARDEN_IMAGE`(比如脚本里 `source .env` 过),改 `.env` 文件无效,compose 依然用旧值。
|
||||
|
||||
`upgrade.sh` 处理了这两点,并在启动后强制校验「容器实际运行的镜像 ID == 目标镜像 ID」,不匹配直接回滚。
|
||||
|
||||
### 停止 / 启动
|
||||
|
||||
```bash
|
||||
@@ -316,10 +362,24 @@ rm -rf /opt/vaultwarden
|
||||
注册好所有需要的账号后:
|
||||
|
||||
```bash
|
||||
# .env 中设置
|
||||
SIGNUPS_ALLOWED=false
|
||||
cd /opt/vaultwarden
|
||||
sed -i 's/^SIGNUPS_ALLOWED=.*/SIGNUPS_ALLOWED=false/' .env
|
||||
docker compose up -d # 必须 up -d,restart 不生效
|
||||
```
|
||||
|
||||
验证是否真的关闭了(应返回 `Registration not allowed or user already exists`):
|
||||
|
||||
```bash
|
||||
curl -s -X POST http://127.0.0.1:8080/identity/accounts/register/send-verification-email \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"email":"probe@example.invalid","name":"probe"}'
|
||||
```
|
||||
|
||||
> **不要用 `/api/config` 里的 `disableUserRegistration` 判断**。该字段来自
|
||||
> `is_signup_disabled()`,含义是「是否隐藏 UI 上的注册链接」,而非「注册是否被拒绝」。
|
||||
> 当 `INVITATIONS_ALLOWED=true` 且未配置 SMTP 时,即使 `SIGNUPS_ALLOWED=false`
|
||||
> 它也会显示为 `false`(因为管理员邀请的用户仍需走注册流程)。真正的拦截在注册接口里。
|
||||
|
||||
### 2. 限制管理面板访问
|
||||
|
||||
在 `nginx/vaultwarden.conf` 中取消注释 `/admin` 的 IP 限制部分,仅允许你的 IP 访问。
|
||||
@@ -347,6 +407,42 @@ docker compose ps
|
||||
curl http://127.0.0.1:8080/alive
|
||||
```
|
||||
|
||||
### 客户端登录报错,控制台 404 /identity/accounts/prelogin/password
|
||||
|
||||
**症状**:已登录的会话正常,但重新登录失败,浏览器控制台显示:
|
||||
|
||||
```
|
||||
POST https://vault.example.com/identity/accounts/prelogin/password 404 (Not Found)
|
||||
```
|
||||
|
||||
**原因**:服务端版本过旧。Bitwarden 客户端从 v2026.4.0 起改用 `/identity/accounts/prelogin/password`
|
||||
这个新路由,而 Vaultwarden 在 **1.36.0** 才实现它。客户端会自动更新,服务端不会。
|
||||
|
||||
**确认**:
|
||||
|
||||
```bash
|
||||
# 看服务端实到底跑的是哪个版本
|
||||
curl -s https://vault.example.com/api/config | grep -o '"gitHash":"[^"]*"'
|
||||
|
||||
# 直接探测新旧两个路由
|
||||
curl -s -o /dev/null -w '%{http_code}\n' -X POST -H 'Content-Type: application/json' \
|
||||
-d '{"email":"x@example.com"}' https://vault.example.com/identity/accounts/prelogin/password
|
||||
```
|
||||
|
||||
旧路由返回 200、新路由返回 404,即可确诊。
|
||||
|
||||
**修复**:升级到 1.36.0 以上。
|
||||
|
||||
```bash
|
||||
cd /opt/vaultwarden && bash upgrade.sh
|
||||
```
|
||||
|
||||
如果暂时无法升级,可在 Nginx 的 443 server 块内加一条重写作为临时过渡:
|
||||
|
||||
```nginx
|
||||
rewrite ^/identity/accounts/prelogin/password/?$ /identity/accounts/prelogin last;
|
||||
```
|
||||
|
||||
### 502 Bad Gateway
|
||||
|
||||
```bash
|
||||
|
||||
+84
-3
@@ -24,11 +24,72 @@ source "$BASE_DIR/setup.sh"
|
||||
# Vaultwarden 专用函数
|
||||
# =============================================================
|
||||
|
||||
GITHUB_REPO="dani-garcia/vaultwarden"
|
||||
|
||||
# 生成随机密码
|
||||
generate_password() {
|
||||
openssl rand -base64 32 | tr -d '/+=' | head -c 32
|
||||
}
|
||||
|
||||
# 查询 GitHub 上的最新 release 版本号(不含前缀 v),失败返回非零
|
||||
resolve_latest_version() {
|
||||
local ver
|
||||
ver=$(curl -sf --max-time 20 \
|
||||
"https://api.github.com/repos/${GITHUB_REPO}/releases/latest" \
|
||||
| grep -o '"tag_name"[[:space:]]*:[[:space:]]*"[^"]*"' \
|
||||
| head -1 | sed 's/.*"\([^"]*\)"$/\1/' | sed 's/^v//') || true
|
||||
[ -n "$ver" ] || return 1
|
||||
echo "$ver"
|
||||
}
|
||||
|
||||
# 把镜像固定到具体版本号。
|
||||
# 必须同时更新 .env 和已导出的环境变量:init_env 里 `set -a; source .env` 已把旧值
|
||||
# 导出到本进程,而 docker compose 对环境变量的优先级高于 .env 文件,只改文件不生效。
|
||||
pin_image_version() {
|
||||
local ref="$1"
|
||||
if grep -q '^VAULTWARDEN_IMAGE=' .env; then
|
||||
sed -i "s|^VAULTWARDEN_IMAGE=.*|VAULTWARDEN_IMAGE=${ref}|" .env
|
||||
else
|
||||
printf '\nVAULTWARDEN_IMAGE=%s\n' "$ref" >> .env
|
||||
fi
|
||||
export VAULTWARDEN_IMAGE="$ref"
|
||||
}
|
||||
|
||||
# 确定本次部署使用的镜像版本。
|
||||
# 不使用 latest 标签的原因:它只在 docker compose pull 时才重新解析,容器不重建就会
|
||||
# 一直跑旧版本,且从配置上看不出实际运行的是哪一版。
|
||||
setup_image_version() {
|
||||
local current="${VAULTWARDEN_IMAGE:-}"
|
||||
|
||||
# 已固定到具体版本 → 尊重现有配置,升级交给 upgrade.sh
|
||||
if [ -n "$current" ] && [[ "$current" != *":latest" ]]; then
|
||||
log " 镜像: ${current}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# 已有实例却仍在用 latest:此时固定版本等同于一次升级,
|
||||
# 而升级必须走 upgrade.sh(带冷备份、完整性校验、自动回滚),这里不擅自处理。
|
||||
if docker compose ps -a -q vaultwarden 2>/dev/null | grep -q .; then
|
||||
warn "检测到已存在的实例,且 .env 仍在使用 latest 标签"
|
||||
warn "latest 不会自动更新,也看不出实际运行版本,建议固定到具体版本号:"
|
||||
warn " bash upgrade.sh # 安全升级到最新版,并自动写回版本号"
|
||||
warn "本次部署不改动镜像配置"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log "正在查询 Vaultwarden 最新版本..."
|
||||
local ver
|
||||
if ! ver="$(resolve_latest_version)"; then
|
||||
warn "无法从 GitHub 获取最新版本号(网络问题?),本次回退使用 latest 标签"
|
||||
warn "部署完成后建议手动固定: VAULTWARDEN_IMAGE=vaultwarden/server:<版本号>"
|
||||
[ -n "$current" ] || pin_image_version "vaultwarden/server:latest"
|
||||
return 0
|
||||
fi
|
||||
|
||||
pin_image_version "vaultwarden/server:${ver}"
|
||||
log " 镜像: vaultwarden/server:${ver}(当前最新 release,已写入 .env)"
|
||||
}
|
||||
|
||||
init_env() {
|
||||
step "初始化 Vaultwarden 配置"
|
||||
|
||||
@@ -83,6 +144,8 @@ init_env() {
|
||||
log " 域名: ${VAULTWARDEN_DOMAIN}"
|
||||
log " 邮箱: ${CERTBOT_EMAIL}"
|
||||
log " 注册: ${SIGNUPS_ALLOWED:-true}"
|
||||
|
||||
setup_image_version
|
||||
}
|
||||
|
||||
create_dirs() {
|
||||
@@ -95,21 +158,39 @@ create_dirs() {
|
||||
log "备份目录: $backup_dir"
|
||||
}
|
||||
|
||||
# 确认容器真的跑在配置指定的镜像上,避免「配置改了但容器没重建」这类静默失败
|
||||
verify_running_version() {
|
||||
local cid actual expect="${VAULTWARDEN_IMAGE:-}"
|
||||
cid=$(docker compose ps -a -q vaultwarden 2>/dev/null | head -1)
|
||||
[ -n "$cid" ] || return 0
|
||||
|
||||
actual=$(docker inspect "$cid" --format '{{.Config.Image}}' 2>/dev/null || true)
|
||||
if [ -n "$expect" ] && [ "$actual" != "$expect" ]; then
|
||||
warn "容器实际镜像 (${actual}) 与配置 (${expect}) 不一致"
|
||||
warn "请检查: docker compose up -d --force-recreate"
|
||||
return 0
|
||||
fi
|
||||
log " 运行镜像: ${actual}"
|
||||
}
|
||||
|
||||
start_services() {
|
||||
step "启动 Vaultwarden 服务"
|
||||
|
||||
log "正在拉取镜像..."
|
||||
docker compose pull
|
||||
|
||||
# --force-recreate:容器处于 stopped 状态时,up -d 只会把原容器重新 start,
|
||||
# 不会因镜像或环境变量变更而重建,导致「配置改了但没生效」。
|
||||
log "正在启动容器..."
|
||||
docker compose up -d
|
||||
docker compose up -d --force-recreate
|
||||
|
||||
local port="${VAULTWARDEN_PORT:-8080}"
|
||||
log "等待 Vaultwarden 就绪..."
|
||||
local max_wait=30
|
||||
for i in $(seq 1 "$max_wait"); do
|
||||
for _ in $(seq 1 "$max_wait"); do
|
||||
if curl -sf "http://127.0.0.1:${port}/alive" &> /dev/null; then
|
||||
log "Vaultwarden 启动成功!"
|
||||
verify_running_version
|
||||
return
|
||||
fi
|
||||
sleep 2
|
||||
@@ -144,7 +225,7 @@ show_info() {
|
||||
if [[ "${SIGNUPS_ALLOWED:-true}" == "true" ]]; then
|
||||
echo -e "${GREEN}║${NC} ${YELLOW}⚠ 注册功能已开启,注册完账号后建议关闭:${NC}"
|
||||
echo -e "${GREEN}║${NC} ${YELLOW} 修改 .env 中 SIGNUPS_ALLOWED=false${NC}"
|
||||
echo -e "${GREEN}║${NC} ${YELLOW} 然后 docker compose restart${NC}"
|
||||
echo -e "${GREEN}║${NC} ${YELLOW} 然后 docker compose up -d(不是 restart)${NC}"
|
||||
fi
|
||||
echo -e "${GREEN}║${NC}"
|
||||
echo -e "${GREEN}╚══════════════════════════════════════════════════════════╝${NC}"
|
||||
|
||||
@@ -0,0 +1,700 @@
|
||||
#!/usr/bin/env bash
|
||||
# -E 让 ERR trap 能在函数内部生效,保证不可逆区间的异常一定触发回滚
|
||||
set -Eeuo pipefail
|
||||
|
||||
# ============================================
|
||||
# Vaultwarden 安全升级脚本
|
||||
#
|
||||
# 设计原则:
|
||||
# 1. 先拉镜像再动服务 —— 网络失败时服务零影响
|
||||
# 2. 冷备份(停容器 + WAL checkpoint)—— 保证快照一致
|
||||
# 3. 升级前后比对数据指纹 —— 数据异常自动回滚
|
||||
# 4. 回滚不删数据 —— 旧数据目录改名保留,不 rm
|
||||
#
|
||||
# 用法:
|
||||
# bash upgrade.sh 升级到 GitHub 最新 release
|
||||
# bash upgrade.sh --check 只检查,不做任何改动
|
||||
# bash upgrade.sh --version 1.37.0
|
||||
# bash upgrade.sh --rollback 回滚到上次升级前的状态
|
||||
# bash upgrade.sh --yes 跳过交互确认(用于自动化)
|
||||
# ============================================
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# ===== 输出样式 =====
|
||||
if [ -t 1 ]; then
|
||||
RED=$'\033[0;31m'; GREEN=$'\033[0;32m'; YELLOW=$'\033[1;33m'
|
||||
CYAN=$'\033[0;36m'; BOLD=$'\033[1m'; NC=$'\033[0m'
|
||||
else
|
||||
RED=''; GREEN=''; YELLOW=''; CYAN=''; BOLD=''; NC=''
|
||||
fi
|
||||
|
||||
log() { echo -e "${GREEN}[✓]${NC} $*"; }
|
||||
info() { echo -e "${CYAN}[i]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
|
||||
error(){ echo -e "${RED}[✗]${NC} $*" >&2; }
|
||||
step() { echo ""; echo -e "${BOLD}${CYAN}━━━ $* ━━━${NC}"; }
|
||||
|
||||
# ===== 全局状态 =====
|
||||
GITHUB_REPO="dani-garcia/vaultwarden"
|
||||
TIMESTAMP="$(date +%Y%m%d_%H%M%S)"
|
||||
STATE_FILE="$SCRIPT_DIR/.upgrade-state"
|
||||
MODE="upgrade" # upgrade | check | rollback
|
||||
TARGET_VERSION=""
|
||||
ASSUME_YES=0
|
||||
SAFETY_STAGE="none" # none → stopped → armed → done,决定异常退出时如何兜底
|
||||
BACKUP_PATH=""
|
||||
ROLLBACK_TAG=""
|
||||
OLD_IMAGE_REF=""
|
||||
COUNTS_BEFORE=""
|
||||
|
||||
# =============================================================
|
||||
# 参数解析
|
||||
# =============================================================
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Vaultwarden 安全升级脚本
|
||||
|
||||
用法: bash upgrade.sh [选项]
|
||||
|
||||
选项:
|
||||
-c, --check 只做预检和版本对比,不做任何改动
|
||||
-v, --version <ver> 指定目标版本(如 1.37.0),默认取 GitHub 最新 release
|
||||
-r, --rollback 回滚到上次升级前的状态(读取 .upgrade-state)
|
||||
-y, --yes 跳过交互确认
|
||||
-h, --help 显示本帮助
|
||||
|
||||
示例:
|
||||
bash upgrade.sh --check # 先看看会发生什么
|
||||
bash upgrade.sh # 正式升级
|
||||
bash upgrade.sh --rollback # 升级后发现问题,回滚
|
||||
EOF
|
||||
}
|
||||
|
||||
parse_args() {
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
-c|--check) MODE="check" ;;
|
||||
-r|--rollback) MODE="rollback" ;;
|
||||
-y|--yes) ASSUME_YES=1 ;;
|
||||
-v|--version)
|
||||
shift
|
||||
[ $# -gt 0 ] || { error "--version 需要一个参数"; exit 1; }
|
||||
TARGET_VERSION="${1#v}"
|
||||
;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) error "未知参数: $1"; echo ""; usage; exit 1 ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
}
|
||||
|
||||
confirm() {
|
||||
[ "$ASSUME_YES" -eq 1 ] && return 0
|
||||
local prompt="$1" answer
|
||||
echo ""
|
||||
read -r -p "${prompt} [y/N] " answer
|
||||
[[ "$answer" =~ ^[Yy]$ ]]
|
||||
}
|
||||
|
||||
# =============================================================
|
||||
# 环境加载与预检
|
||||
# =============================================================
|
||||
load_env() {
|
||||
[ -f .env ] || { error ".env 不存在,请先运行 deploy.sh"; exit 1; }
|
||||
sed -i 's/\r$//' .env
|
||||
set -a; source .env; set +a
|
||||
|
||||
DATA_DIR="${VAULTWARDEN_DATA_DIR:-/var/lib/vaultwarden}"
|
||||
BACKUP_BASE="${BACKUP_DIR:-/var/backups/vaultwarden}"
|
||||
PORT="${VAULTWARDEN_PORT:-8080}"
|
||||
DOMAIN="${VAULTWARDEN_DOMAIN:-}"
|
||||
CURRENT_IMAGE="${VAULTWARDEN_IMAGE:-vaultwarden/server:latest}"
|
||||
DB_FILE="$DATA_DIR/db.sqlite3"
|
||||
}
|
||||
|
||||
preflight() {
|
||||
step "环境预检"
|
||||
|
||||
[ "$(id -u)" -eq 0 ] || { error "需要 root 权限运行"; exit 1; }
|
||||
|
||||
local missing=0
|
||||
for cmd in docker curl tar; do
|
||||
command -v "$cmd" >/dev/null 2>&1 || { error "缺少命令: $cmd"; missing=1; }
|
||||
done
|
||||
docker compose version >/dev/null 2>&1 || { error "docker compose 不可用"; missing=1; }
|
||||
[ "$missing" -eq 0 ] || exit 1
|
||||
|
||||
[ -f docker-compose.yml ] || { error "docker-compose.yml 不存在"; exit 1; }
|
||||
[ -d "$DATA_DIR" ] || { error "数据目录不存在: $DATA_DIR"; exit 1; }
|
||||
[ -f "$DB_FILE" ] || { error "数据库不存在: $DB_FILE"; exit 1; }
|
||||
|
||||
# 磁盘空间:要求可用空间 ≥ 数据目录的 3 倍(备份 + 解压余量),至少 500MB
|
||||
mkdir -p "$BACKUP_BASE"
|
||||
local data_kb avail_kb need_kb
|
||||
data_kb=$(du -sk "$DATA_DIR" | cut -f1)
|
||||
avail_kb=$(df -Pk "$BACKUP_BASE" | awk 'NR==2{print $4}')
|
||||
[ -n "$avail_kb" ] || { error "无法检测 $BACKUP_BASE 的可用空间"; exit 1; }
|
||||
need_kb=$(( data_kb * 3 ))
|
||||
[ "$need_kb" -lt 512000 ] && need_kb=512000
|
||||
if [ "$avail_kb" -lt "$need_kb" ]; then
|
||||
error "磁盘空间不足: 可用 $((avail_kb/1024))MB,需要 $((need_kb/1024))MB"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "root 权限、依赖命令、数据目录、磁盘空间 均正常"
|
||||
info "数据目录 $DATA_DIR ($((data_kb/1024))MB),可用空间 $((avail_kb/1024))MB"
|
||||
}
|
||||
|
||||
ensure_sqlite3() {
|
||||
command -v sqlite3 >/dev/null 2>&1 && return 0
|
||||
warn "sqlite3 未安装,正在安装(用于 WAL checkpoint 和完整性校验)..."
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
DEBIAN_FRONTEND=noninteractive apt-get update -qq
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq sqlite3
|
||||
elif command -v yum >/dev/null 2>&1; then
|
||||
yum install -y -q sqlite
|
||||
else
|
||||
error "无法自动安装 sqlite3,请手动安装后重试"
|
||||
exit 1
|
||||
fi
|
||||
command -v sqlite3 >/dev/null 2>&1 || { error "sqlite3 安装失败"; exit 1; }
|
||||
log "sqlite3 已安装"
|
||||
}
|
||||
|
||||
# =============================================================
|
||||
# 版本解析
|
||||
# =============================================================
|
||||
resolve_target_version() {
|
||||
if [ -n "$TARGET_VERSION" ]; then
|
||||
info "使用指定版本: $TARGET_VERSION"
|
||||
return 0
|
||||
fi
|
||||
info "查询 GitHub 最新 release..."
|
||||
TARGET_VERSION=$(curl -sf --max-time 20 \
|
||||
"https://api.github.com/repos/${GITHUB_REPO}/releases/latest" \
|
||||
| grep -o '"tag_name"[[:space:]]*:[[:space:]]*"[^"]*"' \
|
||||
| head -1 | sed 's/.*"\([^"]*\)"$/\1/' | sed 's/^v//') || true
|
||||
|
||||
if [ -z "$TARGET_VERSION" ]; then
|
||||
error "无法从 GitHub 获取最新版本(网络问题?)"
|
||||
error "请手动指定: bash upgrade.sh --version 1.37.0"
|
||||
exit 1
|
||||
fi
|
||||
log "GitHub 最新 release: $TARGET_VERSION"
|
||||
}
|
||||
|
||||
# 从 compose 项目推导容器 ID(而非硬编码容器名),停止状态也能拿到
|
||||
compose_container_id() {
|
||||
docker compose ps -a -q vaultwarden 2>/dev/null | head -1 || true
|
||||
}
|
||||
|
||||
# 当前容器实际运行的镜像 ID(sha256:...),拿不到则返回空
|
||||
running_image_id() {
|
||||
local cid
|
||||
cid="$(compose_container_id)"
|
||||
[ -n "$cid" ] || return 0
|
||||
docker inspect "$cid" --format '{{.Image}}' 2>/dev/null || true
|
||||
}
|
||||
|
||||
get_running_githash() {
|
||||
curl -sf --max-time 10 "http://127.0.0.1:${PORT}/api/config" 2>/dev/null \
|
||||
| grep -o '"gitHash"[[:space:]]*:[[:space:]]*"[^"]*"' \
|
||||
| sed 's/.*"\([^"]*\)"$/\1/' || true
|
||||
}
|
||||
|
||||
# =============================================================
|
||||
# 数据指纹
|
||||
# =============================================================
|
||||
# 统计关键表行数,用于升级前后比对。表不存在时输出 "-"
|
||||
snapshot_counts() {
|
||||
local db="$1" t out=""
|
||||
for t in users ciphers folders sends attachments organizations collections; do
|
||||
local n
|
||||
n=$(sqlite3 "$db" "SELECT COUNT(*) FROM $t;" 2>/dev/null || echo "-")
|
||||
out+="${t}=${n} "
|
||||
done
|
||||
echo "${out% }"
|
||||
}
|
||||
|
||||
print_counts() {
|
||||
local label="$1" counts="$2" kv
|
||||
echo " ${label}:"
|
||||
for kv in $counts; do
|
||||
printf " %-16s %s\n" "${kv%%=*}" "${kv##*=}"
|
||||
done
|
||||
}
|
||||
|
||||
# =============================================================
|
||||
# 备份
|
||||
# =============================================================
|
||||
cold_backup() {
|
||||
step "冷备份(服务已停止,快照一致)"
|
||||
|
||||
mkdir -p "$BACKUP_BASE"
|
||||
|
||||
info "执行 WAL checkpoint,将未落盘数据合并进主库..."
|
||||
sqlite3 "$DB_FILE" "PRAGMA wal_checkpoint(TRUNCATE);" >/dev/null
|
||||
log "WAL 已合并"
|
||||
|
||||
info "校验数据库完整性..."
|
||||
local integrity
|
||||
integrity=$(sqlite3 "$DB_FILE" "PRAGMA integrity_check;" 2>&1 | head -1)
|
||||
if [ "$integrity" != "ok" ]; then
|
||||
error "数据库完整性检查未通过: $integrity"
|
||||
error "升级已中止,服务未改动。请先修复数据库。"
|
||||
docker compose up -d
|
||||
exit 1
|
||||
fi
|
||||
log "数据库完整性: ok"
|
||||
|
||||
COUNTS_BEFORE="$(snapshot_counts "$DB_FILE")"
|
||||
print_counts "升级前数据统计" "$COUNTS_BEFORE"
|
||||
|
||||
BACKUP_PATH="${BACKUP_BASE}/pre-upgrade-${TIMESTAMP}.tar.gz"
|
||||
info "打包整个数据目录 → $BACKUP_PATH"
|
||||
tar czf "$BACKUP_PATH" -C "$(dirname "$DATA_DIR")" "$(basename "$DATA_DIR")"
|
||||
|
||||
# 校验备份包。注意:这里先把清单收进变量再用 here-string 匹配,
|
||||
# 不能写成 `tar tzf ... | grep -q`:grep -q 命中即退出会让 tar 收到
|
||||
# SIGPIPE,在 pipefail 下整条管道被判为失败,导致好备份被误判成坏的。
|
||||
info "校验备份包..."
|
||||
local listing
|
||||
listing="$(tar tzf "$BACKUP_PATH")" || { error "备份包损坏,无法列出内容"; exit 1; }
|
||||
grep -q 'db\.sqlite3$' <<<"$listing" || { error "备份包中没有数据库文件"; exit 1; }
|
||||
grep -q 'rsa_key\.pem$' <<<"$listing" || warn "备份包中没有 rsa_key.pem"
|
||||
|
||||
# 同时备份 .env(含 ADMIN_TOKEN)
|
||||
cp .env "${BACKUP_BASE}/env-${TIMESTAMP}.bak"
|
||||
chmod 600 "${BACKUP_BASE}/env-${TIMESTAMP}.bak"
|
||||
|
||||
log "备份完成: $BACKUP_PATH ($(du -h "$BACKUP_PATH" | cut -f1))"
|
||||
}
|
||||
|
||||
# =============================================================
|
||||
# 镜像与容器
|
||||
# =============================================================
|
||||
tag_rollback_image() {
|
||||
step "锚定当前镜像(用于回滚)"
|
||||
|
||||
local image_id
|
||||
image_id="$(running_image_id)"
|
||||
if [ -z "$image_id" ]; then
|
||||
image_id=$(docker image inspect "$CURRENT_IMAGE" --format '{{.Id}}' 2>/dev/null || true)
|
||||
fi
|
||||
[ -n "$image_id" ] || { error "找不到当前运行的镜像,无法建立回滚点"; exit 1; }
|
||||
|
||||
ROLLBACK_TAG="vaultwarden/server:pre-upgrade-${TIMESTAMP}"
|
||||
docker tag "$image_id" "$ROLLBACK_TAG"
|
||||
OLD_IMAGE_REF="$CURRENT_IMAGE"
|
||||
|
||||
log "当前镜像已标记为 $ROLLBACK_TAG"
|
||||
info "镜像 ID: ${image_id#sha256:}"
|
||||
}
|
||||
|
||||
pull_target_image() {
|
||||
step "拉取目标镜像(此步骤不影响运行中的服务)"
|
||||
|
||||
local target="vaultwarden/server:${TARGET_VERSION}"
|
||||
info "docker pull $target ..."
|
||||
if ! docker pull "$target"; then
|
||||
error "镜像拉取失败,服务未做任何改动,可安全重试"
|
||||
exit 1
|
||||
fi
|
||||
log "镜像已就绪: $target"
|
||||
}
|
||||
|
||||
switch_image_in_env() {
|
||||
local new_ref="$1"
|
||||
cp .env ".env.bak-${TIMESTAMP}"
|
||||
chmod 600 ".env.bak-${TIMESTAMP}"
|
||||
if grep -q '^VAULTWARDEN_IMAGE=' .env; then
|
||||
sed -i "s|^VAULTWARDEN_IMAGE=.*|VAULTWARDEN_IMAGE=${new_ref}|" .env
|
||||
else
|
||||
printf '\nVAULTWARDEN_IMAGE=%s\n' "$new_ref" >> .env
|
||||
fi
|
||||
# 必须同步更新导出的变量:load_env 里 `set -a; source .env` 已经把旧值导出到
|
||||
# 本进程环境,而 docker compose 对环境变量的优先级高于 .env 文件 —— 只改文件
|
||||
# 的话 compose 仍会读到旧镜像,表现为「容器重建了但版本没变」。
|
||||
export VAULTWARDEN_IMAGE="$new_ref"
|
||||
info ".env 中 VAULTWARDEN_IMAGE → $new_ref"
|
||||
}
|
||||
|
||||
wait_healthy() {
|
||||
local max_wait="${1:-60}" i
|
||||
info "等待服务就绪(最多 ${max_wait}s)..."
|
||||
for i in $(seq 1 "$max_wait"); do
|
||||
if curl -sf --max-time 3 "http://127.0.0.1:${PORT}/alive" >/dev/null 2>&1; then
|
||||
log "服务已就绪(${i}s)"
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# =============================================================
|
||||
# 验证
|
||||
# =============================================================
|
||||
# 确认容器真的跑在目标镜像上,防止「改了配置但容器没重建」这类静默失败
|
||||
verify_running_image() {
|
||||
local expect="$1" cid actual_ref actual_id expect_id
|
||||
cid="$(compose_container_id)"
|
||||
[ -n "$cid" ] || { error "找不到容器"; return 1; }
|
||||
|
||||
actual_ref=$(docker inspect "$cid" --format '{{.Config.Image}}' 2>/dev/null || true)
|
||||
actual_id=$(docker inspect "$cid" --format '{{.Image}}' 2>/dev/null || true)
|
||||
expect_id=$(docker image inspect "$expect" --format '{{.Id}}' 2>/dev/null || true)
|
||||
|
||||
if [ "$actual_id" != "$expect_id" ]; then
|
||||
error "容器未运行在目标镜像上"
|
||||
error " 期望: $expect ($expect_id)"
|
||||
error " 实际: $actual_ref ($actual_id)"
|
||||
return 1
|
||||
fi
|
||||
log "容器镜像确认: $actual_ref"
|
||||
return 0
|
||||
}
|
||||
|
||||
# $1: strict(默认) —— 要求新版 prelogin 路由存在(升级后)
|
||||
# lenient —— 该路由 404 属正常(回滚到 1.36.0 之前的版本后)
|
||||
verify_endpoints() {
|
||||
local strict="${1:-strict}"
|
||||
step "接口验证"
|
||||
local failed=0
|
||||
|
||||
# 1. /alive
|
||||
if curl -sf --max-time 5 "http://127.0.0.1:${PORT}/alive" >/dev/null; then
|
||||
log "/alive 正常"
|
||||
else
|
||||
error "/alive 无响应"; failed=1
|
||||
fi
|
||||
|
||||
# 2. 新版 prelogin 路由(1.36.0+ 才有,是本次升级的核心目标)
|
||||
local code
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 8 \
|
||||
-X POST -H 'Content-Type: application/json' \
|
||||
-d '{"email":"healthcheck@example.invalid"}' \
|
||||
"http://127.0.0.1:${PORT}/identity/accounts/prelogin/password" || echo "000")
|
||||
if [ "$code" = "200" ]; then
|
||||
log "/identity/accounts/prelogin/password → 200(新版客户端可登录)"
|
||||
elif [ "$strict" = "lenient" ]; then
|
||||
info "/identity/accounts/prelogin/password → $code(该版本无此路由,回滚后属预期)"
|
||||
else
|
||||
error "/identity/accounts/prelogin/password → $code(期望 200)"; failed=1
|
||||
fi
|
||||
|
||||
# 3. 旧版 prelogin 路由(兼容老客户端)
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 8 \
|
||||
-X POST -H 'Content-Type: application/json' \
|
||||
-d '{"email":"healthcheck@example.invalid"}' \
|
||||
"http://127.0.0.1:${PORT}/identity/accounts/prelogin" || echo "000")
|
||||
if [ "$code" = "200" ]; then
|
||||
log "/identity/accounts/prelogin → 200(旧客户端兼容)"
|
||||
else
|
||||
warn "/identity/accounts/prelogin → $code"
|
||||
fi
|
||||
|
||||
# 4. 经 Nginx 的外部访问(失败只告警,属于代理层问题,不触发回滚)
|
||||
if [ -n "$DOMAIN" ]; then
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 \
|
||||
"https://${DOMAIN}/api/config" || echo "000")
|
||||
if [ "$code" = "200" ]; then
|
||||
log "https://${DOMAIN}/api/config → 200(Nginx 反代正常)"
|
||||
else
|
||||
warn "https://${DOMAIN}/api/config → $code(检查 Nginx/证书,不影响本次升级判定)"
|
||||
fi
|
||||
fi
|
||||
|
||||
return $failed
|
||||
}
|
||||
|
||||
verify_data_integrity() {
|
||||
step "数据完整性验证"
|
||||
|
||||
local integrity
|
||||
integrity=$(sqlite3 "$DB_FILE" "PRAGMA integrity_check;" 2>&1 | head -1)
|
||||
if [ "$integrity" != "ok" ]; then
|
||||
error "升级后数据库完整性检查失败: $integrity"
|
||||
return 1
|
||||
fi
|
||||
log "数据库完整性: ok"
|
||||
|
||||
local counts_after
|
||||
counts_after="$(snapshot_counts "$DB_FILE")"
|
||||
print_counts "升级后数据统计" "$counts_after"
|
||||
|
||||
# 逐表比对:行数只允许持平或增加,减少视为数据丢失
|
||||
local kv table before after lost=0
|
||||
for kv in $COUNTS_BEFORE; do
|
||||
table="${kv%%=*}"; before="${kv##*=}"
|
||||
after=$(echo "$counts_after" | tr ' ' '\n' | grep "^${table}=" | cut -d= -f2 || true)
|
||||
# 升级前该表就不存在 / 升级后读不到 → 跳过比对
|
||||
if [ "$before" = "-" ] || [ -z "$after" ] || [ "$after" = "-" ]; then
|
||||
continue
|
||||
fi
|
||||
if [ "$after" -lt "$before" ]; then
|
||||
error "表 $table 行数减少: $before → $after"
|
||||
lost=1
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$lost" -eq 1 ]; then
|
||||
error "检测到数据丢失!"
|
||||
return 1
|
||||
fi
|
||||
log "所有表行数无减少,数据完好"
|
||||
return 0
|
||||
}
|
||||
|
||||
# =============================================================
|
||||
# 回滚
|
||||
# =============================================================
|
||||
save_state() {
|
||||
cat > "$STATE_FILE" <<EOF
|
||||
# Vaultwarden 升级状态记录 —— 由 upgrade.sh 自动生成
|
||||
UPGRADE_TIMESTAMP=${TIMESTAMP}
|
||||
BACKUP_PATH=${BACKUP_PATH}
|
||||
ROLLBACK_TAG=${ROLLBACK_TAG}
|
||||
OLD_IMAGE_REF=${OLD_IMAGE_REF}
|
||||
NEW_IMAGE_REF=vaultwarden/server:${TARGET_VERSION}
|
||||
DATA_DIR=${DATA_DIR}
|
||||
ENV_BACKUP=${SCRIPT_DIR}/.env.bak-${TIMESTAMP}
|
||||
EOF
|
||||
chmod 600 "$STATE_FILE"
|
||||
}
|
||||
|
||||
# 恢复数据目录:旧目录改名保留,绝不 rm
|
||||
restore_data_dir() {
|
||||
local backup_tar="$1" ts="$2"
|
||||
local parent failed_dir
|
||||
parent="$(dirname "$DATA_DIR")"
|
||||
failed_dir="${DATA_DIR}.failed-${ts}"
|
||||
|
||||
if [ -d "$DATA_DIR" ]; then
|
||||
mv "$DATA_DIR" "$failed_dir"
|
||||
warn "升级后的数据目录已保留为: $failed_dir(未删除)"
|
||||
fi
|
||||
tar xzf "$backup_tar" -C "$parent"
|
||||
[ -f "$DB_FILE" ] || { error "恢复失败:$DB_FILE 不存在"; return 1; }
|
||||
log "数据目录已从备份恢复"
|
||||
}
|
||||
|
||||
# 服务已停、但备份尚未就绪时的紧急恢复:此阶段 .env 未改动,直接拉起原版本即可
|
||||
emergency_start_old() {
|
||||
step "紧急恢复"
|
||||
warn "升级在备份阶段中止,数据未被改动,正在拉起原服务..."
|
||||
docker compose up -d || true
|
||||
if wait_healthy 60; then
|
||||
log "原服务已恢复运行,本次升级未造成任何影响"
|
||||
else
|
||||
error "原服务恢复失败!请手动执行: cd $SCRIPT_DIR && docker compose up -d"
|
||||
error "查看日志: docker compose logs --tail 100"
|
||||
fi
|
||||
}
|
||||
|
||||
# 统一的退出兜底:确保任何异常路径都不会把服务丢在停机状态
|
||||
on_exit() {
|
||||
local code=$?
|
||||
trap - EXIT ERR
|
||||
[ "$code" -eq 0 ] && return 0
|
||||
case "$SAFETY_STAGE" in
|
||||
stopped) emergency_start_old ;;
|
||||
armed) do_rollback "脚本异常中止(退出码 $code)" ;;
|
||||
*) : ;; # 尚未动过服务,无需处理
|
||||
esac
|
||||
return 0
|
||||
}
|
||||
|
||||
do_rollback() {
|
||||
trap - ERR EXIT # 先解除,避免回滚过程自身出错导致递归
|
||||
SAFETY_STAGE="done"
|
||||
set +e
|
||||
local reason="$1"
|
||||
step "自动回滚"
|
||||
error "触发原因: $reason"
|
||||
|
||||
docker compose down || true
|
||||
restore_data_dir "$BACKUP_PATH" "$TIMESTAMP" || {
|
||||
error "自动恢复失败!请手动处理:"
|
||||
error " tar xzf $BACKUP_PATH -C $(dirname "$DATA_DIR")"
|
||||
exit 1
|
||||
}
|
||||
switch_image_in_env "$ROLLBACK_TAG"
|
||||
docker compose up -d --force-recreate
|
||||
|
||||
if wait_healthy 60; then
|
||||
log "回滚完成,已恢复到升级前状态"
|
||||
else
|
||||
error "回滚后服务仍未就绪,请检查: docker compose logs --tail 100"
|
||||
fi
|
||||
echo ""
|
||||
warn "升级失败已回滚。备份保留在: $BACKUP_PATH"
|
||||
exit 1
|
||||
}
|
||||
|
||||
manual_rollback() {
|
||||
step "手动回滚"
|
||||
|
||||
[ -f "$STATE_FILE" ] || { error "找不到升级记录 $STATE_FILE,无法自动回滚"; exit 1; }
|
||||
# shellcheck disable=SC1090
|
||||
source "$STATE_FILE"
|
||||
DB_FILE="$DATA_DIR/db.sqlite3" # 状态文件里的 DATA_DIR 可能与 .env 不同,重新推导
|
||||
|
||||
echo ""
|
||||
info "将回滚到:"
|
||||
info " 镜像: $ROLLBACK_TAG"
|
||||
info " 备份: $BACKUP_PATH"
|
||||
info " 时间: $UPGRADE_TIMESTAMP"
|
||||
echo ""
|
||||
|
||||
[ -f "$BACKUP_PATH" ] || { error "备份文件不存在: $BACKUP_PATH"; exit 1; }
|
||||
docker image inspect "$ROLLBACK_TAG" >/dev/null 2>&1 \
|
||||
|| { error "回滚镜像不存在: $ROLLBACK_TAG"; exit 1; }
|
||||
|
||||
warn "回滚会丢弃升级后产生的所有数据变更(新增/修改的密码条目)"
|
||||
confirm "确认回滚?" || { info "已取消"; exit 0; }
|
||||
|
||||
docker compose down || true
|
||||
restore_data_dir "$BACKUP_PATH" "$(date +%Y%m%d_%H%M%S)" || exit 1
|
||||
switch_image_in_env "$ROLLBACK_TAG"
|
||||
docker compose up -d --force-recreate
|
||||
|
||||
if wait_healthy 60; then
|
||||
log "回滚完成"
|
||||
verify_endpoints lenient || true
|
||||
else
|
||||
error "服务未就绪,请检查: docker compose logs --tail 100"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# =============================================================
|
||||
# 主流程
|
||||
# =============================================================
|
||||
show_plan() {
|
||||
local githash
|
||||
githash="$(get_running_githash)"
|
||||
|
||||
echo ""
|
||||
echo -e "${BOLD}══════════ 升级计划 ══════════${NC}"
|
||||
echo " 域名: ${DOMAIN:-未配置}"
|
||||
echo " 当前镜像: $CURRENT_IMAGE"
|
||||
echo " 当前 gitHash: ${githash:-未知(服务未响应?)}"
|
||||
echo " 目标版本: vaultwarden/server:${TARGET_VERSION}"
|
||||
echo " 数据目录: $DATA_DIR"
|
||||
echo " 备份目录: $BACKUP_BASE"
|
||||
echo ""
|
||||
echo " 执行步骤:"
|
||||
echo " 1. 锚定当前镜像为回滚点"
|
||||
echo " 2. 拉取新镜像(失败则零影响退出)"
|
||||
echo " 3. 停止容器"
|
||||
echo " 4. WAL checkpoint + 完整性校验 + 冷备份"
|
||||
echo " 5. 切换镜像并启动"
|
||||
echo " 6. 验证接口 + 比对数据行数"
|
||||
echo " 7. 任一验证失败 → 自动回滚"
|
||||
echo -e "${BOLD}═════════════════════════════${NC}"
|
||||
}
|
||||
|
||||
main() {
|
||||
parse_args "$@"
|
||||
|
||||
echo -e "${CYAN}${BOLD}"
|
||||
echo " Vaultwarden 安全升级"
|
||||
echo -e "${NC}"
|
||||
|
||||
load_env
|
||||
|
||||
if [ "$MODE" = "rollback" ]; then
|
||||
[ "$(id -u)" -eq 0 ] || { error "需要 root 权限"; exit 1; }
|
||||
manual_rollback
|
||||
exit 0
|
||||
fi
|
||||
|
||||
preflight
|
||||
resolve_target_version
|
||||
show_plan
|
||||
|
||||
# 已是目标版本?
|
||||
local cur_image_id target_image_id
|
||||
cur_image_id="$(running_image_id)"
|
||||
target_image_id=$(docker image inspect "vaultwarden/server:${TARGET_VERSION}" \
|
||||
--format '{{.Id}}' 2>/dev/null || true)
|
||||
if [ -n "$cur_image_id" ] && [ "$cur_image_id" = "$target_image_id" ]; then
|
||||
echo ""
|
||||
log "当前运行的已经是 ${TARGET_VERSION},无需升级"
|
||||
if [ "$MODE" = "check" ]; then
|
||||
exit 0
|
||||
fi
|
||||
confirm "仍要强制重新部署一次?" || { info "已取消"; exit 0; }
|
||||
fi
|
||||
|
||||
if [ "$MODE" = "check" ]; then
|
||||
echo ""
|
||||
info "--check 模式,未做任何改动"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
confirm "确认开始升级?" || { info "已取消"; exit 0; }
|
||||
|
||||
ensure_sqlite3
|
||||
# 先拉镜像再打回滚标签:拉取失败时服务未动,也不留下无用的标签
|
||||
pull_target_image
|
||||
tag_rollback_image
|
||||
|
||||
# ===== 从这里开始服务会中断,全程由 on_exit 兜底 =====
|
||||
trap on_exit EXIT
|
||||
trap 'exit 1' ERR
|
||||
|
||||
step "停止容器"
|
||||
SAFETY_STAGE="stopped" # 异常退出 → 直接拉起原版本
|
||||
docker compose stop
|
||||
log "容器已停止"
|
||||
|
||||
cold_backup
|
||||
save_state
|
||||
SAFETY_STAGE="armed" # 备份已就位,异常退出 → 完整回滚
|
||||
|
||||
step "启动新版本"
|
||||
switch_image_in_env "vaultwarden/server:${TARGET_VERSION}"
|
||||
# 必须 --force-recreate:容器处于 stopped 状态时,docker compose up -d 只会
|
||||
# 把原容器重新 start,不会因为镜像变更而重建,结果是「升级了但还跑着旧镜像」。
|
||||
docker compose up -d --force-recreate
|
||||
|
||||
if ! wait_healthy 90; then
|
||||
docker compose logs --tail 40 || true
|
||||
do_rollback "服务在 90 秒内未就绪"
|
||||
fi
|
||||
if ! verify_running_image "vaultwarden/server:${TARGET_VERSION}"; then
|
||||
do_rollback "容器未运行在目标镜像上"
|
||||
fi
|
||||
if ! verify_endpoints; then
|
||||
do_rollback "接口验证失败"
|
||||
fi
|
||||
if ! verify_data_integrity; then
|
||||
do_rollback "数据完整性验证失败"
|
||||
fi
|
||||
|
||||
# ===== 成功 =====
|
||||
SAFETY_STAGE="done"
|
||||
trap - EXIT ERR
|
||||
step "升级成功"
|
||||
local new_githash
|
||||
new_githash="$(get_running_githash)"
|
||||
echo ""
|
||||
echo -e "${GREEN}${BOLD}══════════ 完成 ══════════${NC}"
|
||||
echo " 运行版本: vaultwarden/server:${TARGET_VERSION}"
|
||||
echo " gitHash: ${new_githash:-未知}"
|
||||
echo " 备份文件: $BACKUP_PATH"
|
||||
echo " 回滚镜像: $ROLLBACK_TAG"
|
||||
echo ""
|
||||
echo " 如需回滚: bash upgrade.sh --rollback"
|
||||
echo " 查看日志: docker compose logs --tail 50"
|
||||
echo -e "${GREEN}${BOLD}══════════════════════════${NC}"
|
||||
echo ""
|
||||
warn "请登录 Web 端确认数据无误后,再考虑清理旧镜像:"
|
||||
warn " docker image rm $ROLLBACK_TAG"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user