> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dify.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Weaviate Server Upgrade Path

> How to move a self-hosted Weaviate server across minor versions without losing vector data

Dify 1.17.1 updates its bundled Weaviate server from `1.27.0` to `1.39.2`, and an existing Weaviate data volume cannot cross that gap in one step. This procedure covers Dify's default, single-node Docker Compose deployment. Fresh deployments start with the new version on an empty volume and do not need this procedure.

Weaviate neither tests nor supports skipping minor versions, and any release may carry an on-disk migration that expects the previous version to have run at least once.

The supported path is to step through every minor release in order, landing on the latest patch of each. Below is that ladder, along with the two operational details that most often go wrong along the way.

<Warning>
  Back up your Weaviate volume before you start. An upgrade that goes wrong partway through the ladder is only recoverable if you have a snapshot to return to.
</Warning>

## Check Your Current Version

From the Dify repository root, enter the `docker` directory and keep the shell there. The default `docker-compose.yaml` does not publish a host port for Weaviate, so run the check inside the Compose network rather than against `localhost` on your host:

```bash theme={null}
cd docker
docker compose exec -T weaviate \
  wget -qO- --header="Authorization: Bearer <WEAVIATE_API_KEY>" \
  http://localhost:8080/v1/meta \
  | python3 -c "import sys, json; print(json.load(sys.stdin)['version'])"
```

The `localhost` in the URL is the Weaviate container's own loopback, not your host's. The Weaviate image has no `curl` and no Python, so the request uses busybox `wget` and the JSON is formatted by Python on your host.

If this reports `1.39.2`, you are done. Continue below from `1.27.0`; if it reports an intermediate version in the ladder, resume with the next rung. For a version earlier than `1.27.0`, follow the [Weaviate v4 migration guide](/en/self-host/deploy/troubleshooting/weaviate-v4-migration) first.

## Back Up Your Data

For a Docker deployment, the simplest backup is an offline copy of the volume. First stop Dify's request and worker services, and keep them stopped until the final rung passes:

If your `docker-compose.yaml` does not define `api_websocket`, omit it from the command below.

```bash theme={null}
docker compose stop -t 120 nginx api api_websocket worker worker_beat || exit 1
```

Run [Verify Each Step](#verify-each-step) once to record the starting object count and confirm synchronization. Then stop Weaviate and copy the volume:

```bash theme={null}
docker compose stop -t -1 weaviate || exit 1
sudo cp -a ./volumes/weaviate ./volumes/weaviate_backup_$(date +%Y%m%d) || exit 1
```

`cp -a` preserves ownership, permissions and timestamps. A plain `cp -r` run under `sudo` rewrites them, which can leave the restored volume unreadable by the Weaviate container.

In production, prefer Weaviate's [backup module](https://docs.weaviate.io/deploy/configuration/backups) writing to S3, GCS, or Azure. It produces a restorable snapshot rather than a raw directory copy, and it can run against a live instance. The local filesystem backend is for development only.

## Upgrade One Minor at a Time

Move through every minor release in order, always landing on that minor's latest patch.

| Step        | Image tag                                          |
| :---------- | :------------------------------------------------- |
| 0 (current) | `semitechnologies/weaviate:1.27.0`                 |
| 1           | `cr.weaviate.io/semitechnologies/weaviate:1.27.27` |
| 2           | `cr.weaviate.io/semitechnologies/weaviate:1.28.16` |
| 3           | `cr.weaviate.io/semitechnologies/weaviate:1.29.11` |
| 4           | `cr.weaviate.io/semitechnologies/weaviate:1.30.23` |
| 5           | `cr.weaviate.io/semitechnologies/weaviate:1.31.22` |
| 6           | `cr.weaviate.io/semitechnologies/weaviate:1.32.27` |
| 7           | `cr.weaviate.io/semitechnologies/weaviate:1.33.18` |
| 8           | `cr.weaviate.io/semitechnologies/weaviate:1.34.20` |
| 9           | `cr.weaviate.io/semitechnologies/weaviate:1.35.23` |
| 10          | `cr.weaviate.io/semitechnologies/weaviate:1.36.23` |
| 11          | `cr.weaviate.io/semitechnologies/weaviate:1.37.16` |
| 12          | `cr.weaviate.io/semitechnologies/weaviate:1.38.14` |
| 13          | `cr.weaviate.io/semitechnologies/weaviate:1.39.2`  |

The registry changes at step 1. Existing deployments pull `semitechnologies/weaviate` from Docker Hub; the ladder uses `cr.weaviate.io/semitechnologies/weaviate`, Weaviate's own registry, which is what Dify's updated Compose file points at. The images are the same, so you are changing both registry and version as one deliberate move.

Patch versions were current at the time of writing. For the intermediate rungs, check for a newer patch of that minor before you start and prefer it.

<Warning>
  The final rung is the exception. Land on the version your `docker-compose.yaml` pins, not the newest patch available. If you take the volume to a newer patch and then apply Dify's pin, you have downgraded onto a volume the newer binary already wrote to—the case the [rollback warning](#roll-back-a-failed-step) covers.
</Warning>

Stepping one minor at a time also limits your blast radius. When a rung misbehaves, you roll back a single version and know exactly which release caused it, instead of bisecting a jump that crossed every minor at once.

<Info>
  Staying close to the newest release keeps you in the fix window. Weaviate supports the [three most recent minor versions](https://weaviate.io/weaviate-eol-policy)—the current one and the two before it. Anything older is end of life and has no claim on bug or security fixes. Revisit periodically after this migration and move to the current patch.
</Info>

## Upgrade with Docker Compose

Dify pins the Weaviate image in `docker/docker-compose.yaml`, `docker/docker-compose.middleware.yaml`, and `docker/docker-compose-template.yaml`. Edit the file you deploy with, then repeat the loop below for each rung.

<Steps>
  <Step title="Set the Next Image Tag">
    Change the `weaviate` service to the next minor's latest patch, for example `image: cr.weaviate.io/semitechnologies/weaviate:1.28.16`.
  </Step>

  <Step title="Stop the Container Gracefully">
    ```bash theme={null}
    docker compose stop -t -1 weaviate || exit 1
    ```

    Never use `docker kill` or `docker rm -f` here. See [Stop Weaviate Gracefully](#stop-weaviate-gracefully) for what a hard kill costs you.
  </Step>

  <Step title="Start the New Version">
    ```bash theme={null}
    docker compose up -d weaviate
    ```
  </Step>

  <Step title="Verify Before Moving On">
    Run [Verify Each Step](#verify-each-step) before continuing.
  </Step>
</Steps>

If you run Weaviate through `docker-compose.middleware.yaml` while developing from source, the same loop applies with the file named explicitly:

```bash theme={null}
docker compose -f docker-compose.middleware.yaml stop -t -1 weaviate || exit 1
docker compose -f docker-compose.middleware.yaml --profile weaviate up -d weaviate
```

## Stop Weaviate Gracefully

This is the single detail most likely to cost you working search, and it is easy to get wrong.

Weaviate holds its HNSW vector index in memory and records it to disk through a commit log. When the container is hard-killed—`docker kill`, `docker rm -f`, an out-of-memory kill, or a stop that exceeds Docker's grace period—that commit log can be left incomplete, and objects it had not yet recorded end up missing from the graph.

Your objects are not lost. The graph is. Testing this on 1.39.2, a hard kill immediately after importing 500 objects into a 700-object collection left it like this:

| Check                           | Result                         |
| :------------------------------ | :----------------------------- |
| Object count                    | 700, correct                   |
| Listing all objects             | returns all 700                |
| Keyword (BM25) search           | finds all 500 imported objects |
| Fetch by ID                     | returns exact vectors          |
| `near_vector` with `limit: 700` | **680**                        |

Twenty objects sat in the store, fully intact, and vector search could not reach them.

What makes this dangerous is that nothing announces it. The server logged no error, no warning, and nothing at all about the commit log—there is no line to grep for. And the object count, the check most people reach for first, reads perfectly healthy.

It also does not heal on its own. The same twenty stayed missing after the restart, after a graceful stop and start, after a second one, and after re-writing the same objects. To repair it, re-index the affected knowledge base in Dify so the collection is built again from scratch.

So prevention is the whole game here. Use `docker compose stop` or `docker compose down`, both of which send SIGTERM, and let the process finish flushing. A timeout of `-1` waits indefinitely instead of falling back to SIGKILL:

```bash theme={null}
docker compose stop -t -1 weaviate || exit 1
```

To check whether it already happened, compare the object count against what vector search can actually reach. [Verify Each Step](#verify-each-step) gives you the count; run a `near_vector` query with `limit` set to that number. Fewer results back means the difference is your unreachable objects.

## Wait for the Index to Mount After Each Restart

`/v1/meta` starts answering about two seconds after the container starts, on every version on the ladder. Your collections take longer to mount, and from 1.31 onward they take much longer—measured across the whole ladder, the gap between `/v1/meta` answering and the first successful collection query is:

| Server version | Gap before queries work |
| :------------- | :---------------------- |
| 1.27 - 1.30    | under 0.3s              |
| 1.31 and later | 4 - 9s                  |

A query fired inside that window does not report "still starting". It reports that your data is not there:

* `GET /v1/objects?class=...` returns **404** with an empty body.
* A GraphQL query returns **422** with `no graphql provider present, this is most likely because no schema is present. Import a schema first!`

That is alarming to read halfway through an upgrade, and it is a false alarm. The window is transient and needs no action beyond waiting. Give each restart ten seconds before you conclude anything, and if you script health checks, poll until a real collection query succeeds rather than trusting `/v1/meta` alone.

## Verify Each Step

Confirm that data survived and that search works. A server that boots is not evidence of a healthy index.

Set your key and collection name once, then run the four checks. Dify replaces every hyphen in the knowledge base ID with an underscore when it names the collection, so copy the exact name from the second check rather than building it by hand:

```bash theme={null}
KEY="<WEAVIATE_API_KEY>"
# Knowledge base ID from its Dify URL (/datasets/<id>/documents), hyphens as underscores
COLLECTION="Vector_index_9f4e2b7a_1c3d_4e5f_8a9b_0c1d2e3f4a5b_Node"

# Reported server version
docker compose exec -T weaviate \
  wget -qO- --header="Authorization: Bearer $KEY" http://localhost:8080/v1/meta \
  | python3 -c "import sys, json; print('version', json.load(sys.stdin)['version'])"

# Collections are present
docker compose exec -T weaviate \
  wget -qO- --header="Authorization: Bearer $KEY" http://localhost:8080/v1/schema \
  | python3 -c "import sys, json; print([c['class'] for c in json.load(sys.stdin)['classes']])"

# Object count for one collection
docker compose exec -T weaviate \
  wget -qO- --header="Authorization: Bearer $KEY" \
  --header="Content-Type: application/json" \
  --post-data="{\"query\":\"{ Aggregate { $COLLECTION { meta { count } } } }\"}" \
  http://localhost:8080/v1/graphql \
  | python3 -c "import sys, json; print('count', json.load(sys.stdin)['data']['Aggregate']['$COLLECTION'][0]['meta']['count'])"

# Single-node metadata is synchronized
docker compose exec -T weaviate \
  wget -qO- --header="Authorization: Bearer $KEY" \
  http://localhost:8080/v1/cluster/statistics \
  | python3 -c "import sys, json; d=json.load(sys.stdin); assert d.get('synchronized') is True and len(d.get('statistics', [])) == 1; print('synchronized')"
```

Continue only if the reported version matches the current rung, the object count is unchanged, and the synchronization check succeeds.

<Warning>
  Do not use `GET /v1/objects?class=...&limit=1` to count objects. Its `totalResults` field reports the size of the page it returned, not the size of the collection, so with `limit=1` it always prints `1`. Use the `Aggregate` query above.
</Warning>

After `1.39.2` passes, run `docker compose stop -t -1 weaviate || exit 1`. Revert only the temporary image edit without running Compose, then follow the [Docker Compose upgrade instructions](/en/self-host/deploy/quick-start/docker-compose#upgrade) for Dify 1.17.1. Run a test retrieval before returning the deployment to service; getting chunks back confirms the vector index survived, which the object count alone does not tell you.

## Roll Back a Failed Step

Before starting Dify 1.17.1, set the Weaviate image back to `semitechnologies/weaviate:1.27.0`, then restore the initial backup:

```bash theme={null}
docker compose stop -t -1 weaviate || exit 1
test -d ./volumes/weaviate_backup_YYYYMMDD || exit 1
sudo mv ./volumes/weaviate ./volumes/weaviate_failed_$(date +%Y%m%dT%H%M%S) || exit 1
sudo cp -a ./volumes/weaviate_backup_YYYYMMDD ./volumes/weaviate || exit 1
docker compose up -d weaviate
```

<Warning>
  Rolling the image tag back without restoring the data is not safe. Once a newer version has written to the volume, an older binary may not read it.
</Warning>

Verify Weaviate, then start the original Dify stack and run a test retrieval.

## What Does Not Change

Your collections keep their existing layout, so no re-indexing is required and your knowledge bases keep working as they are. Dify stores each dataset as a `Vector_index_<dataset_id>_Node` collection, with the ID's hyphens replaced by underscores, and a self-provided named vector `default`. Dify does not set a distance metric, so the collection uses the server default of cosine. That structure is identical before and after the upgrade.

The Python `weaviate-client` also needs no attention. Use the version your Dify release pins: `4.20.5` for Dify 1.14.0 through 1.16.1, and `4.22.0` for 1.17.0 through 1.17.1. Both work against every server version on the ladder, so you can upgrade the server independently of Dify.

<Info>
  Running a Weaviate server older than 1.27, or moving from client v3 to v4? Start with the [Weaviate v4 migration guide](/en/self-host/deploy/troubleshooting/weaviate-v4-migration), which covers 1.19.0 through 1.26.x and the schema migration up to 1.27, then return here.
</Info>
