Dify backup and upgrade
Use this when backing up, restoring, upgrading, or moving a self-hosted Dify (including air-gap). Do not compose down -v.
What must move together
Offline copies that "only brought images + compose" fail. Pack all of:
| Piece |
Typical path |
If missing |
| Images |
docker save matching compose tags |
cannot start |
| Compose + nginx |
docker-compose.yaml, nginx/ |
cannot orchestrate |
.env |
includes SECRET_KEY |
model/tool credentials will not decrypt |
Postgres dify |
volume or pg_dump -Fc |
apps/workflows/datasets gone |
Postgres dify_plugin |
same |
plugin install records gone |
volumes/plugin_daemon/plugin_packages/ |
.difypkg blobs |
plugins vanish |
volumes/plugin_daemon/cwd/ |
plugin venvs |
plugins reinstall from scratch |
| Vector store |
weaviate / milvus volumes |
RAG empty |
| App storage |
volumes/app/storage |
uploads gone |
SECRET_KEY must stay byte-identical. Changing it logs everyone out and blank-decrypts providers. Plugin-daemon / sandbox tags may not match the api/web tag — copy whatever compose currently pins.
Postgres dump (prefer this over copying volumes/db live)
docker compose exec -T db_postgres pg_dump -U postgres -d dify -Fc > dify.dump
docker compose exec -T db_postgres pg_dump -U postgres -d dify_plugin -Fc > dify_plugin.dump
pg_restore --list dify.dump | head
Air-gap pack / restore
compose stop api worker worker_beat web plugin_daemon (keep db up for dump, or stop all for volume tar).
docker save every image in compose (api, web, plugin_daemon, sandbox, agent_*, nginx, postgres, redis, vector, minio/etcd/squid if present).
- Tar
volumes/plugin_daemon volumes/app vector volumes redis as needed.
- Copy compose +
.env + nginx.
- Target:
docker load, lay volumes at the same relative compose paths, compose up -d, wait until api logs Application startup complete, then nginx -s reload.
- Air-gap env:
MARKETPLACE_ENABLED=false, CHECK_UPDATE_URL= empty. Point models at an internal OpenAI-compatible URL.
Low-RAM targets: drop SERVER_WORKER_AMOUNT, CELERY_MAX_WORKERS, POSTGRES_SHARED_BUFFERS — those often live in compose command: / listed env, not only .env.
Reboot / nested VM
Start dockerd if needed, confirm storage-driver, compose up -d, GET /console/api/setup.
Upgrade (pin a GitHub tag + digest, not :latest / main)
Heavily customized compose (custom vector image, NO_PROXY, loop/time caps, upload limits) must not be replaced with the official file. Merge new keys into yours.
- Read release notes (1.17:
EDITION → DEPLOYMENT_EDITION; keep COMMUNITY. Leaving a leftover EDITION= in .env is fine).
- Backup:
pg_dump -Fc both dify and dify_plugin, plus .env, compose, nginx, volumes/app/storage, plugin_packages/. Write a rollback note before you migrate.
- Record current image tags and digests (
docker inspect --format '{{.RepoDigests}}'). Pull the new tag (or name@sha256:… through a mirror). Never un-pinned :latest.
- Merge new env keys. Do not turn on
WORKFLOW_LOG_CLEANUP_ENABLED or ENABLE_CONVERSATION_CLEANUP_TASK unless the operator asked — 1.17 ships them off; flipping them deletes run history.
- Prefer rolling recreate, not a stack bounce:
compose up -d --no-deps --force-recreate <svc>. Recreate api first (Alembic). Workers log Running migrations for 1–2 minutes — do not kill them. Then websocket / worker / beat / web / plugin_daemon / agent_*. plugin-daemon tag is independent of api/web; bump it only when the notes say the old daemon breaks model plugins.
- Never
compose down -v. compose down (no -v) is a last resort; volumes stay, but you still lose in-flight runs.
- After recreating api/web:
nginx -s reload. Do not reload nginx while api_websocket is down (host not found in upstream → whole console 502).
- Web 1.17 SSR needs
SERVER_CONSOLE_API_URL=http://api:5001 (container DNS). Leave CONSOLE_API_URL / APP_API_URL empty for the browser.
- Verify:
/console/api/setup, / (unauth 307 to signin is OK), /socket.io/ not 308, login, provider decrypt, plugin list + local runtime ready, one dataset hit-test, one published /v1 run (draft-only does not prove the product).
Cross major versions: do not skip (1.16 → last 1.x → 2.x). Plugins may not load.
Rollback
If Alembic already ran, swapping images back is not enough.
compose stop api/websocket/worker/beat/web/plugin_daemon/agent_* (not down -v).
dropdb / createdb dify and dify_plugin, pg_restore both dumps.
- Restore the old compose +
.env.
up -d --no-deps those app containers on the old image ids.
nginx -s reload.
Mixing new images with an old schema (or the reverse) breaks the console.
1---2name: dify-backup-and-upgrade3description: Use this when backing up, restoring, or upgrading self-hosted Dify, or bringing it back after a host reboot.4---5# Dify backup and upgrade67Use this when backing up, restoring, upgrading, or moving a self-hosted Dify (including air-gap). Do not `compose down -v`.89## What must move together1011Offline copies that "only brought images + compose" fail. Pack **all** of:1213| Piece | Typical path | If missing |14|---|---|---|15| Images | `docker save` matching compose tags | cannot start |16| Compose + nginx | `docker-compose.yaml`, `nginx/` | cannot orchestrate |17| `.env` | includes **`SECRET_KEY`** | model/tool credentials will not decrypt |18| Postgres `dify` | volume or `pg_dump -Fc` | apps/workflows/datasets gone |19| Postgres `dify_plugin` | same | plugin install records gone |20| `volumes/plugin_daemon/plugin_packages/` | `.difypkg` blobs | plugins vanish |21| `volumes/plugin_daemon/cwd/` | plugin venvs | plugins reinstall from scratch |22| Vector store | weaviate / milvus volumes | RAG empty |23| App storage | `volumes/app/storage` | uploads gone |2425`SECRET_KEY` must stay **byte-identical**. Changing it logs everyone out and blank-decrypts providers. Plugin-daemon / sandbox **tags may not match** the api/web tag — copy whatever compose currently pins.2627## Postgres dump (prefer this over copying `volumes/db` live)2829```bash30docker compose exec -T db_postgres pg_dump -U postgres -d dify -Fc > dify.dump31docker compose exec -T db_postgres pg_dump -U postgres -d dify_plugin -Fc > dify_plugin.dump32pg_restore --list dify.dump | head33```3435## Air-gap pack / restore36371. `compose stop` api worker worker_beat web plugin_daemon (keep db up for dump, or stop all for volume tar).382. `docker save` every image in compose (api, web, plugin_daemon, sandbox, agent_*, nginx, postgres, redis, vector, minio/etcd/squid if present).393. Tar `volumes/plugin_daemon` `volumes/app` vector volumes redis as needed.404. Copy compose + `.env` + nginx.415. Target: `docker load`, lay volumes at the **same relative compose paths**, `compose up -d`, wait until api logs `Application startup complete`, then `nginx -s reload`.426. Air-gap env: `MARKETPLACE_ENABLED=false`, `CHECK_UPDATE_URL=` empty. Point models at an internal OpenAI-compatible URL.4344Low-RAM targets: drop `SERVER_WORKER_AMOUNT`, `CELERY_MAX_WORKERS`, `POSTGRES_SHARED_BUFFERS` — those often live in compose `command:` / listed env, not only `.env`.4546## Reboot / nested VM4748Start `dockerd` if needed, confirm storage-driver, `compose up -d`, `GET /console/api/setup`.4950## Upgrade (pin a GitHub tag + digest, not `:latest` / `main`)5152Heavily customized compose (custom vector image, `NO_PROXY`, loop/time caps, upload limits) must **not** be replaced with the official file. Merge new keys into yours.53541. Read release notes (1.17: `EDITION` → `DEPLOYMENT_EDITION`; keep `COMMUNITY`. Leaving a leftover `EDITION=` in `.env` is fine).552. Backup: `pg_dump -Fc` **both** `dify` and `dify_plugin`, plus `.env`, compose, nginx, `volumes/app/storage`, `plugin_packages/`. Write a rollback note **before** you migrate.563. Record current image **tags and digests** (`docker inspect --format '{{.RepoDigests}}'`). Pull the new tag (or `name@sha256:…` through a mirror). Never un-pinned `:latest`.574. Merge new env keys. Do **not** turn on `WORKFLOW_LOG_CLEANUP_ENABLED` or `ENABLE_CONVERSATION_CLEANUP_TASK` unless the operator asked — 1.17 ships them off; flipping them deletes run history.585. Prefer rolling recreate, not a stack bounce: `compose up -d --no-deps --force-recreate <svc>`. Recreate **api first** (Alembic). Workers log `Running migrations` for 1–2 minutes — do not kill them. Then websocket / worker / beat / web / plugin_daemon / agent_*. `plugin-daemon` tag is **independent** of api/web; bump it only when the notes say the old daemon breaks model plugins.596. Never `compose down -v`. `compose down` (no `-v`) is a last resort; volumes stay, but you still lose in-flight runs.607. After recreating api/web: `nginx -s reload`. Do **not** reload nginx while `api_websocket` is down (`host not found in upstream` → whole console 502).618. Web 1.17 SSR needs `SERVER_CONSOLE_API_URL=http://api:5001` (container DNS). Leave `CONSOLE_API_URL` / `APP_API_URL` empty for the browser.629. Verify: `/console/api/setup`, `/` (unauth 307 to signin is OK), `/socket.io/` not 308, login, provider decrypt, plugin list + `local runtime ready`, one dataset hit-test, one **published** `/v1` run (draft-only does not prove the product).6364Cross major versions: do not skip (1.16 → last 1.x → 2.x). Plugins may not load.6566## Rollback6768If Alembic already ran, swapping images back is **not** enough.69701. `compose stop` api/websocket/worker/beat/web/plugin_daemon/agent_* (not `down -v`).712. `dropdb` / `createdb` `dify` and `dify_plugin`, `pg_restore` both dumps.723. Restore the old compose + `.env`.734. `up -d --no-deps` those app containers on the **old** image ids.745. `nginx -s reload`.7576Mixing new images with an old schema (or the reverse) breaks the console.