上个月我把生产环境的GLPI 10系列从容器化部署升级到了GLPI 11系列整个过程比想象中要复杂。很多朋友的第一反应是把镜像tag从glpi/glpi:10.0.x改成glpi/glpi:11.x不就行了如果真这么操作大概率会撞上数据库迁移中断、插件被禁用、甚至升级后白屏只能回滚的尴尬。这篇文章基于我在Docker Compose环境下的完整升级实践把从准备、备份、迁移到踩坑排查、验证回滚的每个环节都拆开来讲也补充了K8s环境的差异点适合正在维护GLPI 10容器实例、准备升级11的运维和集成小伙伴。1. GLPI 11到底改了哪些底层直接影响你的容器方案先把升级动机放一边我们必须先搞清楚一个关键问题GLPI 11和GLPI 10之间除了界面变了底层还动了什么因为容器化部署的核心优势是“镜像即环境”而镜像里面的PHP版本、扩展、文件布局、运行进程一旦变化现有编排文件里的挂载卷、环境变量、健康检查全都可能要跟着调整。1.1 版本变化清单从GLPI 10到GLPI 11官方给出的技术基线变化非常有代表性。对比项GLPI 10.xGLPI 11.xPHP版本要求PHP 7.4官方镜像常用8.1/8.2PHP 8.2数据库基线MariaDB 10.6 / MySQL 5.7MariaDB 10.11 / MySQL 8.0Symfony框架5.4左右6.4甚至更高前端技术仍以jQuery/Sass为主逐步迁移到Vue和现代化组件控制台命令已有Symfony Console命令命令更多结构变化数据库迁移机制Doctrine Migrations持续演进新增多个迁移版本如果你是直接使用官方glpi/glpi镜像PHP版本这些问题通常不需要你操心因为镜像已经装好对应版本和扩展。但如果你在原有自定义镜像上做过二次打包比如自行安装了PHP扩展、修改过php.ini、把Apache换成了NginxFPM那这次升级就等于一次“基础运行时替换”必须重新验证镜像构建过程。1.2 容器运行时层面的差异GLPI 10官方容器里数据路径是经典的/var/www/html里面分files、config、plugins等目录。GLPI 11继续沿用了这个布局但有几个容易踩的细节镜像内Web服务器用户仍然是www-dataUID 33但升级过程中需要写入大量缓存文件和临时文件如果原有挂载卷的权限是通过chmod -R 777勉强维持的升级时很可能直接白屏。PHP 8.2之后一些老PHP扩展比如旧的mysql扩展早已移除PHP 8.4进一步收紧deprecation warning会被逐渐清理。第三方插件如果在代码里使用过时函数升级到GLPI 11后运行时会报错甚至导致整个页面500。官方镜像的定时任务cron是通过容器内常驻进程拉起的GLPI 11调整了部分任务脚本路径升级后必须确认容器内cron进程还在跑否则升级完成后资产同步、工单通知这类计划任务全部失效。所以这次升级的本质不是“换个tag”而是“换运行时、换数据库结构、换插件兼容层”三件事同时发生。理解了这一点下面所有步骤才有意义。2. 动手前的环境盘点与备份这一步决定回滚是否有效我在实际操作中见过太多人跳过盘点步骤直接改镜像tag就执行docker compose up -d结果升级失败后连回滚都做不彻底。原因很简单GLPI升级的数据库迁移是不可逆的一旦数据库结构从10变成11你无法只靠“把镜像切回10”来恢复必须用备份数据完整回退。所以备份和盘点不是可有可无而是整个项目的安全底线。2.1 盘点当前版本、镜像和挂载卷开始之前先把环境信息全部记录下来。建议按下面顺序执行# 查看当前容器状态和编排文件 docker compose ps docker compose config # 看当前使用的镜像版本 docker inspect glpi --format {{.Config.Image}} # 看容器内GLPI版本与PHP版本 docker exec glpi php -v docker exec glpi php -r echo GLPI_VERSION . PHP_EOL; 2/dev/null || docker exec glpi cat /var/www/html/version # 查看数据库版本 docker exec glpi-db mariadb --version这一步的核心目的有两个一是确认当前GLPI小版本10.0.x的具体值因为官方数据库迁移脚本是按小版本持续叠加的从10.0.1升和从10.0.18升迁移步数不一样二是确认数据库本身是否已经偏离官方基线太远。挂载卷的盘点同样关键。执行docker inspect glpi | grep -A 20 Mounts确认这几个目录是否都做了持久化/var/www/html/files上传文件、缓存、日志、session都在这丢失最致命/var/www/html/config数据库连接配置和自定义配置/var/www/html/plugins插件本体/var/www/html/marketplace从市场安装的未注册插件很多人只挂载了files和config插件放在容器层里一升级容器重建插件直接没了。这个坑特别隐蔽建议在盘点阶段就补上插件目录的持久化。2.2 文件卷和数据库备份操作备份要分两条线文件卷备份和数据库逻辑备份。文件卷备份我推荐直接在宿主机上借助一个临时alpine容器做避免在原容器内操作污染运行环境# 备份整个/var/www/html下的持久化数据 docker run --rm \ -v glpi_html:/data:ro \ -v $(pwd):/backup \ alpine tar czf /backup/glpi_html_backup_$(date %F).tar.gz \ -C /data . # 单独备份插件目录方便单独恢复 docker run --rm \ -v glpi_plugins:/data:ro \ -v $(pwd):/backup \ alpine tar czf /backup/glpi_plugins_backup_$(date %F).tar.gz \ -C /data .数据库备份必须用逻辑导出不能用简单复制数据目录的方式。因为数据库迁移失败时最容易恢复的是mysqldump出来的SQL文件。执行时注意加--single-transaction避免锁表影响在线业务docker compose exec -T glpi-db sh -c \ mariadb-dump -uroot -p$MARIADB_ROOT_PASSWORD --single-transaction --routines --triggers glpidb \ glpi10_backup_$(date %F).sql备份完成之后至少要确认两件事第一SQL文件不是0字节开头有完整的建表语句第二压缩包大小和原卷大小比例合理明显偏小说明可能有目录没挂载成功。2.3 插件兼容性排查GLPI 11对插件兼容性的检查比10严格很多。官方在插件市场里对每个插件都会标注兼容的GLPI版本区间升级前我建议逐个盘点plugins目录下的插件版本。操作上可以直接查插件目录下的plugin.xmldocker exec glpi sh -c for p in /var/www/html/plugins/*/plugin.xml; do echo $p ; grep -E name|version|glpi|compat $p | head -5; done我这次生产环境有8个第三方插件其中表单插件和PDF导出插件都是直接导致升级界面卡死的元凶。第三个步骤会详细讲这个坑这里先给结论升级GLPI 11之前如果插件没有发布兼容11的版本宁可临时停用也不要带着旧插件直接跑数据库迁移。2.4 启用维护模式并检查配置备份备份做完后我习惯先把GLPI切入维护模式避免用户在升级过程中提交工单或修改资产导致迁移过程中出现并发写入docker compose exec glpi php bin/console glpi:maintenance:enable然后单独备份config目录下最关键的config_db.php这个文件里保存了数据库连接信息。如果在后续升级过程中新版镜像启动时读取不到整个应用会直接掉进安装流程非常危险。docker compose exec glpi cat /var/www/html/config/config_db.php config_db_backup.php到这一步文件备份、数据库备份、插件清单、维护模式都准备好了才算真正具备“随便折腾”的底气。3. 核心迁移镜像切换、数据库升级与缓存重建前面所有准备工作都是为了这一步能平稳执行。这里我以Docker Compose环境为例把完整的迁移链路捋清楚。3.1 修改编排文件并拉取新镜像首先修改docker-compose.yml里的镜像版本。官方镜像的tag从10.0.x切到11.x时不要图省事用latest必须锁定具体小版本号比如glpi/glpi:11.0.1。原因很简单latest标签会在你重新pull时漂移下次重建容器很可能偷偷升级补丁版跨版本兼容性很难保证。services: glpi: image: glpi/glpi:11.0.1 container_name: glpi ports: - 80:80 volumes: - glpi_html:/var/www/html - glpi_files:/var/www/html/files - glpi_plugins:/var/www/html/plugins - glpi_config:/var/www/html/config environment: - MYSQL_HOSTglpi-db - MYSQL_PORT3306 - MYSQL_USERglpi - MYSQL_PASSWORDyour_password - GLPI_ENVIRONMENT_IDproduction depends_on: - glpi-db glpi-db: image: mariadb:10.11 environment: - MARIADB_ROOT_PASSWORD${MARIADB_ROOT_PASSWORD} - MARIADB_DATABASEglpidb - MARIADB_USERglpi - MARIADB_PASSWORDyour_password volumes: - db_data:/var/lib/mysql如果官方镜像无法满足你的定制需求需要在原有Dockerfile基础上重新构建一定要确认基础镜像里的PHP版本不低于8.2且以下扩展已经安装intl、gd、iconv、mbstring、exif、imap、ldap、zip、opcache、curl、dom、xml、json。GLPI 11对intl和gd的依赖尤其严格缺失会导致升级界面直接报错。改完编排文件后先拉镜像但不启动docker compose pull glpi这个阶段我会顺带把新镜像本地跑起来看一眼环境变量和启动日志确认镜像本身能正常起来再切流量docker compose up -d启动后会看到容器状态如果容器反复重启马上docker logs glpi查看原因。常见的启动失败原因包括文件卷权限不对、数据库连接失败、环境变量缺失。这些在日志里通常都能直接看到。3.2 数据库迁移的两种方式新镜像起来后访问GLPI地址时有两种情况一是页面自动跳转到升级向导二是依然显示旧版页面但报数据库结构不一致。无论哪种都需要完成数据库迁移。我个人推荐在容器内用命令行完成迁移原因是在Web界面执行时如果PHP执行时间受限可能迁移到一半报超时。命令行没有这个问题而且日志更清晰# 进容器执行数据库更新命令 docker compose exec glpi php bin/console glpi:database:update --no-interaction如果控制台提示找不到这个命令可以先执行php bin/console list查看当前版本支持的命令名。GLPI 10之后基于Symfony控制台架构数据库迁移相关的命令在不同小版本里名称可能有变化但glpi:database:update这个入口在10到11的升级路径里是官方文档明确提到的。执行过程中控制台会输出一批迁移脚本的执行结果包括新增表、修改字段索引、填充默认数据。这个阶段千万不要中断容器也不要手动去数据库里改表结构。GLPI的Doctrine迁移是有版本记录表的中断后重新执行时会从上次断点继续但如果有人手动改了表结构迁移版本记录就乱了后面排查非常痛苦。如果你更习惯Web方式访问/install/update.php按界面提示一步步走。实际体验上Web方式会把升级前检查列得很清楚包括目录权限、PHP扩展、数据库版本这些检查项能帮你快速定位问题。但最终执行迁移的仍然是同一套迁移脚本所以两种方式本质没有区别区别只在于日志可读性和超时控制。3.3 升级后清理缓存数据库迁移完成后容器内PHP变量缓存、Symfony缓存、twig模板缓存都需要重建否则页面会一直加载旧配置或者直接白屏。清理缓存建议在迁移完成后马上执行docker compose exec glpi php bin/console glpi:cache:clear这个命令会清理files/_cache下的编译产物。清理完再访问首页正常情况下会看到登录页。如果看到空白页不要慌大概率是缓存权限问题第四章节会讲具体的排查方式。3.4 K8s环境下的升级差异如果你的GLPI是跑在Kubernetes里的升级思路类似但要多考虑两个问题副本数量和迁移并发。多副本状态下如果Deployment直接滚动更新所有新Pod会同时尝试执行数据库迁移轻则迁移锁冲突重则多个迁移进程同时改表结构导致数据异常。所以K8s环境下我建议的做法是先把Deployment缩容到0或者至少缩到1个副本保证只有一个GLPI实例在做迁移。用一次性Job执行数据库迁移命令而不是靠Pod启动时的逻辑触发迁移。apiVersion: batch/v1 kind: Job metadata: name: glpi-migration-11 spec: template: spec: restartPolicy: Never containers: - name: glpi-migrate image: registry.local/glpi:11.0.1 command: - php - bin/console - glpi:database:update - --no-interaction envFrom: - configMapRef: name: glpi-config - secretRef: name: glpi-db-secret volumeMounts: - name: glpi-files mountPath: /var/www/html/files - name: glpi-plugins mountPath: /var/www/html/plugins - name: glpi-config mountPath: /var/www/html/config volumes: - name: glpi-files persistentVolumeClaim: claimName: glpi-files-pvc - name: glpi-plugins persistentVolumeClaim: claimName: glpi-plugins-pvc - name: glpi-config persistentVolumeClaim: claimName: glpi-config-pvcJob跑完后查看Pod日志确认迁移成功再更新Deployment的镜像tag恢复副本数。这样能最大程度避免并发迁移带来的不可控问题。4. 我踩过的坑插件兼容、目录权限、迁移中断这次升级过程中我实际遇到了三个比较有价值的问题都是文档里不会自动提醒你的。单独拿出来说是想让大家在操作时心里有数。4.1 插件集体报错带着旧插件跑迁移的后果第一个坑发生在数据库迁移阶段。当时我没仔细核对插件兼容性直接带着全部旧插件执行了glpi:database:update。迁移脚本执行到一半控制台输出一条错误某个插件的数据库表字段类型和GLPI 11新的ORM映射不一致导致ALTER TABLE失败。这个错误的本质是GLPI 11升级时官方迁移脚本会把自己核心表结构调整完然后触发插件自己的钩子升级逻辑。如果插件自身代码还在用GLPI 10时代的API迁移过程就可能抛出异常。解决办法就是回到备份状态重新来。先恢复数据库备份然后把出问题的插件目录临时改名docker compose exec glpi mv /var/www/html/plugins/bad-plugin /tmp/bad-plugin-backup接着再执行数据库迁移迁移完再单独处理插件。这个坑给我的教训是升级期间除了官方核心插件所有第三方插件最好先挪出plugins目录等核心迁移完成、确认页面正常后再一个一个放回去测试。不要怕麻烦这比迁移失败恢复备份高效得多。4.2 白屏排查链路文件目录权限与PHP扩展第二个坑是升级完成后首页白屏。当时日志文件files/_log/php-errors.log里看到了类似“file_put_contents(...): Failed to open stream: Permission denied”的错误。根本原因在于GLPI 11启动后要写新的缓存目录和日志文件而我的NFS挂载卷里files/_cache目录的所有者是NFS服务器的某个高UID用户容器内www-dataUID 33没有写入权限。在GLPI 10时代缓存路径权限要求没这么敏感所以一直没暴露。排查链路分享给大家先看容器是否存活docker compose ps。看docker logs glpi最近几十行有没有PHP Fatal错误。进容器直接尝试写文件docker compose exec glpi touch /var/www/html/files/_cache/test.txt如果报Permission denied基本就是权限问题。找到卷在宿主机上的实际路径执行chown -R 33:33 files/_cacheUID 33对应镜像内www-data或者更粗暴一点chmod -R 777 files/_cache。生产环境不建议777我这里只是定位用最终固定为chown -R 33:33。清缓存再访问php bin/console glpi:cache:clear。如果是自定义镜像或自建FPM环境还要额外确认PHP扩展。GLPI 11在升级检查阶段会列出一堆必选扩展但如果你跳过了Web向导直接用命令行迁移缺失扩展不一定立刻报错而是在某些页面渲染时才暴露。建议迁移后执行php -m核对扩展列表重点检查intl、gd、curl、mbstring。4.3 数据库迁移中断后的处理顺序第三个坑是我在测试环境故意制造并验证的迁移执行到一半网络抖动导致数据库连接断开控制台抛错退出。处理顺序很重要。第一步是千万不要直接重新执行迁移命令而是先看错误日志确认迁移脚本停在了哪个版本。GLPI的迁移表一般是glpi_events或Doctrine自带的版本记录表但更简单的方式是看files/_log/sql-errors.log里面有最近一次SQL错误的详细信息。第二步是判断这次中断是否已经对表结构产生了未记录变更。如果迁移脚本在同一个事务里执行中断后回滚是干净的但某些DDL在MySQL/MariaDB里是隐式提交的可能已经改了一部分表结构而版本记录表还没更新。这种情况直接重新执行迁移命令很可能报“表已存在”或者“字段已存在”但迁移脚本又不知道这个状态。稳妥做法是如果是小型环境直接恢复数据库备份重来如果是大型环境不能随便停服务联系有经验的DBA手工对齐迁移版本表再继续跑余下迁移。不建议在生产环境核心数据库上用“硬恢复”之外的方式硬刚这个状态。我把这个经验总结成一个简单的排查对照表坑现象根因处理插件兼容迁移中断报插件表结构错误插件API不兼容GLPI 11临时停用插件核心迁移后逐个启用目录权限升级后白屏/500www-data无法写缓存chown -R 33:33 files/_cache数据库中断控制台报错退出网络/超时/锁冲突看SQL错误日志必要则恢复备份静态资源404页面无样式无图标前端构建缓存未清理glpi:cache:clear后刷新5. 升级后的验证清单与回滚预案升级完成不等于结束必须验证功能完整性同时把回滚预案准备好才能算真正交付。5.1 功能验证清单我每次升级后都会按这个清单过一遍确认版本号正确。在容器里执行docker compose exec glpi php bin/console glpi:config:get version访问首页确认登录页正常能登录管理员账号。检查资产列表随便打开一个已有资产确认详情页、关联文档、历史记录都正常。检查服务台模块新建一张测试工单确认流程模板、通知邮件都正常。检查计划任务状态执行一次docker compose exec glpi php bin/console glpi:cron:run这个命令会手动触发一轮cron任务能快速发现任务脚本路径是否异常。查看日志目录确认files/_log/php-errors.log里没有新的Fatal错误sql-errors.log为空或只有历史记录。如果接了OCS Inventory、JAMF、Agent同步还要确认同步任务能正常推送数据这点容易被忽略但恰恰是运维资产系统最核心的部分。5.2 回滚操作的实际顺序如果升级验证不通过回滚必须按以下顺序执行顺序错了会导致数据错乱停掉GLPI容器docker compose stop glpi。恢复文件卷用之前备份的tar包把files、config、plugins目录恢复到备份时状态tar xzf glpi_html_backup_$(date).tar.gz -C /var/lib/docker/volumes/对应的宿主路径恢复数据库docker exec -i glpi-db mariadb -uroot -p密码 glpidb glpi10_backup.sql将编排文件里的镜像tag改回10.0.x执行docker compose up -d。这里必须强调一个反直觉的点绝对不要尝试只把镜像切回10而不恢复数据库备份。因为数据库结构已经被11的迁移脚本改过了没有任何官方机制支持从11降级回10只能靠备份数据恢复。5.3 把升级固化到自动化脚本经过这次升级我的体会是升级不是一次性的手工操作而应该是可重复执行的工程流程。尤其是当你维护的GLPI实例不止一套或者后续还要升级到11.x的下一个补丁版本时手动执行很容易遗漏某个环节。我建议把下面这套动作固化成一个脚本至少包含自动备份文件和数据库到指定目录自动检查插件兼容列表执行docker compose pull执行维护模式执行数据库迁移执行缓存清理跑一遍基础健康检查验证失败时自动执行回滚脚本脚本化的另一个好处是测试环境和生产环境可以用同一套流程先在测试环境完整跑一遍确认所有插件都正常再对生产环境操作。我在这次升级中就是在测试环境发现表单插件不兼容提前处理掉了才让生产环境升级一次通过。这个习惯强烈建议大家培养。