UPGRADE.md
This guide upgrades an existing UnoPim installation to the current release without downtime you did not plan for and without losing data.
Read the CHANGELOG and the release notes before you start. This is a major release: it changes the PHP requirement, invalidates existing API tokens, and runs migrations that cannot be reversed.
<a name="before-you-start"></a>
Each of these needs a decision or an action before you book downtime.
The release does not run on PHP 8.3. Upgrade PHP on the server first, or the upgrade will refuse to start.
php -v
Earlier releases shipped a shared OAuth signing key pair. They are no longer distributed ā each installation now generates its own. Replacing the old pair invalidates every existing access and refresh token.
Plan for this: after the upgrade, every integration must obtain new tokens. Integrations are also migrated to dedicated robot users, and their credentials are revealed once in the admin panel.
If you maintain custom packages or theme overrides, check them against the BC Breaks section of the CHANGELOG. The ones that most often bite:
| Change | What breaks |
|---|---|
| Laravel 12 ā 13, Symfony 8 | Overridden method signatures, middleware references, service providers |
| Admin URL changes | Hardcoded links to catalog/families/*, catalog/attributegroups/*, settings/data-transfer/*, integrations/api-keys/* |
| AJAX navigation and the global save bar | Browser tests and extensions that assume full-page reloads or old DOM IDs |
| Variant value storage | Extensions reading products.values directly must use Product::resolvedValues() |
| Removed classes and services | Webkul\FPC, spatie/laravel-responsecache, ThemeCustomizationRepository, old webhook settings classes |
The upgrade writes and verifies its own database dump, and refuses to continue
if it cannot. That is a safety net, not a backup strategy. Keep your own copy
of the database and the storage/ directory somewhere off the server.
<a name="how-the-upgrade-works"></a>
The new release is installed beside the current one, not on top of it:
/var/www/unopim/
āāā releases/2.1.6/ ā keeps running while you prepare
āāā releases/3.0.0/ ā the new release
āāā current -> releases/3.0.0
Your web server document root points at current/public. Cutover is a symlink
move. Rollback is a symlink move back.
This matters. Overwriting files in place leaves deleted files behind, destroys
your config/ and composer.json edits, and puts live traffic on a
half-swapped application. None of that can happen when the new release is a
separate directory.
php artisan unopim:upgrade then does the rest. It runs in five phases, and
the first three change nothing:
| Phase | What it does |
|---|---|
| 1. Preflight | PHP version, extensions, database, installed release, pending migrations, running jobs, writable paths, disk space. Any failure stops here ā your site is still up and untouched. |
| 2. Drift | Lists the .env keys, config files and Composer requirements you must merge by hand. It never edits them. |
| 3. Sizing | Counts the rows that will be migrated, names the irreversible migrations, and estimates the maintenance window. |
| 4. Migration | Verified database dump ā maintenance mode ā migrate --force ā caches cleared ā workers restarted. |
| 5. Verification | Confirms the data migrations actually landed. On failure the site stays in maintenance mode and a restore recipe is printed. |
Run phases 1ā3 as often as you like, days in advance, with --dry-run.
<a name="track-a"></a>
If you are not already using a releases/ + current layout, create it once:
cd /var/www/unopim
mkdir -p releases
mv your-existing-installation releases/2.1.6
ln -s releases/2.1.6 current
Point your web server at /var/www/unopim/current/public and reload it. Nothing
has changed yet ā the same code is serving from a new path.
cd /var/www/unopim/releases
curl -fL -o unopim.zip https://github.com/unopim/unopim/archive/refs/tags/v3.0.0.zip
unzip -q unopim.zip
mv unopim-3.0.0 3.0.0
rm unopim.zip
cd /var/www/unopim/releases/3.0.0
cp ../2.1.6/.env .env
cp -a ../2.1.6/storage/app/. storage/app/
Do not copy config/, composer.json, vendor/, or
storage/framework/. Phase 2 tells you exactly what to merge from the old
config/ and composer.json.
composer install --no-dev --optimize-autoloader
php artisan unopim:upgrade --dry-run --from=/var/www/unopim/releases/2.1.6
Read all three phases. Fix any preflight failure, merge what phase 2 lists, and use the phase 3 estimate to book your maintenance window.
php artisan unopim:upgrade --from=/var/www/unopim/releases/2.1.6
Stop your queue workers first if they are managed outside Supervisor. The command aborts on its own if a tracked import or export job is still running.
cd /var/www/unopim
ln -sfn releases/3.0.0 current
sudo systemctl reload php8.4-fpm
sudo systemctl reload nginx # or: sudo systemctl reload apache2
sudo supervisorctl restart unopim-worker
<a name="track-b"></a>
Same model, using a worktree instead of an archive. Useful when you carry local commits.
cd /var/www/unopim/releases/2.1.6
git fetch --tags
git worktree add ../3.0.0 v3.0.0
Then continue from Track A step 3. Your local commits stay on their branch; merge or rebase them onto the tag in the new worktree before running the upgrade, and let phase 2 confirm nothing was left behind.
<a name="track-c"></a>
Containers are already side-by-side: a new image tag is a new release. There is no file copying, and the archive instructions above do not apply.
compose.yaml now runs UnoPim from the published images and needs no .env.
The stack that builds from a checkout moved to compose.dev.yaml
(compose.dev.apache.yaml for Apache, compose.mysql.yaml for MySQL on the
image stack). docker-compose.yml and docker-compose.hub.yml remain as thin
includes.
A bare docker compose up in a clone now resolves the image stack rather
than building. If you were building from a checkout, switch explicitly:
docker compose -f compose.dev.yaml up -d
New Docker environments default to PostgreSQL. Changing the profile does not
migrate your data. Keep your existing COMPOSE_PROFILES, DB_CONNECTION,
DB_HOST and DB_PORT values exactly as they are.
docker compose pull
docker compose run --rm app php artisan unopim:upgrade --dry-run
docker compose run --rm app php artisan unopim:upgrade
docker compose up -d
The database lives in a volume, so it survives the image swap. Rollback is re-pinning the previous image tag and restoring the database dump.
<a name="after-the-upgrade"></a>
The upgrade deliberately does not reindex: on a large catalog it runs far longer than the migration itself, and search falls back to the database in the meantime rather than failing. Run it once the site is back up:
php artisan unopim:elastic:clear
php artisan unopim:category:index
php artisan unopim:product:index
For a small catalog you can fold it into the upgrade with --with-reindex.
Re-apply your config/ changes and re-add your own Composer requirements.
Add the new .env keys ā they fall back to defaults, but you should set them
deliberately:
| Key | Why it matters |
|---|---|
CORS_ALLOWED_ORIGINS, CORS_ALLOWED_METHODS | Defaults to *. Restrict it in production. |
REST_API_RATE_LIMIT | Requests per minute against the REST API. |
NOTIFICATIONS_ENABLED | Turns the notification system on. |
MICROSOFT_SSO_* | Only needed if you enable Microsoft SSO. |
Remove RESPONSE_CACHE_ENABLED. It no longer has any effect.
The upgrade runs optimize:clear and does not re-cache, so the new release
starts from source. If your deployment relies on cached config, rebuild it after
cutover:
php artisan config:cache
php artisan route:cache
php artisan view:cache
Every integration now belongs to a robot user. Open each one in the admin panel, reveal or regenerate its credentials, and update your clients. Existing tokens stopped working the moment the new signing keys were generated.
<a name="rollback"></a>
Because the previous release was never modified, rolling back the application is one command:
cd /var/www/unopim
ln -sfn releases/2.1.6 current
sudo systemctl reload php8.4-fpm && sudo systemctl reload nginx
The database is the part that needs care. The migrations in this release rewrite
data and cannot be reversed ā migrate:rollback will not restore it.
Restore the dump the upgrade wrote:
# MySQL
mysql -u your_db_user -p your_db_name < storage/app/upgrade-backups/<dump>.sql
# PostgreSQL
psql -U your_db_user -d your_db_name -f storage/app/upgrade-backups/<dump>.sql
Then bring the old release out of maintenance mode:
cd /var/www/unopim/releases/2.1.6
php artisan up
<a name="how-long"></a>
--dry-run gives you a figure for your own data. As a planning guide:
| Catalog size | Migration window | Elasticsearch reindex |
|---|---|---|
| Under 100k products | 5ā15 minutes | Minutes, can run inline |
| 100k ā 1M products | 20ā60 minutes | 30 minutes to a few hours, after cutover |
| Over 1M products | 1ā3 hours | Several hours, after cutover |
The migration adds indexes to products and product_completeness, which holds
long locks on large tables, and backfills product associations row by row. Both
scale with catalog size. The Elasticsearch reindex runs after the site is back
up and does not extend your downtime.
<a name="command-reference"></a>
php artisan unopim:upgrade [options]
| Option | Effect |
|---|---|
--from=PATH | Path to the previous release directory. Enables the full drift report ā without it, config files and Composer requirements cannot be compared. |
--dry-run | Runs phases 1ā3 and stops. Changes nothing. |
--with-reindex | Rebuilds Elasticsearch indexes inline. Ignored on catalogs above the configured product limit. |
--skip-backup | Skips the database dump. Only for installations backed up by infrastructure ā you lose the automatic restore point. |
--force | Continues when the installed release is older than the supported floor. Untested territory; take a backup you trust. |
Thresholds ā the supported version floor, sizing throughputs, required
extensions, the inline reindex limit ā live in config/upgrade.php and can be
tuned per installation.
<a name="roadmap"></a>
UnoPim currently ships its packages inside the application repository, which is
why upgrading means replacing a directory. The direction of travel is to publish
packages/Webkul/* as versioned Composer packages so the application becomes a
thin skeleton and upgrades become composer update.
That is a repository restructure, not a release note, and it is not part of this release. The side-by-side layout above is the model either way: when the split lands, only steps 2 to 4 get shorter.