How to Upgrade GLPI on Docker Across Versions

A production runbook to upgrade GLPI on Docker across minor and major versions: decision matrix, consistent backup, schema migration, rehearsed rollback and the real pitfalls (lost config_db, OPcache, incompatible plugin) that cause downtime.

Upgrading GLPI on Docker looks trivial - swap the image tag and bring the container back up - but that apparent simplicity is exactly what produces unplanned downtime. Maintaining client environments, most upgrade incidents don't come from the database migration itself, but from incompatible plugins, the PHP bundled in the new image, or a mis-mapped volume that makes GLPI lose its database configuration. This guide consolidates the runbook we apply in production, with the points where things actually break.

Before touching any container: the compatibility inventory

A GLPI upgrade is not just the image. Three layers have to move together: the core, the plugins and the database schema. The classic mistake is upgrading the image only to discover, with the site down, that a critical plugin (native inventory, an SSO, a custom field) has no release for the new core. Check this first:

  • Exact source and target versions - a minor jump (10.0.15 to 10.0.18) is safe; a major jump (10.x to 11.x) changes the PHP requirement and may retire plugins.
  • Plugin matrix - for every installed plugin, confirm a release compatible with the target exists. A plugin with no compatible version is a business decision (disable, replace or postpone the upgrade), never a 2 a.m. surprise.
  • PHP bundled in the image - the official image ships the right PHP, but if you use your own image (Alpine + php-fpm), GLPI 11 requires PHP 8.2+. A new core on old PHP gives a blank page with no clear log.
  • Disk space - the dump and the volume tar can temporarily double usage. Check before, not during.

Decision matrix: minor, major or skipping versions

The type of jump defines the runbook. This is the matrix we use to size the window and the risk:

ScenarioExampleRiskTypical windowMain concern
Patch/minor10.0.15 → 10.0.18Low5 to 15 minLight schema migration; plugins usually follow
Major10.0.x → 11.0.xHigh30 to 90 minPHP, plugins and behavior changes; stage first
Skipping majors9.5.x → 11.0.xVery highPlan in stagesDo NOT skip; migrate 9.5 → 10.0 → 11.0 in hops

The common mistake here is thinking that skipping versions saves time. GLPI does not guarantee schema migration across a whole major; the db:update routine expects the structure of the immediately previous version. Skipping 9.5 straight to 11 usually stalls the migration halfway, with the database in an inconsistent state - and then rollback stops being optional.

Step by step upgrade

1. Consistent backup (database + volumes)

A backup is not the dump alone. It is the database dump plus the tar of the file volumes (uploads, documents, plugin cache). Both must be from the same instant - which is why we stop the application first, keeping only the database up for the dump:

# Stop the app, keep the database running
docker compose stop glpi-app

# Database dump (dedicated user, password via environment variable)
docker exec glpi-db sh -c 'mysqldump --single-transaction \
  -u glpi -p"${MYSQL_PASSWORD}" glpi' | gzip > /backup/pre-update-$(date +%F).sql.gz

# Tar of the GLPI file volumes
tar -czf /backup/pre-update-files-$(date +%F).tar.gz \
  /var/lib/docker/volumes/glpi_files/_data

--single-transaction produces a coherent dump without locking the InnoDB tables. And never put the literal password on the command line: it leaks into the shell history and into docker inspect.

2. Pin the target version in the compose file

Never use the latest tag in production. It moves the target without warning and prevents a deterministic rollback. Pin the exact version:

# docker-compose.yml
services:
  glpi-app:
    # From:
    image: glpi/glpi:10.0.18
    # To (exact target version, never :latest):
    image: glpi/glpi:11.0.1
    volumes:
      - glpi_files:/var/glpi/files
      - glpi_config:/var/glpi/config   # config_db.php lives here - do NOT lose it

3. Bring up the new image

docker compose pull glpi-app
docker compose up -d glpi-app

4. Run the schema migration

The migration command was renamed between GLPI generations. Confirm which binary is available before running blind:

# GLPI 10/11 (Symfony console):
docker exec -u www-data glpi-app php bin/console db:update --no-interaction

# Older GLPI (early 10.0) used:
# docker exec -u www-data glpi-app php bin/console glpi:database:update --no-interaction

Always run as the web server user (www-data or apache), never as root. Running the migration as root creates cache files with the wrong owner, and GLPI then throws permission errors - a false "the upgrade broke it" that is just ownership.

5. Clear cache and validate

docker exec -u www-data glpi-app php bin/console cache:clear
  • Open the interface and check the version under Setup > General.
  • Run the built-in diagnostic under Setup > Checks (PHP, extensions and permissions).
  • Confirm the critical plugins show as active, not as "needs update".
  • Test a real flow: open a ticket, attach a document, trigger a notification.

What we learned operating this for clients

In maintenance, the incident that repeats the most is not the migration failing - it is the migration appearing to succeed while GLPI comes up with an empty default database. The cause is almost always the same: the volume holding config/config_db.php was not mapped, or was mapped to a different path in the new version. GLPI can't find the database configuration, assumes it's a clean install and shows the installation wizard. Someone in a hurry clicks "install" and creates a new database on top - now, on top of the downtime, there's a data loss risk. That is why, in our checklist, the first post-boot validation is not the version: it is docker exec glpi-app cat /var/glpi/config/config_db.php returning the client's real configuration. If the wizard shows up, we stop everything before any click and review the volume mapping - we never proceed through the interface.

The second pattern is OPcache serving old code after the upgrade. The image changed, the code on disk is new, but php-fpm still has the old version compiled in memory. Symptom: undefined method errors or a core version that doesn't match the image. Recreating the container (not just restarting it) fixes it, because the php-fpm process is reinitialized from scratch.

Rollback plan (rehearsed, not improvised)

A rollback is only reliable if it was thought through beforehand. With the dump and the tar from step 1, the return is deterministic:

# Tear down the new version
docker compose down

# Restore the database from the dump
gunzip < /backup/pre-update-2026-07-02.sql.gz \
  | docker exec -i glpi-db sh -c 'mysql -u glpi -p"${MYSQL_PASSWORD}" glpi'

# Revert the image tag in docker-compose.yml to the previous version
# (e.g. glpi/glpi:11.0.1 back to glpi/glpi:10.0.18)

docker compose up -d

The critical point of the rollback is the schema. Once db:update altered the structure, the database is no longer compatible with the old image. That is why the rollback has to restore the dump - reverting the tag is not enough. Restoring only the image, with the schema already migrated, leaves the old GLPI reading tables of a future structure, which produces silent, hard-to-trace errors.

Doing this without unplanned downtime

If the environment is critical, the window must be rehearsed in staging with a clone of the production database. That is where you find out, without pressure, whether a plugin breaks or whether the migration takes longer than the agreed window. Upgrading straight to production without that rehearsal is betting the client's availability on luck.

If your team has no window to rehearse each upgrade, or has already been burned by an update that turned into an all-nighter, NexTool handles the maintenance and upgrades of your GLPI with a rehearsed rollback and an agreed window. Talk to us about GLPI support and maintenance.


This content was produced with the assistance of artificial intelligence and reviewed by the Nextool Solutions team.

Frequently Asked Questions

Stop the application (keeping the database up), take the dump with --single-transaction and the tar of the file volumes at the same instant, pin the exact image version in docker-compose.yml (never :latest), bring the container up and run php bin/console db:update --no-interaction as www-data. Data loss almost always comes from a mis-mapped volume, not from the migration itself.

No. The db:update routine expects the structure of the immediately previous version. Skipping a whole major stalls the migration halfway and leaves the database inconsistent. The safe path is to migrate in hops: 9.5 to 10.0, validate, then 10.0 to 11.0.

It depends on the jump. A patch or minor (e.g. 10.0.15 to 10.0.18) usually takes 5 to 15 minutes. A major upgrade (10.x to 11.x) takes 30 to 90 minutes including backup, migration and validation, and should be rehearsed in staging first.

The container did not find config/config_db.php - usually because the volume holding the database configuration was not mapped or changed path in the new image. Do not click install (that creates an empty database on top). Stop everything, fix the config volume mapping and bring it up again.

Tear down the containers, restore the pre-upgrade database dump and revert the image tag in docker-compose.yml to the previous version. Restoring the dump is mandatory: once db:update changed the schema, the old image is no longer compatible with the migrated structure, so just reverting the tag causes silent errors.

No, the opposite. Always run as the web server user (www-data or apache). Running db:update as root creates cache files with the wrong owner and GLPI then throws permission errors - a false 'the upgrade broke it' alarm that is just ownership.

Need help?