Major application upgrades are rarely just a matter of changing one image tag. Our Immich upgrade from v2.7.5 to v3.1.0 was a useful example: the application change also required a PostgreSQL vector-extension migration, a database rewrite, new containers, careful backups, and patience while the search indexes were rebuilt.
The upgrade finished successfully, but the service looked unhealthy for a long time in the middle. This is the part that is easy to misdiagnose. A restarting API container does not always mean that the migration is broken; sometimes PostgreSQL is doing exactly the expensive work the migration requires.
The starting point
The deployment was running Immich v2.7.5 with the pgvector.rs-era PostgreSQL image. Photo uploads and the external library were stored separately from the PostgreSQL data directory, while Docker Compose and Portainer managed the application services. That separation matters: the database contains Immich’s metadata, albums, faces, and search information, but it is not a backup of the original media.
The upgrade path
- Review the Immich v3 upgrade and PostgreSQL migration notes before touching the running stack.
- Create a persistent pre-upgrade database dump and save a copy of the Compose definition alongside it.
- Confirm that the database is on local disk and that the media paths, external library, and database mounts are understood.
- Move the application and machine-learning images to the same v3.1.0 release.
- Replace the old pgvector.rs database image with the Immich PostgreSQL image that supports VectorChord and pgvector, including the recommended shared-memory setting.
- Recreate the services without deleting volumes or bind-mounted data.
- Allow the database migration to finish. Monitor logs and PostgreSQL activity instead of repeatedly restarting the server.
- Verify container health, database extensions, local API access, public API access, and the Immich background jobs.
Immich’s documentation explains that pgvector.rs support was dropped in the v3 release line. The migration path temporarily needs compatibility with the old and new vector extensions while existing vector data is converted and indexes are rebuilt. The database image used for this upgrade exposed VectorChord 0.4.3 and pgvector 0.8.1 after migration, while the older vectors extension remained available during the transition.
Hurdle one: containers with conflicting names
The first deployment attempt hit Docker container-name conflicts. The old stack had containers with names that did not line up cleanly with the Compose project labels, so a normal docker compose up could not claim every name. The safe fix was to remove only the stale application containers that were being replaced, leave all volumes and media mounts intact, and then recreate the stack with the intended project and Compose file.
This is a good reminder that docker compose down --volumes is not a routine upgrade command for a stateful application. Removing containers is one thing; removing PostgreSQL, server-data, or media volumes is a recovery event.
Hurdle two: the migration looked stuck
After the new containers started, PostgreSQL was healthy but the Immich server repeatedly failed its health check for an extended period. The logs showed VectorChord setup followed by reindexing of the face and clip indexes. PostgreSQL also showed active table alterations and vacuum work. Host monitoring showed very high I/O wait, which was evidence that the database was busy rather than idle.
The important log message was that reindexing could take a while and the server should not be restarted. Online Immich discussions describe the same pattern: the server can remain unhealthy while the vector indexes are rebuilt, and repeated restarts can prolong or interfere with the process. We therefore watched the database activity, checked that restart counts were not increasing continuously, and let the migration run.
Verification after the migration
- The Immich server, machine-learning service, PostgreSQL, and Redis containers became healthy.
- The server logged a successful v3.1.0 microservices startup.
- The database reported VectorChord 0.4.3, pgvector 0.8.1, and the compatibility vectors extension.
- The local Immich API health endpoint returned HTTP 200.
- The public HTTPS API health endpoint also returned HTTP 200.
- Face-search and smart-search migration work completed without repeated migration errors.
One server restart was recorded during recovery, but there was no continuing restart loop after the migration settled. A healthy Docker status alone was not considered sufficient; the API checks and application startup logs provided the stronger confirmation.
What we would do next time
- Schedule the migration with enough time for a large database rewrite and index rebuild.
- Take a persistent database dump before changing the image tags, and keep the Compose definition with the dump.
- Record the old image names, extension versions, mount paths, and container names before the change.
- Expect the API to be unavailable during the expensive part of the migration.
- Use PostgreSQL activity, disk I/O, logs, and restart counts to distinguish progress from a crash loop.
- Do not repeatedly restart a server while Immich reports that vector reindexing is in progress.
- After the upgrade, test both local and public access and inspect failed library, thumbnail, face, and smart-search jobs.
- Complete an independent 3-2-1 backup and restore test. A database dump by itself is not an Immich backup.
Useful references
- Immich upgrade guide
- Immich PostgreSQL and VectorChord migration notes
- Immich backup and restore guidance
- Community report: unhealthy server during vector index rebuilding
- Community report: VectorChord and vectors extension compatibility
The main lesson is simple: plan this as a database migration, not a normal container refresh. Back up the database and media, preserve the data mounts, expect a long quiet-looking period, and verify the service from the API after the database work has finished.