Writing Library & Research Systems

How to Upgrade OJS 3.3 to 3.5: Pre-Flight Checks, Upgrade and Rollback

Upgrade OJS 3.3 directly to 3.5: fix pre-flight blockers first, move to PHP 8.2+, merge config, run upgrade.php, switch cron and keep a tested rollback.

Upgrade OJS 3.3 to 3.5

Yes, you can upgrade OJS 3.3 to 3.5 in one jump. The 3.5 upgrade script runs the 3.4 migrations itself, so you don’t need to stop at 3.4 first. What decides whether it goes smoothly is the preparation. Your PHP version has to change (3.5 needs PHP 8.2 or later). The 3.4 pre-flight checks stop the upgrade over data problems you can find and fix in advance. Cron and plugins have changed. And you need a rollback plan you have actually tested. This guide covers all of it in order, with the commands I use on Ubuntu with Apache and MySQL/MariaDB.

It assumes you can SSH into the server, run sudo and log in to the database. If you’re still on 3.2 or older, do that step first using my OJS 3.2 to 3.3 upgrade guide, because 3.5 refuses to upgrade anything older than 3.3.

Why upgrade OJS 3.3 to 3.5 now

OJS 3.3 was PKP’s first long-term support (LTS) release. At the time of writing, the OJS download page labels 3.5 as the current LTS (3.5.0-6, released at the start of November 2026) and says 3.3 “will be supported until at least January 1, 2027”. PKP’s LTS policy ties the 3.3 end-of-life to the designation of the next LTS, with at least 12 months’ notice. Read together, those suggest a 2027 deadline, but PKP hasn’t published a firm end date that I could find. Once 3.3 reaches EOL it gets no more security patches. Planning a weekend for this now is much better than rushing it after an advisory.

There is also no good reason to target 3.4. The download page lists 3.4 without the LTS label, so you would only have to upgrade again later.

What changes between 3.3 and 3.5

These are the changes that affect the upgrade itself. The new editorial features are covered in PKP’s own release material. Everything here comes from the 3.4 and 3.5 release notes and the upgrade scripts in the PKP GitHub repositories.

AreaOJS 3.3OJS 3.5
PHP7.3 to 8.28.2 or later (the installer refuses older)
DatabaseMySQL/MariaDB or PostgreSQL 9.5+MySQL 5.7.22+, MariaDB or PostgreSQL 9.5+, with InnoDB required on MySQL/MariaDB (foreign keys)
Scheduled tasksphp tools/runScheduledTasks.php or the Acron pluginphp lib/pkp/tools/scheduler.php run or the built-in web task runner. Acron is gone
Background jobsNoneA Laravel job queue, run on web requests by default or by a worker
DOIspubIds DOI pluginBuilt into core, with Crossref and DataCite as registration agencies
ORCIDORCID Profile plugin plus config entriesBuilt into core, with settings stored in the database
Usage statisticsusageStats pluginCore statistics with new metrics tables (data migrated)
Locale codesen_USen
config.inc.phpNo app keyNew app_key, [schedule] and [queues] settings, and allowed_hosts

Before you start: the pre-flight checklist

The 3.5 package runs a pre-flight check before it changes your schema. If any check fails, the upgrade stops with an error. I read through those checks in the pkp-lib source, and most of them can be found ahead of time with a few SQL queries. Run these on the 3.3 database (replace ojs with your database name):

-- 1. Tables not using InnoDB (MySQL/MariaDB). Must return nothing.
SELECT table_name, engine FROM information_schema.tables
WHERE table_schema = 'ojs' AND LOWER(engine) <> 'innodb';

-- 2. Journals missing a contact name or email. Must return nothing.
SELECT j.path FROM journals j
LEFT JOIN journal_settings e ON e.journal_id = j.journal_id AND e.setting_name = 'contactEmail'
LEFT JOIN journal_settings n ON n.journal_id = j.journal_id AND n.setting_name = 'contactName'
WHERE e.journal_id IS NULL OR n.journal_id IS NULL;

-- 3. Authors pointing at a user group that no longer exists.
SELECT a.author_id, a.publication_id FROM authors a
LEFT JOIN user_groups ug ON ug.user_group_id = a.user_group_id
WHERE ug.user_group_id IS NULL;

-- 4. Submissions with no locale.
SELECT COUNT(*) FROM submissions WHERE locale IS NULL;

How to fix each one:

  • MyISAM tables: convert them with ALTER TABLE table_name ENGINE=InnoDB;. Also check that the server’s default engine is InnoDB, because the pre-flight check tests that as well.
  • Missing contact details: set the principal contact name and email under the journal’s settings (in 3.3: Settings > Journal > Contact) for every journal the query lists, including old or disabled ones.
  • Orphaned authors: the upgrade first cleans up orphaned rows, then runs this check, so a few hits may disappear on their own. If any remain, give those authors a valid contributor user group, or remove them, in a sandbox copy first.
  • Submissions with no locale: the upgrade copies the locale from the publication where it can. Only rows it can’t fix stop the upgrade.

The other checks:

  • Unprocessed usage statistics logs. If files_dir/usageStats/usageEventLogs/ contains logs older than yesterday, or processing, reject or stage is not empty, the upgrade stops. This usually means the 3.3 scheduled tasks haven’t been running. Process the logs on 3.3 with php tools/runScheduledTasks.php plugins/generic/usageStats/scheduledTasks.xml, then check the folders again. A stuck file left in reject has to be fixed or removed by hand.
  • base_url must match the log URLs. The check compares base_url in config.inc.php with the URLs in the newest usage log. If you’re rehearsing on a staging hostname, this check is the reason it fails.
  • Section editor role. If a journal has section editor assignments but its “Section editor” role was deleted, recreate the role before upgrading.
  • Duplicate emails or usernames (PostgreSQL only). From 3.4, these are case-insensitive. MySQL’s default collation already treats them that way. On PostgreSQL, find duplicates with SELECT LOWER(email), COUNT(*) FROM users GROUP BY 1 HAVING COUNT(*) > 1; and merge them on 3.3 with php tools/mergeUsers.php keepUsername dropUsername.

Then go through the plugins. In 3.3, open Website Settings > Plugins, list every third-party plugin and theme, and check whether a 3.5 release exists, either in the 3.5 Plugin Gallery or on the developer’s site. Anything with no 3.5 version needs a decision before you upgrade: replace it, live without it, or wait. For themes in particular, switch the journal to the bundled Default theme before upgrading if yours has no 3.5 release. A missing active theme caused fatal errors after upgrade in early 3.5 builds (issue #11396, fixed in 3.5.0-2), and it’s an easy risk to avoid. Note that the ORCID Profile, Acron and usageStats plugins are replaced by core features, so you don’t need 3.5 versions of those.

Step 1: Set variables and record the starting state

I use the same paths as my OJS 3.5 install guide. Adjust them to match your server:

export OJS_WEB=/var/www/ojs
export OJS_FILES=/var/www/ojs-files
export OJS_VERSION=3.5.0-6
export BACKUP=/root/ojs-upgrade-$(date +%F)
sudo mkdir -p $BACKUP

cd $OJS_WEB
php tools/upgrade.php check
grep -A8 '^\[database\]' config.inc.php
php -v

The check output starts with Code version: and Database version:, and both should say 3.3.0.x. Write them down. Use the latest 3.5.0-x build listed on the download page rather than copying my version number, because each build fixes upgrade bugs. Two examples: 3.5.0-1 fixed SQL error 1064 during upgrade on MariaDB/MySQL (#11529), and 3.5.0-2 fixed a foreign key violation in the 3.3 DOI migration (#11663).

Step 2: Get PHP to 8.2 or later

This is where most upgrades go wrong. The 3.3 README lists PHP versions up to 8.2. The 3.5 installer checks for 8.2.0 and won’t run on anything older. So PHP 8.2 is the one version both releases officially support. Ubuntu 22.04 ships PHP 8.1 and Ubuntu 24.04 ships 8.3, so in practice you’ll probably need to install a second PHP version (on 22.04, the usual source is the Ondřej Surý PPA).

Install the new PHP with the extensions OJS 3.5 needs (the OJS README lists them; bcmath and intl are the ones most often missing). Don’t switch Apache over yet. Then make sure your shell uses the same version Apache will use. A common trap is Apache on 8.3 while php on the command line is still 8.1, which makes the upgrade script fail immediately with an “unsupported PHP version” error.

sudo apt install php8.3 libapache2-mod-php8.3 php8.3-cli php8.3-mysql \
  php8.3-mbstring php8.3-xml php8.3-intl php8.3-gd php8.3-curl \
  php8.3-zip php8.3-bcmath
php8.3 -m | grep -E 'bcmath|intl|mbstring|zip|gd|curl'

Below, I call php8.3 explicitly for every upgrade command, so the CLI’s default version doesn’t matter.

Step 3: Stop background tasks and close the site

Comment out the cron line that runs tools/runScheduledTasks.php (check sudo crontab -l -u www-data and /etc/cron.d). If you use the Acron plugin instead, disable it in the plugin list. Then block visitors. OJS has no maintenance mode, so the PKP upgrade guide suggests a deny-all rule in Apache. I put a temporary rule in the vhost that allows only my own IP address and reload Apache.

Step 4: Back up everything

Back up the database, both file directories, the config file and the whole code directory. The upgrade migrates data inside files_dir as well as in the database, so a database dump on its own isn’t enough to roll back.

sudo mysqldump --single-transaction --routines -u ojsuser -p ojs \
  | gzip > $BACKUP/ojs-db.sql.gz
sudo tar -czf $BACKUP/ojs-files.tgz -C $(dirname $OJS_FILES) $(basename $OJS_FILES)
sudo cp $OJS_WEB/config.inc.php $BACKUP/
sudo mv $OJS_WEB $BACKUP/ojs-3.3-code
ls -lh $BACKUP

Moving the old code directory instead of deleting it keeps the rollback simple, as you’ll see below. Before you continue, check the dump with zcat $BACKUP/ojs-db.sql.gz | tail -1. It should end with -- Dump completed.

Warning: Do the whole procedure on a sandbox copy first (restore these backups to a test server or VM). Big jumps can take hours on large journals, and the PKP guide says plainly that you should only touch production after a successful sandbox run. Remember the base_url check: in the sandbox, keep base_url the same as production and point your hosts file at the test server.

Step 5: Install the 3.5 code and merge the config

cd /tmp
wget https://pkp.sfu.ca/ojs/download/ojs-$OJS_VERSION.tar.gz
sudo mkdir $OJS_WEB
sudo tar -xzf ojs-$OJS_VERSION.tar.gz -C $OJS_WEB --strip-components=1
sudo cp $BACKUP/config.inc.php $OJS_WEB/
sudo rsync -a $BACKUP/ojs-3.3-code/public/ $OJS_WEB/public/
diff $OJS_WEB/config.inc.php $OJS_WEB/config.TEMPLATE.inc.php | less

If you have a custom .htaccess in the web root, copy it back as well. Then edit config.inc.php using the 3.5 and 3.4 release notes. These are the changes that matter coming from 3.3:

  • locale = en_US becomes locale = en, since locale codes were standardised in 3.4. The locale/ directory lists the valid codes.
  • Date formats change from strftime to PHP date syntax. The 3.4 notes give replacements such as date_format_short = "Y-m-d". The old values still work for now but are deprecated.
  • Remove client_charset (OJS is UTF-8 only now) and the old scheduled_tasks setting.
  • Set allowed_hosts to your journal hostnames, for example allowed_hosts = '["journals.example.org"]'. PKP recommends it to protect against host header injection.
  • Copy in the new [schedule] and [queues] sections and the app_key line from the template. Leave app_key empty, because the upgrade fills it in.
  • The [orcid] client ID and secret move into the database during the upgrade. Keep them until the upgrade finishes, then remove them.

Set ownership so that both the web server and the upgrade can write where they need to. That includes config.inc.php for now, so the upgrade can write the app key:

sudo chown -R www-data:www-data $OJS_WEB/cache $OJS_WEB/public \
  $OJS_WEB/plugins $OJS_WEB/config.inc.php $OJS_FILES

Step 6: Run the upgrade

Run it as the web server user so that any files it creates have the right owner. Log everything:

cd $OJS_WEB
sudo -u www-data php8.3 tools/upgrade.php check
sudo -u www-data php8.3 -d memory_limit=2048M tools/upgrade.php upgrade \
  2>&1 | tee $BACKUP/upgrade.log

The check should now report the code version as 3.5.0.x, the database version as 3.3.0.x, and the message Database version is older than code version. The upgrade prints timestamped log lines for each migration and, if everything works, ends with Successfully upgraded to version 3.5.0.6 (or whichever build you installed). On a small journal it takes minutes. Journals with years of statistics take much longer, because the metrics tables are rebuilt.

If it fails, read the last lines of the log. There’s an important detail in how failures are recorded. The 3.4 and 3.5 stages each have their own pre-flight checkpoint. If the 3.4 part completes and the 3.5 check then fails, OJS records the database as “3.4.9.9” and logs A pre-flight check failed. The software was successfully upgraded to 3.4.9.9 but could not be upgraded further. You can fix the cause and run the upgrade again from there. If a migration fails partway through, though, the script itself recommends restoring from backup, and so do I.

Step 7: Switch PHP, app key, cron and jobs

# Apache to the new PHP
sudo a2dismod php8.1 && sudo a2enmod php8.3 && sudo systemctl restart apache2

# Confirm the app key was written
sudo -u www-data php8.3 lib/pkp/tools/appKey.php validate
# If it reports a missing key:
sudo -u www-data php8.3 lib/pkp/tools/appKey.php generate

# Lock config back down once the key is present
sudo chown root:www-data config.inc.php && sudo chmod 640 config.inc.php

The upgrade step that writes the app key only logs an error if it can’t write to config.inc.php. It doesn’t stop the upgrade, so it’s easy to miss. Once you have a valid key, back up config.inc.php again. 3.5 uses the key for encryption, and from 3.5.0-6 that includes stored credentials (#12074), so don’t regenerate it on a working site.

Next, the new cron line. tools/runScheduledTasks.php no longer exists in 3.5, so the old cron entry would just fail silently every time. PKP’s scheduled tasks guide recommends running the scheduler every minute and turning off the web runner:

# crontab for www-data
* * * * * cd /var/www/ojs && php8.3 lib/pkp/tools/scheduler.php run >> /dev/null 2>&1

# config.inc.php
[schedule]
task_runner = Off

Then the job queue. The upgrade can leave jobs queued (fixing statistics region codes is one example). By default, job_runner = On works through them during normal page requests, which is fine for most single-journal sites. For larger sites, PKP’s jobs guide recommends a Supervisor-managed worker running php lib/pkp/tools/jobs.php work with job_runner = Off. Either way, check the queue now:

sudo -u www-data php8.3 lib/pkp/tools/jobs.php list
sudo -u www-data php8.3 lib/pkp/tools/jobs.php run
sudo -u www-data php8.3 lib/pkp/tools/jobs.php failed

Step 8: Verify before reopening

While the site is still limited to your own IP, go through this list:

  1. php8.3 tools/upgrade.php check shows the same code and database version.
  2. The journal homepage, the current issue, an article landing page and a PDF galley all load. Search returns results.
  3. Login and password reset work, and a test email arrives.
  4. A test submission can go through submission, review assignment and a decision. 3.5 has a redesigned editorial workflow, so warn your editors before they see it.
  5. DOIs: check the DOI settings and that existing DOIs still show on articles. Re-enter the Crossref or DataCite credentials if they’re missing.
  6. ORCID: check the ORCID settings in the journal settings (the location may change between builds) and that verified iDs still show.
  7. Statistics: the dashboard shows historical figures, not zeros.
  8. The OAI endpoint answers: curl -s "https://journals.example.org/index.php/abc/oai?verb=Identify" | head
  9. Install your third-party plugins and theme from the 3.5 Plugin Gallery or from their developers, then check the reader side again.

Then remove the IP restriction, reload Apache and watch /var/log/apache2/error.log for an hour. I also put an uptime monitor on the homepage and the OAI endpoint after any upgrade. Uptime Kuma takes ten minutes to set up for that.

Rollback plan

Because you moved the old code instead of deleting it, rolling back is a few commands. Rehearse it in the sandbox too:

sudo mv $OJS_WEB /root/ojs-3.5-failed
sudo mv $BACKUP/ojs-3.3-code $OJS_WEB
mysql -u root -p -e "DROP DATABASE ojs; CREATE DATABASE ojs DEFAULT CHARACTER SET utf8mb4;"
zcat $BACKUP/ojs-db.sql.gz | mysql -u root -p ojs
sudo rm -rf $OJS_FILES && sudo tar -xzf $BACKUP/ojs-files.tgz -C $(dirname $OJS_FILES)
sudo a2dismod php8.3 && sudo a2enmod php8.1 && sudo systemctl restart apache2

Match the character set in CREATE DATABASE to your original database, and grant the OJS database user access again if dropping the database removed it. Re-enable the old cron line, and you’re back on 3.3 exactly as you left it. Decide your rollback cut-off time before you start. If production isn’t verified by then, roll back and try again later, rather than fixing things live while editors wait.

Common errors and fixes

  • “Your server’s PHP version is not supported”: the CLI is running PHP older than 8.2. Call php8.3 explicitly.
  • “Upgrade unsupported. See the Upgrade Guide”: the database is older than 3.3. Upgrade to 3.3 first.
  • “Storage engine that doesn’t support foreign key constraints detected”: some tables are MyISAM. Restore the backup, convert the tables to InnoDB, and run again.
  • “There are unprocessed log files from more than 1 day ago”: process the usage logs on 3.3 (see the checklist above) or move the stale logs out of the way after you’ve decided you don’t need those statistics.
  • “A contact name and email must be set on the context(s)”: set the contact for every journal path in the message.
  • “The base_url in config.inc.php should be the same as in URLs in the usage stats log file”: you changed the hostname, which usually happens in staging. Use the production base_url.
  • Out of memory during metrics migration: raise -d memory_limit, restore the backup and run again.
  • 500 error or white page after upgrade: check the Apache error log. The usual causes are an incompatible third-party theme or plugin, Apache still on old PHP, or wrong ownership on cache/. Clearing cache/*.php and cache/t_compile helps after plugin changes.
  • Scheduled emails or DOI deposits stopped: the old runScheduledTasks.php cron line is still there. Replace it with scheduler.php run.

If your error isn’t listed, search the PKP forum for the exact message first; 3.3 to 3.5 is a well-trodden path and most failures have been reported before. When you post, include your exact from and to versions, PHP and database versions, and the last 50 lines of the upgrade output. Check that every third-party plugin you rely on has a 3.5 release before you start, and avoid one-click installer upgrades for a jump this size: doing it by hand, as above, means you always know which step failed.

Next steps

Once you’re on 3.5, applying future 3.5.0-x builds is the same procedure without the PHP and config work, so it takes much less time. Subscribe to the PKP announcements forum so you hear about security builds. If you’re replacing the server instead of upgrading in place, build a clean server with my OJS 3.5 on Ubuntu guide, restore the 3.3 database and files there, and run Step 6 on the new machine. That gives you a fresh OS and leaves the old server untouched as your rollback.

Sources and further reading

Was this worth your time?

Comments

No comments yet. If something here helped or didn’t work for you, say so below.

Leave a comment

Comments are moderated. Be kind, stay on topic, and leave out passwords or other secrets when sharing configs.