Dify Docker Compose 通用无损升级指南:从备份、双版本预演到切换与回滚
一、这篇文章能解决什么问题
直接在生产目录执行 git pull、修改镜像标签并运行 docker compose up -d,虽然步骤短,但会把代码升级、数据库迁移、配置变更和生产切换绑在一起。一旦新版启动失败、凭据无法解密或知识库向量缺失,回滚会非常被动。
本文采用以下升级路线:
text
资产盘点
↓
完整备份并验证可恢复性
↓
在独立目录和独立 Compose 项目中恢复数据
↓
执行数据库迁移和全链路验收
↓
暂停生产写入并做最终同步
↓
新版接管固定入口
↓
观察期与回滚准备
↓
定向清理旧版本
目标是:
- 应用、工作流、知识库、账号、插件、模型凭据和上传文件不丢失;
- 新版先在隔离端口完成预演,验证通过后再接管正式流量;
- 外部域名、端口和 API 路径保持不变;
- 切换失败时,旧版本仍保留切换时刻的数据,可以快速恢复;
- 观察期结束并完成备份验证后,再删除旧版资源。
这里的"无损"指数据完整、配置可恢复和切换可回滚,不代表绝对零停机。没有负载均衡或外部反向代理时,正式端口交接会产生短暂维护窗口;SSE 和 WebSocket 长连接还需要客户端具备自动重连能力。
本文默认旧版和新版都使用 PostgreSQL、Redis、Weaviate 与本地文件存储。使用 MySQL、Milvus、Qdrant、外部数据库或对象存储时,需要将对应产品的原生备份与恢复流程纳入方案,不能直接照搬本文的数据目录命令。
二、执行前必须知道的边界
1. 命令运行环境
本文命令以 Linux Bash 为准,建议在 tmux 或 screen 会话中执行,避免 SSH 中断导致升级停在中间。执行账号需要具有 Docker 权限,并能读取 Dify 持久化目录。
bash
docker version
docker compose version
git --version
rsync --version
df -h
docker system df
至少准备以下磁盘空间:
- 一份当前环境;
- 一份新版预发布环境;
- 一份完整备份;
- 数据库恢复、镜像拉取和临时文件所需余量。
生产环境通常需要接近当前 Dify 持久化数据量 2~3 倍的可用空间。空间不足时不要开始升级。
2. 双版本必须真正隔离
旧版与新版至少要隔离以下资源:
| 资源 | 隔离方式 |
|---|---|
| 源码和 Docker 目录 | 两个物理目录 |
| Compose 容器和网络 | 两个 Compose 项目名 |
| PostgreSQL、Redis、Weaviate | 两套独立持久化目录或独立实例 |
app/storage 与 plugin_daemon |
两套独立目录 |
| 对外端口 | 新版预演使用独立端口 |
示例布局:
text
/srv/dify/ # 现有维护路径,可为目录或软链接
/srv/dify-releases/dify-1.16.1/ # 新版物理目录
/srv/dify-backups/ # 位于新旧版本目录之外
升级期间不要急着移动正在运行的旧目录。先把它的真实路径记录为 OLD_DIR,待观察期结束、旧容器全部删除后,再决定是否让原维护路径改为指向新版的软链接。
3. 为什么不能共用同一数据库
新版执行 flask db upgrade 后,数据库结构可能已经不再兼容旧 API。如果旧版和新版共用一套数据库,回滚时不能简单启动旧容器。
推荐做法是:
- 旧版继续使用旧 PostgreSQL、Redis 和持久化目录;
- 新版从备份恢复出一套独立副本;
- 正式切换前用最终备份覆盖新版副本;
- 旧版数据库停止但保留,作为快速回滚入口。
三、步骤 0:确认真实目录和 Compose 项目名
先查看当前 Compose 项目,不要凭容器名前缀猜测:
bash
docker compose ls
docker ps --format \
'table {{.Names}}\t{{.Label "com.docker.compose.project"}}\t{{.Image}}'
选择当前生产 Dify 对应的 Compose 项目名,然后在同一个 Bash 会话中设置变量:
bash
set -Eeuo pipefail
umask 077
export OLD_VERSION='1.11.4'
export TARGET_VERSION='1.16.1'
# STABLE_PATH 是运维人员平时进入的固定路径。
export STABLE_PATH='/srv/dify'
# OLD_DIR 必须解析为旧版真实物理目录,不能保留为可能变化的软链接。
export OLD_DIR="$(realpath -- "$STABLE_PATH")"
export RELEASE_ROOT='/srv/dify-releases'
export NEW_DIR="$RELEASE_ROOT/dify-$TARGET_VERSION"
export BACKUP_ROOT='/srv/dify-backups'
export UPGRADE_ID="$(date +%Y%m%d-%H%M%S)"
export BACKUP_DIR="$BACKUP_ROOT/$UPGRADE_ID-before-$TARGET_VERSION"
# 根据 docker compose ls 的真实结果修改。
export OLD_PROJECT='docker'
export NEW_PROJECT="dify_${TARGET_VERSION//./_}_stage"
# 根据旧版 docker/.env 的真实配置修改。
export DB_USER='postgres'
export MAIN_DB='dify'
export PLUGIN_DB='dify_plugin'
export NEW_HTTP_PORT='18082'
export NEW_HTTPS_PORT='18443'
export NEW_PLUGIN_DEBUG_PORT='15003'
升级过程应在同一个 tmux/screen 和 Bash 会话中完成。如果重新登录,必须重新执行本节变量定义,并再次确认 OLD_DIR、NEW_DIR、BACKUP_DIR 和两个 Compose 项目名,不能依赖丢失的临时变量继续执行。
执行保护性检查:
bash
test -d "$OLD_DIR/docker"
test -f "$OLD_DIR/docker/docker-compose.yaml"
test ! -e "$NEW_DIR"
case "$MAIN_DB:$PLUGIN_DB" in
*[!A-Za-z0-9_:]*)
echo '数据库名只能包含字母、数字和下划线' >&2
exit 1
;;
esac
mkdir -p "$RELEASE_ROOT" \
"$BACKUP_DIR"/{preflight,database,config,storage,final}
chmod 700 "$BACKUP_DIR"
docker ps \
--filter "label=com.docker.compose.project=$OLD_PROJECT" \
--format '{{.Names}} | {{.Image}} | {{.Status}}'
最后一条命令必须列出当前旧版 Dify 容器。如果没有输出,说明 OLD_PROJECT 配错了,必须停止操作并重新确认。
真实迁移中,升级前应先留下版本、入口和容器状态证据:

图中为真实命令结果的脱敏整理,只保留升级判断所需信息。
四、步骤 1:建立升级前数据基线
1. 确认版本、镜像和服务状态
bash
cd "$OLD_DIR/docker"
git -C "$OLD_DIR" describe --tags --always 2>/dev/null || true
docker compose -p "$OLD_PROJECT" ps -a
docker compose -p "$OLD_PROJECT" images
curl -sS -D - -o /dev/null \
http://127.0.0.1/console/api/system-features \
| grep -iE 'HTTP/|X-Version'
访问 / 可能只返回 307 跳转,并不一定携带版本头,因此版本检查应使用实际 API 路径,而不是只检查首页。
保存 Git 状态、Compose 解析结果和配置校验值。注意:解析后的 Compose 可能包含明文密钥,必须限制权限,不能放进公开截图或提交到 Git。
bash
git -C "$OLD_DIR" status --short \
> "$BACKUP_DIR/preflight/git-status.txt" 2>/dev/null || true
docker compose -p "$OLD_PROJECT" config \
> "$BACKUP_DIR/config/old-compose.resolved.yml"
chmod 600 "$BACKUP_DIR/config/old-compose.resolved.yml"
sha256sum .env docker-compose.yaml \
> "$BACKUP_DIR/preflight/deployment-files.sha256"
2. 记录数据库数量
使用相同 SQL 分别在升级前和升级后查询。表结构可能随版本变化,执行前先用 \dt 确认表名。
bash
docker compose -p "$OLD_PROJECT" exec -T db_postgres \
psql -v ON_ERROR_STOP=1 -U "$DB_USER" -d "$MAIN_DB" -At \
> "$BACKUP_DIR/preflight/database-counts.txt" <<'SQL'
SELECT 'accounts=' || count(*) FROM accounts;
SELECT 'tenants=' || count(*) FROM tenants;
SELECT 'apps=' || count(*) FROM apps;
SELECT 'datasets=' || count(*) FROM datasets;
SELECT 'documents=' || count(*) FROM documents;
SELECT 'workflows=' || count(*) FROM workflows;
SELECT 'workflow_runs=' || count(*) FROM workflow_runs;
SQL
cat "$BACKUP_DIR/preflight/database-counts.txt"
数量只能证明"没有明显减少",不能证明业务功能可用。还要提前选择以下回归样本:
- 一个简单问答应用;
- 一个包含 LLM、代码、HTTP 或工具节点的工作流;
- 一个知识库检索应用;
- 一个文件上传或文件解析流程;
- 一个需要插件授权的工具;
- 一个阻塞 API 和一个流式 API。
3. 记录本地文件清单
bash
cd "$OLD_DIR/docker"
find volumes/app/storage -type f -printf '%P|%s\n' \
| sort > "$BACKUP_DIR/preflight/app-storage-files.txt"
find volumes/plugin_daemon -type f -printf '%P|%s\n' \
| sort > "$BACKUP_DIR/preflight/plugin-files.txt"
wc -l "$BACKUP_DIR/preflight/app-storage-files.txt" \
"$BACKUP_DIR/preflight/plugin-files.txt"
du -sh volumes/app/storage volumes/plugin_daemon volumes/weaviate
df -h "$OLD_DIR" "$BACKUP_ROOT" "$RELEASE_ROOT"
五、步骤 2:执行完整备份并验证
1. 备份 Dify 主库、插件库和 PostgreSQL 全局对象
bash
cd "$OLD_DIR/docker"
docker compose -p "$OLD_PROJECT" exec -T db_postgres \
pg_dump -U "$DB_USER" -Fc "$MAIN_DB" \
> "$BACKUP_DIR/database/dify.dump"
plugin_db_exists="$(
docker compose -p "$OLD_PROJECT" exec -T db_postgres \
psql -U "$DB_USER" -d postgres -Atqc \
"SELECT 1 FROM pg_database WHERE datname='$PLUGIN_DB'"
)"
if [ "$plugin_db_exists" = '1' ]; then
docker compose -p "$OLD_PROJECT" exec -T db_postgres \
pg_dump -U "$DB_USER" -Fc "$PLUGIN_DB" \
> "$BACKUP_DIR/database/dify_plugin.dump"
else
echo '当前版本不存在独立插件库,已跳过。'
fi
docker compose -p "$OLD_PROJECT" exec -T db_postgres \
pg_dumpall -U "$DB_USER" --globals-only \
> "$BACKUP_DIR/database/postgres-globals.sql"
只备份 dify 主库是不完整的。插件化版本通常还包含 dify_plugin 数据库,保存插件安装和插件运行所需数据。
不要把复制运行中的 PostgreSQL data 目录作为主要备份方式。逻辑备份能避免缓存页、WAL 和文件时间点不一致带来的恢复风险。
2. 验证数据库备份可读取
不要求宿主机安装 PostgreSQL 工具,直接使用旧数据库容器中的 pg_restore:
bash
cd "$OLD_DIR/docker"
docker compose -p "$OLD_PROJECT" exec -T db_postgres \
pg_restore -l < "$BACKUP_DIR/database/dify.dump" \
> "$BACKUP_DIR/database/dify.list"
if [ -s "$BACKUP_DIR/database/dify_plugin.dump" ]; then
docker compose -p "$OLD_PROJECT" exec -T db_postgres \
pg_restore -l < "$BACKUP_DIR/database/dify_plugin.dump" \
> "$BACKUP_DIR/database/dify_plugin.list"
fi
test -s "$BACKUP_DIR/database/dify.list"
test ! -e "$BACKUP_DIR/database/dify_plugin.dump" \
|| test -s "$BACKUP_DIR/database/dify_plugin.list"
3. 备份上传文件和插件文件
bash
mkdir -p "$BACKUP_DIR/storage/app-storage" \
"$BACKUP_DIR/storage/plugin-daemon" \
"$BACKUP_DIR/storage/weaviate"
rsync -aHAX --numeric-ids \
"$OLD_DIR/docker/volumes/app/storage/" \
"$BACKUP_DIR/storage/app-storage/"
rsync -aHAX --numeric-ids \
"$OLD_DIR/docker/volumes/plugin_daemon/" \
"$BACKUP_DIR/storage/plugin-daemon/"
这是在线预同步,主要用于缩短维护窗口。正式切换前还必须停止写入后再次增量同步。
4. 正确处理 Weaviate 和 Redis
Weaviate 的向量数据不能只看 PostgreSQL 中的知识库记录。可选择:
- 使用 Weaviate 原生 Backup/Restore;
- 新旧 Weaviate 镜像版本相同或已确认存储格式兼容时,短暂停止旧 Weaviate 后复制数据目录。
第二种方式示例:
bash
cd "$OLD_DIR/docker"
docker compose -p "$OLD_PROJECT" stop weaviate
rsync -aHAX --numeric-ids --delete \
"$OLD_DIR/docker/volumes/weaviate/" \
"$BACKUP_DIR/storage/weaviate/"
docker compose -p "$OLD_PROJECT" start weaviate
这会产生短暂的知识库检索不可用窗口。如果新旧 Weaviate 镜像差异较大,不要直接复制目录,应改用原生备份恢复并查阅目标版本兼容说明。
Redis 主要保存缓存、会话和 Celery 队列,通常不把运行中的 Redis 数据目录复制到新版。正式切换前应阻止新请求并等待任务排空,然后让新版使用独立的干净 Redis。强行复制队列可能造成任务重复执行或丢失确认状态。
5. 备份配置和自定义内容
bash
cp -a "$OLD_DIR/docker/.env" "$BACKUP_DIR/config/old.env"
cp -a "$OLD_DIR/docker/docker-compose.yaml" "$BACKUP_DIR/config/"
cp -a "$OLD_DIR/docker/nginx" "$BACKUP_DIR/config/nginx"
if git -C "$OLD_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
git -C "$OLD_DIR" rev-parse HEAD \
> "$BACKUP_DIR/config/source-head.txt"
git -C "$OLD_DIR" diff --binary \
> "$BACKUP_DIR/config/tracked-customizations.patch"
fi
还要备份宿主机反向代理、TLS 证书、DNS、Webhook、第三方回调、自定义前端源码、Dockerfile 和外部存储配置。
必须保留原 SECRET_KEY。更换它会导致模型密钥、工具凭据等历史加密数据无法解密。PLUGIN_DAEMON_KEY 和 PLUGIN_DIFY_INNER_API_KEY 也应从旧环境迁移,不要使用目标版本示例默认值覆盖。
6. 生成正确的 SHA-256 清单
生成清单时必须排除清单文件自身:
bash
cd "$BACKUP_DIR"
find . -type f ! -name SHA256SUMS -print0 \
| sort -z \
| xargs -0 sha256sum > SHA256SUMS
sha256sum -c SHA256SUMS
如果把 SHA256SUMS 自身纳入扫描,它会在写入过程中发生变化,后续校验必然失败。

图中为真实数据库备份和校验结果,路径及校验值已脱敏。
完成后把备份复制到另一台服务器、NAS 或对象存储。只保存在同一块磁盘上的备份无法应对宿主机或磁盘故障。
六、步骤 3:准备目标版本独立目录
1. 获取指定版本
bash
git clone --branch "$TARGET_VERSION" --depth 1 \
https://github.com/langgenius/dify.git "$NEW_DIR"
git -C "$NEW_DIR" describe --tags --exact-match
cd "$NEW_DIR/docker"
cp .env.example .env
chmod 600 .env
不要直接把旧 .env 覆盖到新版,因为目标版本可能新增、删除或重命名变量。
2. 以新版配置结构为基准迁移变量
Dify 1.16.1 将不少变量从根 .env.example 拆到了 docker/envs/**/*.env.example,Compose 中还可能直接引用 ${变量名}。如果只比较根模板,会误把仍然有效的旧配置当成"已删除变量"。
下面的脚本会构建目标版本支持的变量集合:
- 根
.env.example; envs目录下所有模块模板;docker-compose.yaml中引用的环境变量。
它以新版根模板为主体,迁移旧环境中仍被目标版本支持的值;不在根模板但仍受支持的旧变量会追加到新版根 .env。在 1.16.1 Compose 中,根 .env 位于各模块 env_file 列表末尾,可覆盖模块默认值。
bash
python3 - "$BACKUP_DIR/config/old.env" \
"$NEW_DIR/docker" \
"$NEW_DIR/docker/.env" <<'PY'
import re
import sys
from pathlib import Path
old_path, docker_dir, output_path = map(Path, sys.argv[1:])
template_path = docker_dir / ".env.example"
compose_path = docker_dir / "docker-compose.yaml"
key_pattern = re.compile(r"^([A-Z][A-Z0-9_]*)=(.*)$")
compose_pattern = re.compile(r"\$\{([A-Z][A-Z0-9_]*)")
def read_values(path: Path) -> dict[str, str]:
values: dict[str, str] = {}
for line in path.read_text(encoding="utf-8").splitlines():
match = key_pattern.match(line)
if match:
values[match.group(1)] = match.group(2)
return values
old_values = read_values(old_path)
root_keys = set(read_values(template_path))
supported_keys = set(root_keys)
for module_template in (docker_dir / "envs").rglob("*.env.example"):
supported_keys.update(read_values(module_template))
supported_keys.update(
compose_pattern.findall(compose_path.read_text(encoding="utf-8"))
)
output: list[str] = []
for line in template_path.read_text(encoding="utf-8").splitlines(keepends=True):
match = key_pattern.match(line.rstrip("\r\n"))
if match and match.group(1) in old_values:
newline = "\n" if line.endswith("\n") else ""
output.append(f"{match.group(1)}={old_values[match.group(1)]}{newline}")
else:
output.append(line)
for key in sorted((set(old_values) & supported_keys) - root_keys):
output.append(f"\n{key}={old_values[key]}\n")
output_path.write_text("".join(output), encoding="utf-8")
PY
chmod 600 "$NEW_DIR/docker/.env"
再比较变量名,人工处理新增项和删除项:
bash
sed -n 's/^\([A-Z][A-Z0-9_]*\)=.*/\1/p' \
"$BACKUP_DIR/config/old.env" | sort -u \
> "$BACKUP_DIR/config/old-env.keys"
{
sed -n 's/^\([A-Z][A-Z0-9_]*\)=.*/\1/p' \
"$NEW_DIR/docker/.env.example"
find "$NEW_DIR/docker/envs" -type f -name '*.env.example' -print0 \
| xargs -0 sed -n 's/^\([A-Z][A-Z0-9_]*\)=.*/\1/p'
grep -rho '\${[A-Z][A-Z0-9_]*' \
"$NEW_DIR/docker/docker-compose.yaml" | sed 's/^${//'
} | sort -u > "$BACKUP_DIR/config/new-supported-env.keys"
echo '=== 目标版本新增变量 ==='
comm -13 "$BACKUP_DIR/config/old-env.keys" \
"$BACKUP_DIR/config/new-supported-env.keys"
echo '=== 目标版本已删除变量 ==='
comm -23 "$BACKUP_DIR/config/old-env.keys" \
"$BACKUP_DIR/config/new-supported-env.keys"
迁移后仍要人工复核新增变量。自动脚本只能判断"变量名是否仍受支持",不能替你决定新功能应该启用还是关闭。
3. 设置预发布端口
至少修改新版 .env 中以下端口:
dotenv
EXPOSE_NGINX_PORT=18082
EXPOSE_NGINX_SSL_PORT=18443
EXPOSE_PLUGIN_DEBUGGING_PORT=15003
只修改 Nginx 端口不够。Dify 1.11.4 和 1.16.1 默认都可能发布 Plugin Daemon 调试端口 5003,不修改会导致新版启动时报 port is already allocated。
若使用 Milvus、Elasticsearch、OceanBase、OpenGauss 等会发布宿主机端口的中间件,也要逐项检查目标版本 Compose 的 ports,确保与旧版及宿主机现有服务不冲突。
bash
for port in "$NEW_HTTP_PORT" "$NEW_HTTPS_PORT" "$NEW_PLUGIN_DEBUG_PORT"; do
if ss -ltn | awk '{print $4}' | grep -Eq ":${port}$"; then
echo "端口已被占用:$port" >&2
exit 1
fi
done
4. Dify 1.16.1 的 Agent 与协作配置
1.16.1 新增了 agent_backend、local_sandbox、agent_ssrf_proxy 和 api_websocket 等服务。生产环境不能继续使用 Compose 中标注为开发用途的默认密钥。
在新版 .env 中生成并追加独立密钥:
bash
python3 - "$NEW_DIR/docker/.env" <<'PY'
import re
import secrets
import sys
from pathlib import Path
path = Path(sys.argv[1])
text = path.read_text(encoding="utf-8")
keys = (
"DIFY_AGENT_SERVER_SECRET_KEY",
"DIFY_AGENT_API_TOKEN",
"DIFY_AGENT_SHELLCTL_AUTH_TOKEN",
)
for key in keys:
if re.search(rf"(?m)^{re.escape(key)}=.+$", text):
continue
text += f"\n{key}={secrets.token_urlsafe(48)}\n"
path.write_text(text, encoding="utf-8")
PY
chmod 600 "$NEW_DIR/docker/.env"
同时检查:
dotenv
ENABLE_COLLABORATION_MODE=true
COMPOSE_PROFILES=${VECTOR_STORE:-weaviate},${DB_TYPE:-postgresql},collaboration
NEXT_PUBLIC_SOCKET_URL=ws://预发布主机:18082
正式环境启用 HTTPS 时,NEXT_PUBLIC_SOCKET_URL 应使用 wss://正式域名。如果不使用协作功能,可以根据目标版本注释移除 collaboration Profile,但这会停用专用 WebSocket 服务,必须结合实际功能验证。
5. 校验配置并拉取镜像
bash
cd "$NEW_DIR/docker"
docker compose -p "$NEW_PROJECT" config -q
docker compose -p "$NEW_PROJECT" config --services
docker compose -p "$NEW_PROJECT" config --images | sort -u
docker compose -p "$NEW_PROJECT" pull
docker compose config 的完整输出可能包含密钥,不要直接截图或发布。
七、步骤 4:恢复预发布数据并执行迁移
1. 启动新版 PostgreSQL 和 Redis
bash
cd "$NEW_DIR/docker"
docker compose -p "$NEW_PROJECT" up -d db_postgres redis
docker compose -p "$NEW_PROJECT" ps db_postgres redis
2. 重建并恢复主库和插件库
以下命令只适用于新版独立数据库实例,不能对仍在提供生产服务的数据库执行:
bash
cd "$NEW_DIR/docker"
docker compose -p "$NEW_PROJECT" exec -T db_postgres \
psql -v ON_ERROR_STOP=1 -U "$DB_USER" -d postgres -c \
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity
WHERE datname='$MAIN_DB' AND pid <> pg_backend_pid();"
docker compose -p "$NEW_PROJECT" exec -T db_postgres \
dropdb -U "$DB_USER" --if-exists --force "$MAIN_DB"
docker compose -p "$NEW_PROJECT" exec -T db_postgres \
createdb -U "$DB_USER" "$MAIN_DB"
docker compose -p "$NEW_PROJECT" exec -T db_postgres \
pg_restore -v -U "$DB_USER" --no-owner --no-privileges \
-d "$MAIN_DB" < "$BACKUP_DIR/database/dify.dump" \
> "$BACKUP_DIR/database/stage-main-restore.log" 2>&1
if [ -s "$BACKUP_DIR/database/dify_plugin.dump" ]; then
docker compose -p "$NEW_PROJECT" exec -T db_postgres \
psql -v ON_ERROR_STOP=1 -U "$DB_USER" -d postgres -c \
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity
WHERE datname='$PLUGIN_DB' AND pid <> pg_backend_pid();"
docker compose -p "$NEW_PROJECT" exec -T db_postgres \
dropdb -U "$DB_USER" --if-exists --force "$PLUGIN_DB"
docker compose -p "$NEW_PROJECT" exec -T db_postgres \
createdb -U "$DB_USER" "$PLUGIN_DB"
docker compose -p "$NEW_PROJECT" exec -T db_postgres \
pg_restore -v -U "$DB_USER" --no-owner --no-privileges \
-d "$PLUGIN_DB" < "$BACKUP_DIR/database/dify_plugin.dump" \
> "$BACKUP_DIR/database/stage-plugin-restore.log" 2>&1
fi
如果使用外部 PostgreSQL、非默认角色或自定义授权,应先在隔离实例中检查 postgres-globals.sql,再决定如何恢复角色。不要未经审查直接覆盖生产全局对象。
3. 恢复本地持久化文件
在新版 API、Worker、Plugin Daemon 和 Weaviate 尚未运行时执行:
bash
mkdir -p "$NEW_DIR/docker/volumes/app/storage" \
"$NEW_DIR/docker/volumes/plugin_daemon" \
"$NEW_DIR/docker/volumes/weaviate"
rsync -aHAX --numeric-ids --delete \
"$BACKUP_DIR/storage/app-storage/" \
"$NEW_DIR/docker/volumes/app/storage/"
rsync -aHAX --numeric-ids --delete \
"$BACKUP_DIR/storage/plugin-daemon/" \
"$NEW_DIR/docker/volumes/plugin_daemon/"
# 只有确认 Weaviate 存储格式兼容时才执行。
rsync -aHAX --numeric-ids --delete \
"$BACKUP_DIR/storage/weaviate/" \
"$NEW_DIR/docker/volumes/weaviate/"
使用 S3、OSS、COS、Azure Blob 等对象存储时,不要复制本地空目录。应确认新版继续访问正确的存储桶和前缀,或者先完成对象级迁移与数量校验。
4. 执行数据库迁移并启动新版
bash
cd "$NEW_DIR/docker"
docker compose -p "$NEW_PROJECT" run --rm api flask db upgrade
docker compose -p "$NEW_PROJECT" up -d
docker compose -p "$NEW_PROJECT" ps -a
查询 Alembic 迁移版本:
bash
docker compose -p "$NEW_PROJECT" exec -T db_postgres \
psql -U "$DB_USER" -d "$MAIN_DB" \
-c 'SELECT version_num FROM alembic_version;'
检查一次性权限容器:
bash
init_id="$(
docker compose -p "$NEW_PROJECT" ps -a -q init_permissions
)"
docker inspect "$init_id" \
--format 'state={{.State.Status}} exit={{.State.ExitCode}}'
init_permissions 正常完成后的状态应为 state=exited exit=0,这不是启动失败。它负责调整 app/storage 权限;因此升级后必须同时验证历史文件和新增文件。
最后检查关键日志:
bash
docker compose -p "$NEW_PROJECT" logs --tail=200 api
docker compose -p "$NEW_PROJECT" logs --tail=200 worker
docker compose -p "$NEW_PROJECT" logs --tail=200 plugin_daemon
docker compose -p "$NEW_PROJECT" logs --tail=200 agent_backend

图中展示真实迁移后的数据库、权限容器与基础服务检查,环境标识已移除。
八、步骤 5:预发布全链路验收
1. 版本、容器和中间件
bash
curl -fsS -D /tmp/dify-stage-headers.txt -o /dev/null \
"http://127.0.0.1:$NEW_HTTP_PORT/console/api/system-features"
cat /tmp/dify-stage-headers.txt | grep -iE 'HTTP/|X-Version'
stage_version="$(
awk 'BEGIN{IGNORECASE=1} /^X-Version:/{gsub("\r", ""); print $2}' \
/tmp/dify-stage-headers.txt
)"
test "$stage_version" = "$TARGET_VERSION"
cd "$NEW_DIR/docker"
docker compose -p "$NEW_PROJECT" ps -a
docker compose -p "$NEW_PROJECT" exec -T db_postgres \
pg_isready -U "$DB_USER" -d "$MAIN_DB"
docker compose -p "$NEW_PROJECT" exec -T redis sh -lc \
'REDISCLI_AUTH="$REDIS_PASSWORD" redis-cli ping'
2. 数据库和文件数量对比
bash
docker compose -p "$NEW_PROJECT" exec -T db_postgres \
psql -v ON_ERROR_STOP=1 -U "$DB_USER" -d "$MAIN_DB" -At \
> "$BACKUP_DIR/preflight/stage-database-counts.txt" <<'SQL'
SELECT 'accounts=' || count(*) FROM accounts;
SELECT 'tenants=' || count(*) FROM tenants;
SELECT 'apps=' || count(*) FROM apps;
SELECT 'datasets=' || count(*) FROM datasets;
SELECT 'documents=' || count(*) FROM documents;
SELECT 'workflows=' || count(*) FROM workflows;
SELECT 'workflow_runs=' || count(*) FROM workflow_runs;
SQL
diff -u "$BACKUP_DIR/preflight/database-counts.txt" \
"$BACKUP_DIR/preflight/stage-database-counts.txt"
find "$NEW_DIR/docker/volumes/app/storage" -type f -printf '%P|%s\n' \
| sort > "$BACKUP_DIR/preflight/stage-app-storage-files.txt"
diff -u "$BACKUP_DIR/preflight/app-storage-files.txt" \
"$BACKUP_DIR/preflight/stage-app-storage-files.txt"
预发布验收期间如果主动创建测试数据,差异必须能逐项解释,不能简单忽略数量变化。
3. 模型供应商和插件
人工检查并真实调用:
- LLM;
- Embedding;
- Rerank;
- 语音模型;
- 已安装插件;
- 需要凭据的工具;
- 插件安装、升级和执行。

跨版本升级后还要确认模型供应商已经按目标版本要求完成插件化迁移:

如果模型供应商仍存在但凭据全部失效,优先检查 SECRET_KEY 是否与旧版一致,而不是立即重新填写所有密钥。
4. 阻塞工作流 API
请求参数必须与测试工作流"开始"节点定义的变量一致。下面的 query 只是示例:
bash
read -rsp '测试工作流 App Key: ' TEST_APP_KEY
echo
curl -sS --max-time 180 \
-X POST "http://127.0.0.1:$NEW_HTTP_PORT/v1/workflows/run" \
-H "Authorization: Bearer $TEST_APP_KEY" \
-H 'Content-Type: application/json' \
-d '{
"inputs": {"query": "upgrade smoke test"},
"response_mode": "blocking",
"user": "upgrade-check"
}' | tee /tmp/dify-blocking-check.json
blocking_status="$(
jq -r '.data.status // .status // "missing"' \
/tmp/dify-blocking-check.json
)"
echo "blocking_status=$blocking_status"
test "$blocking_status" = 'succeeded'
unset TEST_APP_KEY
HTTP 200 只表示接口接收了请求。必须检查最终状态为 succeeded。
5. SSE 流式工作流
bash
read -rsp '测试工作流 App Key: ' TEST_APP_KEY
echo
curl -sS -N --max-time 180 \
-X POST "http://127.0.0.1:$NEW_HTTP_PORT/v1/workflows/run" \
-H "Authorization: Bearer $TEST_APP_KEY" \
-H 'Content-Type: application/json' \
-d '{
"inputs": {"query": "upgrade stream smoke test"},
"response_mode": "streaming",
"user": "upgrade-check"
}' | tee /tmp/dify-stream-check.txt
finished_event="$(
sed -n 's/^data: //p' /tmp/dify-stream-check.txt \
| jq -c 'select(.event == "workflow_finished")' \
| tail -n 1
)"
test -n "$finished_event"
stream_status="$(jq -r '.data.status // "missing"' <<< "$finished_event")"
echo "stream_status=$stream_status"
test "$stream_status" = 'succeeded'
unset TEST_APP_KEY
必须看到 workflow_finished,且最终状态为 succeeded。
6. WebSocket 握手
bash
python3 - "$NEW_HTTP_PORT" <<'PY'
import socket
import sys
port = int(sys.argv[1])
request = (
"GET /socket.io/?EIO=4&transport=websocket HTTP/1.1\r\n"
"Host: 127.0.0.1\r\n"
"Upgrade: websocket\r\n"
"Connection: Upgrade\r\n"
"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
"Sec-WebSocket-Version: 13\r\n\r\n"
).encode()
with socket.create_connection(("127.0.0.1", port), timeout=5) as sock:
sock.sendall(request)
response = sock.recv(4096).decode("latin-1", errors="replace")
status = response.split("\r\n", 1)[0]
print(status)
if " 101 " not in status:
raise SystemExit(1)
PY
成功结果应为 HTTP/1.1 101 Switching Protocols。

图中为真实阻塞工作流、SSE 和 WebSocket 验证结果,不包含 App Key 与业务输入输出。
7. 必须完成的人工回归
- 管理员和普通成员登录;
- 工作空间和权限;
- 历史文件下载、新文件上传和文档解析;
- 知识库检索与召回内容;
- LLM、Embedding、Rerank 和插件工具;
- 代码节点、HTTP 节点和文件输入节点;
- Webhook、触发器、计划任务和外部回调;
- 浏览器控制台中的 CORS、Mixed Content、SSE 和 WebSocket 错误。
九、步骤 6:正式切换前的最终同步
预发布数据来自前一次备份。验收期间旧版仍在产生新数据,因此正式切换前必须再次同步。
1. 阻止新请求并排空任务
先在负载均衡、反向代理或维护页阻止新的登录、上传、工作流和知识库写入,然后停止旧版入口、API 和定时任务:
bash
cd "$OLD_DIR/docker"
docker compose -p "$OLD_PROJECT" stop nginx web api worker_beat
Worker 暂时保留,用于完成已接收任务。检查活动任务:
bash
docker compose -p "$OLD_PROJECT" exec -T worker \
celery -A celery_healthcheck.celery inspect active --timeout 10
docker compose -p "$OLD_PROJECT" exec -T worker \
celery -A celery_healthcheck.celery inspect reserved --timeout 10
必须确认队列为空。若旧版本不包含 celery_healthcheck.celery,应结合 Worker 日志、工作流运行状态和队列监控确认没有在途任务,不能直接杀死正在执行的知识库索引或长工作流。
队列排空后停止剩余写入服务和 Weaviate:
bash
docker compose -p "$OLD_PROJECT" stop \
worker plugin_daemon weaviate
2. 生成切换时刻的最终数据
bash
mkdir -p "$BACKUP_DIR/final"/{database,app-storage,plugin-daemon,weaviate}
plugin_db_exists="$(
docker compose -p "$OLD_PROJECT" exec -T db_postgres \
psql -U "$DB_USER" -d postgres -Atqc \
"SELECT 1 FROM pg_database WHERE datname='$PLUGIN_DB'"
)"
docker compose -p "$OLD_PROJECT" exec -T db_postgres \
pg_dump -U "$DB_USER" -Fc "$MAIN_DB" \
> "$BACKUP_DIR/final/database/dify.dump"
if [ "$plugin_db_exists" = '1' ]; then
docker compose -p "$OLD_PROJECT" exec -T db_postgres \
pg_dump -U "$DB_USER" -Fc "$PLUGIN_DB" \
> "$BACKUP_DIR/final/database/dify_plugin.dump"
fi
rsync -aHAX --numeric-ids --delete \
"$OLD_DIR/docker/volumes/app/storage/" \
"$BACKUP_DIR/final/app-storage/"
rsync -aHAX --numeric-ids --delete \
"$OLD_DIR/docker/volumes/plugin_daemon/" \
"$BACKUP_DIR/final/plugin-daemon/"
# 只有确认存储格式兼容时执行。
rsync -aHAX --numeric-ids --delete \
"$OLD_DIR/docker/volumes/weaviate/" \
"$BACKUP_DIR/final/weaviate/"
docker compose -p "$OLD_PROJECT" exec -T db_postgres \
pg_restore -l < "$BACKUP_DIR/final/database/dify.dump" \
> /dev/null
if [ -s "$BACKUP_DIR/final/database/dify_plugin.dump" ]; then
docker compose -p "$OLD_PROJECT" exec -T db_postgres \
pg_restore -l < "$BACKUP_DIR/final/database/dify_plugin.dump" \
> /dev/null
fi
最终备份验证通过后,停止旧数据库和 Redis,但不要删除容器、目录或数据:
bash
docker compose -p "$OLD_PROJECT" stop db_postgres redis
3. 用最终数据覆盖新版预发布副本
先停止新版所有可能读写数据的服务:
bash
cd "$NEW_DIR/docker"
docker compose -p "$NEW_PROJECT" stop \
nginx web api api_websocket worker worker_beat \
plugin_daemon agent_backend local_sandbox weaviate
新版 PostgreSQL 保持运行,重建并恢复最终数据库:
bash
docker compose -p "$NEW_PROJECT" exec -T db_postgres \
psql -v ON_ERROR_STOP=1 -U "$DB_USER" -d postgres -c \
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity
WHERE datname='$MAIN_DB' AND pid <> pg_backend_pid();"
docker compose -p "$NEW_PROJECT" exec -T db_postgres \
dropdb -U "$DB_USER" --if-exists --force "$MAIN_DB"
docker compose -p "$NEW_PROJECT" exec -T db_postgres \
createdb -U "$DB_USER" "$MAIN_DB"
docker compose -p "$NEW_PROJECT" exec -T db_postgres \
pg_restore -U "$DB_USER" --no-owner --no-privileges \
-d "$MAIN_DB" < "$BACKUP_DIR/final/database/dify.dump"
if [ -s "$BACKUP_DIR/final/database/dify_plugin.dump" ]; then
docker compose -p "$NEW_PROJECT" exec -T db_postgres \
psql -v ON_ERROR_STOP=1 -U "$DB_USER" -d postgres -c \
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity
WHERE datname='$PLUGIN_DB' AND pid <> pg_backend_pid();"
docker compose -p "$NEW_PROJECT" exec -T db_postgres \
dropdb -U "$DB_USER" --if-exists --force "$PLUGIN_DB"
docker compose -p "$NEW_PROJECT" exec -T db_postgres \
createdb -U "$DB_USER" "$PLUGIN_DB"
docker compose -p "$NEW_PROJECT" exec -T db_postgres \
pg_restore -U "$DB_USER" --no-owner --no-privileges \
-d "$PLUGIN_DB" < "$BACKUP_DIR/final/database/dify_plugin.dump"
fi
恢复最终文件:
bash
rsync -aHAX --numeric-ids --delete \
"$BACKUP_DIR/final/app-storage/" \
"$NEW_DIR/docker/volumes/app/storage/"
rsync -aHAX --numeric-ids --delete \
"$BACKUP_DIR/final/plugin-daemon/" \
"$NEW_DIR/docker/volumes/plugin_daemon/"
# 只有确认 Weaviate 存储格式兼容时执行。
rsync -aHAX --numeric-ids --delete \
"$BACKUP_DIR/final/weaviate/" \
"$NEW_DIR/docker/volumes/weaviate/"
重新迁移并启动新版:
bash
docker compose -p "$NEW_PROJECT" run --rm api flask db upgrade
docker compose -p "$NEW_PROJECT" up -d
docker compose -p "$NEW_PROJECT" ps -a
十、步骤 7:让新版接管正式入口
方案 A:外部反向代理切换(推荐)
稳定入口应由宿主机 Nginx、HAProxy、Caddy 或负载均衡器提供,新旧 Dify 分别监听内部端口。Nginx 上游结构示例:
nginx
upstream dify_active {
server 127.0.0.1:18082;
keepalive 32;
}
location / {
proxy_pass http://dify_active;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
}
实际生产配置通常还包含 TLS、上传大小、SSE 超时和安全头,不能用上面的片段直接覆盖现有站点。只修改活动上游地址,并在切换前执行:
bash
nginx -t
systemctl reload nginx
外部域名和 API 地址不变,只有内部上游从旧版切到新版。
方案 B:新版直接接管 80/443
如果没有外部反向代理,旧版 Nginx 停止后,把新版 .env 中:
dotenv
EXPOSE_NGINX_PORT=80
EXPOSE_NGINX_SSL_PORT=443
然后只重建新版 Nginx:
bash
cd "$NEW_DIR/docker"
docker compose -p "$NEW_PROJECT" config -q
docker compose -p "$NEW_PROJECT" up -d --force-recreate nginx
这种方式会产生端口交接窗口。回滚时需要先停止新版 Nginx,才能重新启动旧版 Nginx。
切换后立即验证
bash
curl -fsS -D /tmp/dify-production-headers.txt -o /dev/null \
https://dify.example.com/console/api/system-features
cat /tmp/dify-production-headers.txt | grep -iE 'HTTP/|X-Version'
production_version="$(
awk 'BEGIN{IGNORECASE=1} /^X-Version:/{gsub("\r", ""); print $2}' \
/tmp/dify-production-headers.txt
)"
test "$production_version" = "$TARGET_VERSION"
然后从正式域名重复:
- 登录;
- 阻塞工作流;
- SSE;
- WebSocket;
- 知识库检索;
- 模型与插件调用;
- 历史文件访问和新文件上传;
- 外部 Webhook 与回调。
不要只验证 curl 返回 200。
十一、步骤 8:完整回滚方法
出现以下情况应优先回滚:
- 登录或历史凭据解密异常;
- 工作流大面积失败;
- 插件、模型供应商或 Agent 服务不可用;
- 知识库召回明显缺失;
- 历史上传文件无法访问;
- 数据库迁移错误或关键服务持续重启。
1. 推荐架构下的快速回滚
旧版数据库和目录仍然保留在切换时刻,因此不需要先恢复数据库:
bash
# 先在正式入口阻止新请求。
cd "$NEW_DIR/docker"
docker compose -p "$NEW_PROJECT" stop \
nginx web api api_websocket worker worker_beat \
plugin_daemon agent_backend local_sandbox weaviate
cd "$OLD_DIR/docker"
docker compose -p "$OLD_PROJECT" start \
db_postgres redis weaviate plugin_daemon \
api worker worker_beat web nginx
如果使用外部反向代理,将上游切回旧版端口并执行:
bash
nginx -t
systemctl reload nginx
如果采用 80/443 直接接管模式,确保新版 Nginx 已停止,再启动旧版 Nginx。
2. 旧容器已删除时的恢复
必须从旧版物理目录重新创建容器,不能从已经指向新版的软链接启动:
bash
cd "$OLD_DIR/docker"
docker compose -p "$OLD_PROJECT" down --remove-orphans
docker compose -p "$OLD_PROJECT" up -d
3. 旧数据库也不可用时的恢复
先使用升级前或最终备份恢复独立旧版数据库,再启动旧 API。禁止让旧 API 连接已被 1.16.1 迁移过的数据库。
bash
cd "$OLD_DIR/docker"
docker compose -p "$OLD_PROJECT" up -d db_postgres redis
docker compose -p "$OLD_PROJECT" exec -T db_postgres \
dropdb -U "$DB_USER" --if-exists --force "$MAIN_DB"
docker compose -p "$OLD_PROJECT" exec -T db_postgres \
createdb -U "$DB_USER" "$MAIN_DB"
docker compose -p "$OLD_PROJECT" exec -T db_postgres \
pg_restore -U "$DB_USER" --no-owner --no-privileges \
-d "$MAIN_DB" < "$BACKUP_DIR/final/database/dify.dump"
if [ -s "$BACKUP_DIR/final/database/dify_plugin.dump" ]; then
docker compose -p "$OLD_PROJECT" exec -T db_postgres \
dropdb -U "$DB_USER" --if-exists --force "$PLUGIN_DB"
docker compose -p "$OLD_PROJECT" exec -T db_postgres \
createdb -U "$DB_USER" "$PLUGIN_DB"
docker compose -p "$OLD_PROJECT" exec -T db_postgres \
pg_restore -U "$DB_USER" --no-owner --no-privileges \
-d "$PLUGIN_DB" < "$BACKUP_DIR/final/database/dify_plugin.dump"
fi
docker compose -p "$OLD_PROJECT" up -d
回滚后,新版接管期间新增的数据不会自动出现在旧版中。上线前必须明确恢复点目标:是允许回到切换时刻,还是需要人工补录新版期间数据。
十二、步骤 9:观察期结束后清理旧版本
旧版不能在切换后立即删除。至少经过一个完整业务周期,并确认:
- 正式域名和 API 流量已指向新版;
- 新版版本头、数据库、Redis、向量库和插件服务正常;
- 阻塞工作流、SSE、WebSocket、文件和外部回调全部验收;
- 主库、插件库、配置和持久化数据备份均可读取;
- 回滚材料位于旧目录之外;
- 业务负责人确认观察期结束。
1. 先确认没有运行中的容器挂载旧目录
bash
OLD_REAL="$(realpath -- "$OLD_DIR")"
NEW_REAL="$(realpath -- "$NEW_DIR")"
test "$OLD_REAL" != "$NEW_REAL"
running_ids="$(docker ps -q)"
if [ -n "$running_ids" ]; then
docker inspect $running_ids \
--format '{{.Name}}{{range .Mounts}} {{.Source}}{{end}}' \
| grep -F -- "$OLD_REAL" || true
fi
最后一条命令必须没有输出。如果还有运行中的旧容器引用旧目录,不能继续清理。
2. 定向删除旧 Compose 资源
bash
cd "$OLD_REAL/docker"
docker compose -p "$OLD_PROJECT" config --images \
| sort -u > "$BACKUP_DIR/config/old-images.txt"
docker compose -p "$OLD_PROJECT" down --remove-orphans
docker ps -a \
--filter "label=com.docker.compose.project=$OLD_PROJECT"
docker network ls \
--filter "label=com.docker.compose.project=$OLD_PROJECT"
容器和网络查询必须为空。然后再次检查所有容器挂载:
bash
all_ids="$(docker ps -aq)"
if [ -n "$all_ids" ]; then
docker inspect $all_ids \
--format '{{.Name}}{{range .Mounts}} {{.Source}}{{end}}' \
| grep -F -- "$OLD_REAL" || true
fi
3. 只删除不再引用的旧专用镜像
bash
while IFS= read -r image; do
[ -n "$image" ] || continue
if docker ps -a --filter "ancestor=$image" -q | grep -q .; then
echo "保留仍被容器引用的镜像:$image"
else
docker image rm "$image" || true
fi
done < "$BACKUP_DIR/config/old-images.txt"
不要运行 docker system prune -a 代替定向清理,它可能删除其他项目仍有价值的镜像、网络和构建缓存。
4. 将旧目录移入隔离区,再更新固定维护路径
bash
export QUARANTINE_ROOT='/srv/dify-retired'
mkdir -p "$QUARANTINE_ROOT"
case "$OLD_REAL" in
/srv/dify|/srv/dify-releases/dify-*) ;;
*)
echo "拒绝移动非预期目录:$OLD_REAL" >&2
exit 1
;;
esac
retired_path="$QUARANTINE_ROOT/dify-$OLD_VERSION-$UPGRADE_ID"
test ! -e "$retired_path"
mv -- "$OLD_REAL" "$retired_path"
ln -sfn "$NEW_REAL" "$STABLE_PATH.next"
mv -Tf "$STABLE_PATH.next" "$STABLE_PATH"
readlink -f "$STABLE_PATH"
固定路径必须解析到新版物理目录。旧目录先在隔离区保留一段时间,确认备份可恢复后再人工核对并物理删除。

图中展示真实切换后版本、实时链路、备份可读性以及旧资源归零结果,服务器信息已移除。
十三、常见问题
1. 新版能登录,但模型凭据全部失效
检查 SECRET_KEY 是否与旧版完全一致。历史凭据无法解密通常不是模型服务问题。
2. 应用和工作流存在,但插件消失或无法执行
检查是否迁移了 dify_plugin 数据库和 volumes/plugin_daemon,并查看 Plugin Daemon 日志。只恢复 Dify 主库是不完整的。
3. 知识库记录存在,但检索不到内容
PostgreSQL 只保存知识库元数据,向量内容位于 Weaviate 或其他向量数据库。检查向量数据、索引、鉴权和连接地址是否正确迁移。
4. HTTP 返回 200,但工作流失败
阻塞响应要检查最终 status;SSE 要检查最终 workflow_finished 事件。模型、插件或下游 HTTP 服务失败时,接口层仍可能已经返回 200。
5. 页面正常,但 SSE 中断或 WebSocket 失败
检查代理缓冲、读写超时、Upgrade/Connection 请求头、NEXT_PUBLIC_SOCKET_URL、跨域配置以及 Redis Pub/Sub。
6. init_permissions 显示 Exited
如果退出码是 0,这是预期状态;如果退出码非 0,检查 volumes/app/storage 权限和宿主机文件系统是否允许修改属主。
7. 历史文件打不开
检查 app/storage 文件数量、权限、FILES_URL、APP_API_URL、对象存储域名及代理路由。还要真实上传一份新文件,不能只检查历史文件。
8. 新版启动时报端口被占用
除 80/443 外,还要检查 Plugin Daemon 调试端口 5003,以及当前启用的向量数据库或中间件端口。
9. 数据库升级中途失败
停止继续写入,保存 API、Plugin Daemon 和 PostgreSQL 日志。在隔离数据库中查明原因,不要对同一份生产数据库反复盲目执行迁移。
10. 软链接更新后旧版无法安全启动
不要从固定软链接启动旧版。必须进入旧版物理目录;如果旧容器原来的 bind mount 路径已经被软链接替换,应先 docker compose down,再从旧物理目录重新创建容器。
十四、最终验收清单
- 已确认旧版真实目录和真实 Compose 项目名;
- 新旧目录、Compose 项目、数据库、Redis 和持久化目录完全隔离;
- 主数据库与插件数据库备份可被
pg_restore读取; -
SHA256SUMS校验全部通过; - 上传文件、插件文件、向量数据、配置和自定义内容已备份;
- 备份至少有一份位于不同故障域;
-
SECRET_KEY和插件内部密钥正确迁移; - 1.16.1 Agent 密钥已更换,不再使用开发默认值;
- Nginx、HTTPS、Plugin Daemon 和中间件端口没有冲突;
- 数据库迁移成功,Alembic 版本可查询;
-
init_permissions为Exited (0); - API、Worker、Plugin Daemon 和 Agent Backend 无持续异常;
- 账号、租户、应用、工作流、知识库和文件基线没有回退;
- LLM、Embedding、Rerank、插件工具和知识库真实调用成功;
- 历史文件可访问,新文件可上传和解析;
- 阻塞工作流最终状态为
succeeded; - SSE 收到成功的
workflow_finished; - WebSocket 握手返回 101;
- 在途 Worker 任务排空后才执行最终同步;
- 最终数据库和持久化文件已覆盖预发布副本;
- 正式入口切换后重新完成全部关键验收;
- 回滚入口和恢复点目标已经确认;
- 观察期结束前没有删除旧版;
- 清理前确认没有运行容器挂载旧目录;
- 旧容器、旧网络和旧专用镜像采用定向方式清理;
- 固定维护路径最终解析到新版物理目录。
十五、总结
Dify 跨版本升级的核心不是更换镜像标签,而是同时处理:
- Dify 主数据库;
- 插件数据库;
- 上传文件;
- 向量数据库;
- Plugin Daemon 持久化文件;
- 加密密钥与环境变量;
- 模型和插件兼容性;
- SSE、WebSocket 与反向代理;
- 正式切换、回滚和旧版清理。
把升级拆成"备份---预演---最终同步---切换---验证---观察---清理",并让旧版和新版真正隔离,就能把一次不可逆的覆盖操作,变成一套可验证、可切换、可回滚的发布流程。