Upgrading: from 4.x, 5.x or an earlier 6.0.x to today
This guide is for whoever runs an existing installation and wants to bring it to the current Exponential 6.0 line.
That line is 6.0.15, which follows 6.0.14; Composer installs it with the constraint ~6.0.15 (see
Changelog 6.0.15). Find your version, follow the part for it, and finish with the
checks at the end. They tell you that the result is good.
Warning
.htaccess slows down every request. When .htaccess files are enabled (AllowOverride anything but None), let alone
used, Apache looks for them and reads and evaluates them on every request, in every directory of the path, and
page loading times go up drastically (measured). The only real fix is to switch the feature off for
the virtual host: AllowOverride None in the HTTPS virtual host, and the rules in the virtual host's own
configuration as mod_rewrite rules. That is far faster in production speed tests and well worth the time it takes
to optimize a production configuration. Switch over right away: copy the rules of the
example HTTPS virtual host into your virtual host (with the static cache, also
staticcache_offload.conf), set AllowOverride None, run apachectl configtest, reload Apache and
test the site. The same rules for other servers: nginx, LiteSpeed,
Caddy, IIS, Velocity (built in); where virtual host rules go in a control
panel: Plesk, cPanel, DirectAdmin, ISPConfig.
On LiteSpeed or OpenLiteSpeed, the server's own page cache can also answer anonymous visitors and crawlers without PHP: Page caching with LiteSpeed and OpenLiteSpeed.
Plan for twenty minutes for a small site. The risky step is the database. That is why step 1 is a backup.
Which part do I read?
Find your version: in the admin the Setup > System information page names it; on the command line:
mysql -u USER -p DATABASE -e "SELECT name, value FROM ezsite_data WHERE name LIKE 'ezpublish-%';"(Use the equivalent for PostgreSQL or SQLite; the row names ezpublish-version and ezpublish-release keep their
historic spelling on purpose.) Expected: a value such as 5.4.0 or 6.0.14.
| You run | Read |
|---|---|
| 3.10 up to 5.3 | Part A, then B |
| 5.4.0 or 5.90 (2017.08 and later) | Part B |
| 6.0.0 up to 6.0.14, or an earlier checkout of the 6.0.15 line | Part C |
lovestack (LoveStack, Mugo Web's distribution of the same kernel, mugoweb/ezpublish-legacy) | Coming from LoveStack, then Part C, then exp:lovestack migrate |
Coming from LoveStack
LoveStack shares its origin with Exponential, and its databases are close to Exponential's. One read-only command lists what such a site brings along and what to do with each item:
./console exp:lovestack check --allow-root-user # add --json for a machine-readable reportIt reads the database and the settings and writes nothing. Each line is marked:
| Mark | Meaning |
|---|---|
FOUND | something to do; the line below it says what (for example the version row lovestack, the legacy_2018 extension, [Session] SameSite to rename to CookieSameSite, image.ini [ValidUploadFormats] MIMEList[] to rename to [ImageUploadSettings] AllowedMIMETypes[]) |
info | present, and Exponential works with it as it is: LoveStack's db-schema-version row is kept as a record, its unique index on ezuser.login and its data_text NOT NULL DEFAULT '' column can stay (Exponential stores an empty string there, never NULL), settings that Exponential reads under the same name |
clear | not present |
unknown | the database engine does not tell (MongoDB has no indexes or columns to read) |
It ends with OK (exit code 0) when nothing needs to be done, or NEEDS ATTENTION (exit code 1).
Then back up (Step 1), put the new code in place and apply the database update of Part C (C1 and C2). After that, one more command records the database as Exponential's and lists the settings to change by hand:
./console exp:lovestack migrate --allow-root-user # a dry run: prints what it would write, writes nothing
./console exp:lovestack migrate --apply --allow-root-user # writes itWhat it writes, in the ezsite_data table and nowhere else:
| Row | Value |
|---|---|
ezpublish-version | this release's version (6.0.15stable) when the row still says lovestack. Exponential's version checks (Setup > Upgrade check, Info > About) read this row. The update SQL of C2 sets it too; any other value is left to that file |
lovestack-schema-version | the value of LoveStack's db-schema-version row (unknown when there is none), so the database keeps a record of the LoveStack schema steps it went through. LoveStack's own row stays as it is |
It never changes content, users or settings, and running it again changes nothing. A database that does not come
from LoveStack (no lovestack version row and no db-schema-version row) is refused with a message saying so, and
nothing is written. --json gives the same report as one JSON object.
Below the rows it prints the settings and extensions to change by hand, taken from check:
- LoveStack-only settings, each with the Exponential name to use or a note that Exponential does not read it. For
site.ini [Session] SameSite, rename it toCookieSameSite. Until then Exponential readsSameSiteasCookieSameSiteand logs a deprecation notice (if both are set,CookieSameSiteis used). - The
legacy_2018extension, if it is active: remove it fromActiveExtensions[]andActiveAccessExtensions[]. Exponential keeps the selection datatype (ezenum) in the kernel, and the extension would define it a second time. - The
mugoweb/ezpublish-legacypackage, ifcomposer.jsonstill requires it: replace it with Exponential's.
Thanks to Mugo Web, whose documented schema steps in LoveStack (update/database/mysql/lovestack/1.sql to 4.sql)
made it possible to tell a LoveStack database apart this precisely.
Step 1: back up, always
mysqldump -u USER -p DATABASE > backup-before-upgrade.sql # MySQL or MariaDB
pg_dump -U USER DATABASE > backup-before-upgrade.sql # PostgreSQL
sqlite3 var/storage/sqlite3/exponential.db ".backup 'backup-before-upgrade.db'" # SQLite: use the path of your database file
tar czf backup-var-and-settings.tgz var settings/override settings/siteaccess extension designKeep settings/override/ and your own extensions and designs out of any command that replaces files. The one
check that proves the backup is usable: restore it on a scratch machine, never on the live database.
Part A: from 3.10 up to 5.3 (the old chain)
The database update files form a chain; each takes you one step. Apply them in order, each once, for your engine
(mysql, postgresql, sqlite where it exists), from the new code's update/database/<engine>/ directory:
| From | To | MySQL file |
|---|---|---|
| 3.10 | 4.0 | 4.0/dbupdate-3.10.0-to-4.0.0.sql |
| 4.0 | 4.1 | 4.1/dbupdate-4.0.0-to-4.1.0.sql |
| 4.1 | 4.2 | 4.2/dbupdate-4.1.0-to-4.2.0.sql |
| 4.2 | 4.3 | 4.3/dbupdate-4.2.0-to-4.3.0.sql |
| 4.3 | 4.4 | 4.4/dbupdate-4.3.0-to-4.4.0.sql |
| 4.4 | 4.5 | 4.5/dbupdate-4.4.0-to-4.5.0.sql |
| 4.5 | 4.6 | 4.6/dbupdate-4.5.0-to-4.6.0.sql |
| 4.6 | 4.7 | 4.7/dbupdate-4.6.0-to-4.7.0.sql |
| 4.7 | 5.0 | 5.0/dbupdate-4.7.0-to-5.0.0.sql |
| 5.0 | 5.1 | 5.1/dbupdate-5.0.0-to-5.1.0.sql |
| 5.1 | 5.2 | 5.2/dbupdate-5.1.0-to-5.2.0.sql |
| 5.2 | 5.3 | 5.3/dbupdate-5.2.0-to-5.3.0.sql |
| 5.3 | 5.4 | 5.4/dbupdate-5.3.0-to-5.4.0.sql |
Apply one with the client of your database, for example:
mysql -u USER -p DATABASE < update/database/mysql/4.1/dbupdate-4.0.0-to-4.1.0.sqlRules that keep this safe:
- Do not skip a file and do not apply one twice. If a statement fails because a column already exists, the file was applied before: stop and compare with your backup.
- If you use the database cluster (DFS or database file handler), also apply the
dbupdate-cluster-...file of the same step where one exists (4.3, 4.7, 5.2, 5.4). - The directories named
unstablehold files of pre-releases; do not apply them. - Directories
6.12and7.3are not part of this path; the 6.0 files in Part B take a 5.4 database to today. - Several steps ship a PHP script that repairs data; run the ones of the versions you pass through, from the
project root, for example
php update/common/scripts/4.1/updateimagesystem.php. List them withls update/common/scripts/*, and read the first lines of a script (--helpwhere it offers it) before you run it. The what and why of each release is in its notes: 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7, 5.0, 5.1, 5.2, 5.3, 5.4. - The update chain is checked by
./console exp:checkdbfiles --allow-root-user, which lists missing or stray files of the upgrade path.
Then read the notes of the 5.90 line, which prepared the move to PHP 7 and a longer password rule: 5.90 overview, PHP 7, password length, relation indexing.
Part B: from 5.4 (or 5.90) to Exponential 6.0.0
Put the new code in place. Either a new checkout and a copy of your
settings/override/,settings/siteaccess/,extension/anddesign/into it, or, when you manage the site with Composer, change the constraint ofse7enxweb/exponentialin your project'scomposer.jsonand runcomposer update se7enxweb/exponential. Do not let a tool replace your own settings or extensions.Record the new version in the database. The same file brings the schema to what the 6.0 kernel uses: it widens
ezuser.password_hashfrom thevarchar(50)of every 5.x schema to 255 (with the defaultHashType=php_defaultandUpdateHash=trueeach user's hash becomes a 60-character bcrypt hash at the first sign-in, which the old column cannot hold), addsezcontentobject_trash.trashed(without it moving content to the trash fails) and, on PostgreSQL, renames the sequences to<table>_<column>_seq(the names the kernel reads new ids from). Each change leaves a database that already has it as it is:mysql -u USER -p DATABASE < update/database/mysql/6.0/dbupdate-5.4.0-6.0.0.sqlPostgreSQL:
update/database/postgresql/6.0/dbupdate-5.4-to-6.0.sql. The MySQL file begins withSET default_storage_engine=InnoDB;, which MySQL (from 5.5.3) and MariaDB accept, so run the whole file. Copies of the file from before October 2026 began withSET storage_engine=InnoDB;, which MySQL 5.7.5 and newer and MariaDB 12.0 and newer reject; take the current file. Details: Changelog 6.0.0.PHP: Exponential 6.0.15 runs on PHP 8.0 or newer (PHP 7.4 usable without support), Exponential Velocity on 8.0 or newer (from its release 0.0.4.54; see PHP 8.0 support and Why Exponential supports PHP 7.4). A site on PHP 7.4 can run the current line again from 6.0.15, without support (6.0.8 to 6.0.14 need PHP 8.1); read PHP 8 support for the order of the upgrade steps, and check your own extensions for PHP 8 warnings (classes extending
eZPersistentObjectoreZDataTypeare the usual cases).Continue with Part C, which is the same procedure for every 6.0.x step.
Part C: from any 6.0.x to today
C1. Put the new code in place
Take the new code as in Part B step 1 (checkout of the tag or branch, or composer update). Then regenerate the
class map and clear the caches, in this order:
php bin/php/ezpgenerateautoloads.php -e
php bin/php/ezcache.php --clear-all --allow-root-userExpected: the first prints how many classes it found; the second ends with a line saying the caches were cleared.
Do not use -k on an installation that keeps extra working copies of the code inside its root (for example git worktree folders) without excluding them, e.g. --exclude='<folder>'; otherwise kernel classes are mapped into those copies.
Then reload the PHP-FPM that serves the site (systemctl reload <your-php-fpm-service>), and, if you run Velocity,
./console exp:velocity deploy --dry-run --allow-root-user shows every step the real command would run.
Then compare .htaccess and config.php with the new templates. A release can change .htaccess_root,
.htaccess_root_static and config.php-RECOMMENDED, and your copies stay as they are:
./console exp:setup:status # is .htaccess still a copy of a template?
./console exp:setup:htaccess # the diff between your .htaccess and the new template
./console exp:setup:htaccess --force --apply # take the new rules (backup first), when the diff holds nothing of yours
./console exp:config:list # settings new in config.php-RECOMMENDED show up as (not set)Set the new settings you want with ./console exp:config:set <NAME> <value> --apply; the settings you had keep their
values. The same applies after Parts A and B. See
Setting up .htaccess and config.php from the console.
C2. Apply the database update
The file for the line is update/database/<engine>/6.0/dbupdate-6.0.0-6.0.15.sql (MySQL, PostgreSQL and SQLite
files exist). It records the version, makes the schema changes of the Part B file on MySQL and PostgreSQL (for a site
that came from 5.x with an older copy of that file; a database that already has them is left as it is) and adds the
columns that later changes need (PDF export footer, OPML and podcast fields and more). Apply it once, whole, in
order; if your database already has a column, the statement fails and tells you which one, skip only that
statement. A site from 5.x that applied an older copy of this file and still has a 50-character password_hash, no
ezcontentobject_trash.trashed or (PostgreSQL) <table>_s sequences does not apply it again: it applies the current
Part B file and sets the version row back with
UPDATE ezsite_data SET value='6.0.15stable' WHERE name='ezpublish-version';. Otherwise:
mysql -u USER -p DATABASE < update/database/mysql/6.0/dbupdate-6.0.0-6.0.15.sqlThen create the audit index tables once (the audit trail is on by default from 6.0.15), and copy who trashed what
from <VarDir>/trash/trashed.json into the new columns ezcontentobject_trash.trashed_by and trashed_via (once
per var directory, on every web server of a cluster):
php update/common/scripts/6.0/createaudittables.php
php update/common/scripts/6.0/movetrashrecords.php --remove-fileUntil trashed_by and trashed_via exist the kernel keeps writing trashed.json; see
the trash view.
Check the version row (same query as at the top; expected 6.0.15stable after this file).
A new installation has a unique index on ezuser.login; the file does not add it on an upgrade, since it fails on a
database where two accounts share a login. List such logins and add the index when there are none (the command
refuses otherwise, and changes nothing):
./console exp:user-login-check --allow-root-user
./console exp:user-login-check --fix-index --allow-root-userC3. Read the checklist of each period you pass
The upgrade notes are written per period. Read every one between your version and today; each opens with a quick checklist and a "what to do" column.
| Your version is older than | Read |
|---|---|
| 6.0.8 | Changelog 6.0.8 and PHP 8 support |
| 6.0.10 | Changelog 6.0.10, The product is called Exponential |
| 6.0.13 | Changelog 6.0.13, Security hardening 6.0.13 |
| July 2026 | Behaviour changes of July and August 2026 |
| 16 September 2026 | Behaviour changes of 16 to 30 September 2026 and Security defaults |
| 1 October 2026 | Behaviour changes of 1 to 2 October 2026 |
The three changes people most often meet:
- Accounts without an
ezuser_settingrow cannot sign in (August 2026): make sure each user has one. - Overrides follow
Priority(July 2026): review anyoverride.iniwhere only some blocks have aPriority. - Security headers,
HttpOnly/SameSitesession cookies and a stricter upload check are on by default (September and October 2026): if a host frames your admin, set the header deliberately.
If WebDAV is on: since 6.0.15 its entry script is index_webdav.php. webdav.php only forwards to it and will be
removed in a later major version. Change the virtual host (the HTTPS one too) and the rewrite rules, then test with
curl -i -X PROPFIND -H 'Depth: 1' https://www.example.com/index_webdav.php/ (expect 207 Multi-Status):
The WebDAV entry script is now index_webdav.php.
If SOAP is on (soap.ini EnableSOAP=true, for example for the syndication extension): since 6.0.15 its entry
script is index_soap.php. soap.php only forwards to it and will be removed in a later major version. Let
index_soap.php through in the HTTPS virtual host and in .htaccess, point the SOAP clients at it, reload the web
server, then test with curl -sS -i 'https://www.example.com/index_soap.php?wsdl' (expect 200 and
Error: this web page does only understand POST methods; a 404 or a page of the site means the rule is missing):
The SOAP entry script is now index_soap.php.
This is the step most often forgotten. An upgrade replaces the scripts, but the virtual host, the rewrite rules
and the control panel's directives are yours: no upgrade changes them. Change them on the HTTPS host (*:443), not
only on the HTTP one, and run the curl test after the reload, before you call the upgrade done.
For what changed by month, the story is in the chronicle; every single change is in the ledger of the root repository.
C4. Check your own extensions
php bin/php/ezpgenerateautoloads.php -e
./console exp:checkclasses --allow-root-userexp:checkclasses loads every declared class and reports those PHP refuses to load, which is how a PHP 8
incompatibility in an extension shows. Extensions carry their own version in ezinfo.php and extension.xml;
the extension list of the admin (Setup > Extensions) shows each one, see
Extension list. The order in which extensions load changed in
2026: Extension loading order.
Verify the upgrade
Run these; each states what good looks like.
php bin/php/ezcache.php --clear-all --allow-root-user # ends without an error
./console exp:checkdbfiles --allow-root-user # no file marked as missing for your line
./console exp:expfetch --allow-root-user # the new version, PHP and engine; no error in the HintsBefore the upgrade, keep ./console exp:info --json --no-logo > before.json; after it, the same into after.json:
the difference lists every version that changed (The system card).
Then in the browser: the front page loads; log in to the admin; open Setup > System information and read the version; publish a test item and see it on the site. In the admin, Setup > Upgrade check and Check file consistency list files of the code that differ from the release (File consistency check). Files you changed on purpose are expected there. The upgrade check explains each finding and the database check on the same page.
Last, clear the content cache and, if you use Velocity, its response cache, after the PHP-FPM reload, so a page rendered by old code is not served again.
If something goes wrong
| Symptom | What to do |
|---|---|
| A white page after the code change | var/log/error.log; run C1 again; vendor/ must match the new composer.json |
Class ... not found | php bin/php/ezpgenerateautoloads.php -e, then clear the caches |
| Libraries missing after a failed Composer run | Repairing an installation |
| A SQL file failed half way | restore the backup of step 1 and apply the file again from the start; never repair by hand and re-run |
| Someone cannot sign in | Behaviour changes of July and August 2026, the ezuser_setting item |
A published release can not be changed; the way to get a fix is the next version. To see what a version holds, open its changelog: 6.0.0 to 6.0.15.
Migrations (6.0.15). From 6.0.15 on, a site's own changes after the upgrade (a table, a content class, roles,
a fix of content) are best written as migrations: files under version control that every installation of the site
applies in the same order with ./console exp:migration:migrate --apply, with a caption before each run. The
kernel's upgrade steps are listed among them. Start with Migrations; the word means
something else than upgrading a site: Migrations versus migrating a site.
Demo mode (6.0.15). The environment variable EXP_DEMO_MODE=1 marks a public demo; without it nothing changes. Upgrading a demo: keep the variable for the web server, cron and console, and check the banner afterwards. Upgrading a real site: make sure the variable is set for none of its processes. Demo mode.
VAT rules (6.0.15). Nothing changes in how VAT is charged. VAT rules are applied only when
shop.ini [VATSettings] Handler names a handler (ezdefault), and the shipped settings/shop.ini still names none.
A store whose rules were never applied now sees a notice on Store > VAT rules; to apply them, set
Handler=ezdefault in settings/override/shop.ini.append.php, make sure the customer's country is known at checkout,
and test an order before going live. Changelog 6.0.15.
Related pages
- Getting started: a clean install of the same version, for a test copy.
- Operating a site: backups, caches and repairs after the upgrade.
- Deploying and Velocity engines: reload the right PHP-FPM, or move to a faster server.
- Extensions: versions, loading order and releases of your own extensions.
- Upgrading with the console and Migrations: the upgrade as one command, and the site's own changes after it.
- Setting up .htaccess and config.php from the console: compare both files with the new templates.
- Release notes: Exponential 6.0 changelogs and the extensions' release notes.
Become an Exponential Partner
Partners share their information, provide funding, contribute back and develop the products with us, and that is how Exponential grows. A partner website and dashboard are planned for mid-2027: please become a partner today to speed up our growth rate and land new features faster.