17 KiB
Upgrade from Laravel 10 to Laravel 11, for any server and platform
This guide upgrades an installation of this application from Laravel 10 (any older version of the
main branch) to Laravel 11 (the laravel-11 branch). It is the same for every platform that runs this
code (Timebank.cc, Uuro or any other) and for every server: production, development or test.
Check where you start:
php artisan --version # Laravel Framework 10.x: this guide applies
git branch --show-current # main
The upgrade is done by scripts/upgrade.sh. The script checks the server, makes a verified database backup,
runs the new deploy.sh while the site is in maintenance mode, and lets you test before visitors see the new
version. If something is wrong, it rolls back.
Plan about 30 minutes. The site itself is offline (maintenance page) for about 5 to 15 minutes.
Upgrade one server at a time. Start with a test or development server if you have one.
Status (2026-10-19). Timebank.cc runs Laravel 11 since 2026-10-15. The last installation on Laravel 10 is the Uuro
staging server, which runs a branch of its own (uuro) instead of main. It moves to main during the upgrade
with upgrade.sh --leave-branch, after its config, logos and favicons are copied into its platform folder platform/.
Its day-by-day checklist is RUNBOOK_LARAVEL_10_TO_11.md; this guide explains each step.
1. Before you start
Requirements
| What | Needed | How to check |
|---|---|---|
| PHP for the website and the command line | 8.2 or newer (Laravel 11 needs it) | php -v; the script also asks the website itself |
| Git branch | main |
git branch --show-current |
| tmux or screen | so a dropped SSH connection does not stop the upgrade halfway | tmux -V (capital V); install with apt-get install -y tmux |
| Free disk space | about twice the size of vendor/ plus 2 GB |
df -h . and du -sh vendor |
| MySQL user that may drop and create the database (root or a deployment user) | for migrations, and for a database restore during a rollback | see .env below |
| Time and a quiet moment | not during a busy period or a mailing |
Local changes to tracked files stop the upgrade. Two kinds of change are expected and ignored:
public/build/, whichdeploy.shrebuilds on every server- file modes (the executable bit), which
deploy.sh'schmod -R 775changes
Check with:
git -c core.fileMode=false status --short --untracked-files=no -- . ':(exclude)public/build'
This should print nothing. If it lists files, find out why they were changed on the server before you continue.
Check .env
These keys are read by deploy.sh and scripts/upgrade.sh. Show them without printing passwords:
grep -E '^(APP_URL|DEPLOY_ENVIRONMENT|DEPLOY_APP_DIR|DEPLOY_SERVER_TYPE|DEPLOY_WS_URL|TIMEBANK_CONFIG|TIMEBANK_THEME|DB_DEPLOY_USERNAME|SCOUT_QUEUE_TIMEOUT)=' .env
grep -c '^DB_DEPLOY_PASSWORD=.' .env # 1 = set
| Key | Purpose |
|---|---|
APP_URL |
The site's address. The script uses it to ask the website which PHP version serves it. |
DEPLOY_ENVIRONMENT=server |
deploy.sh runs in server mode. Server mode also needs DEPLOY_APP_DIR, DEPLOY_SERVER_TYPE (dev or prod) and DEPLOY_WS_URL. |
TIMEBANK_CONFIG |
The platform config, config/<name>.php. It can be any name, e.g. uuro. |
TIMEBANK_THEME |
timebank_cc, uuro, vegetable or yellow. |
DB_DEPLOY_USERNAME, DB_DEPLOY_PASSWORD |
The MySQL user for migrations and database restores. Without them you are asked for one: have the MySQL root password ready. |
SCOUT_QUEUE_TIMEOUT=1800 |
Recommended: lets the nightly search re-index finish without the queue worker being killed. |
Know your restart commands
After the deploy you restart long-running processes, because they still hold the old code. The script suggests commands for the services it finds. Typically:
sudo systemctl restart <your Reverb service>
sudo systemctl reload apache2 # or nginx
sudo systemctl reload php8.3-fpm # if the site runs on PHP-FPM
deploy.sh restarts the queue workers itself.
2. Dry run (changes nothing)
Log in to the server, start tmux and go to the application root:
tmux new -s upgrade # reconnect later with: tmux attach -t upgrade
cd /var/www/<your app>
git fetch origin
git show origin/laravel-11:scripts/upgrade.sh > /tmp/timebank-upgrade.sh
bash /tmp/timebank-upgrade.sh --dry-run --target origin/laravel-11
Always run the script from /tmp, taken from the target branch. The copy in your installation is older or
absent.
A good dry run ends with Dry run: nothing was changed and shows:
1. Checks: the PHP version is accepted for both the command line and the website, there is enough disk space, andMade on this server, differ from git: ...(if shown) is fine: deploy.sh makes those files again. A changedpublic/storageis never reported: it is this server's own storage link.2. Plan: current and target commit,Laravel major upgrade (10 -> 11), and the number of new migrations. If any migration changes existing tables, a rollback after going live needs the database restore.
If it stops instead, see Troubleshooting.
3. The upgrade
bash /tmp/timebank-upgrade.sh --target origin/laravel-11
The script goes through eight steps and waits for you at each question. What to answer:
| Step | Question or action | Answer |
|---|---|---|
| 1. Checks | Not running inside tmux or screen... Continue anyway? (only outside tmux) |
Stop (N) and start tmux. |
| 2. Plan | Upgrade this installation to <commit>? |
y |
| 3. Backup | Make a database backup now with scripts/backup-database.sh? |
y. The backup is verified and kept with the upgrade state, together with a snapshot of vendor/ and public/build. |
| 4. Maintenance | The script prints a command with a secret. | In a second tmux window (Ctrl-b c), in the application root, run that command: php artisan down --retry=60 --secret=<secret>. Then press Enter in the first window. |
| 5. Deploy | The new deploy.sh runs and asks its own questions. |
See below. |
| 6. Version check | Shows Laravel Framework 11.x. |
Nothing to do. |
| 7. Restart | Shows restart commands for this server. | Run them in the second window, then press Enter. |
| 8. Go / no-go | Go live (g) or roll back now (r)? |
First test (see below), then g or r. |
Questions from deploy.sh (step 5)
New configuration keys available ... Would you like to merge new keys now? (y/N): pressy(a single key press). For each config file it then asksMerge these keys into <file>? (yes/no): typeyes. New settings are added and your own values stay as they are. A backup of each file goes tostorage/config-backups/.MySQL username [root]/MySQL password: only whenDB_DEPLOY_*is not in.env.You have uncommitted changes. Continue anyway? (y/n): this should not appear any more. If it does, check whatgit statuslists before you answer.
If deploy.sh fails, the script stops and the site stays in maintenance mode. See
Troubleshooting.
Test before going live (step 8)
The site is still in maintenance mode. Open <APP_URL>/<secret> once in your browser: that browser can then
use the site while visitors still see the maintenance page. Check:
- log in, and switch to an organisation or bank profile
- a payment
- a chat message with an image
- a poll, if your platform uses polls
- search
- the error log:
tail -n 50 storage/logs/laravel-$(date +%F).log(orlaravel.log)
Then:
- All fine: answer
g. Runphp artisan upin the second window, then press Enter. - Something wrong: answer
r. The rollback starts right away and may also restore the database, which is safe at this point because nothing went live.
4. After the upgrade
-
Health check (once this version has it):
php artisan upgrade:verifychecks profiles, permissions, white-label settings, payment protection, search and background services, read-only. Run it before and after an upgrade; exit code 1 means a check failed. -
Platform settings (all platforms). See which new settings arrived, and check that nothing is waiting:
php artisan config:merge --all --dry-run # should report nothing newOn a platform other than Timebank.cc, review these in its platform config (
platform/config.php, orconfig/<TIMEBANK_CONFIG>.phpwithout a platform folder). They came from the default template:Setting Default for a new platform Meaning platform_translations.*your existing names Platform name, currency and terms per language. mail.*your existing addresses Sender and recipient addresses. local_pagesall falseTimebank.cc's city pages ( the-hague,lekkernassuh,amst-brus-lisb). A page that is off has no URL, and its footer link disappears.android_app.enabledfalseThe Android app page and the app's update check ( /app/version).account_info.*_self,*_privilegedtrueWhich balances and totals owners and privileged viewers (admins, banks) see. A setting a config file does not have yet keeps its old behaviour until it is merged.
-
Email logo, favicons and preview image (platforms other than Timebank.cc; all branding is described in MULTI_PLATFORM_INSTANCES.md, sections 3 and 6). Without its own logo file, a platform gets a text logo with its name in emails and PDFs. Put the files in the platform folder (Uuro's runbook copies them there from its branch):
platform/public/mail-logo.png dark logo, for white backgrounds (PNG, about 900×700, transparent) platform/public/mail-logo-inverted.png light logo, for the newsletter header platform/public/favicon.ico, favicon-32x32.png, favicon-16x16.png, apple-touch-icon.png platform/public/og-image.png preview for shared links (about 1200×630)deploy.shlinks them at/platform(php artisan platform:link);php artisan platform:checkconfirms it. -
Workers and scheduler. Check that the queue workers run, with all queues:
sudo systemctl status 'timebank-*' --no-pager | grep -E 'Active|●' -
The next days. Watch the error log. When all is well, free the disk space of the snapshot. The script prints the exact command:
rm storage/app/upgrades/<stamp>/vendor.tar.gzKeep the database backup in that folder a little longer.
Routine deploys afterwards
./deploy.sh -e server keeps working as before. It pulls main. Until main itself has been
fast-forwarded to the Laravel 11 version, that pull finds nothing new on an upgraded server, which is
harmless.
main is fast-forwarded to Laravel 11 only after every server has been upgraded, so after Uuro: a Laravel 10
server's own deploy.sh would otherwise pull Laravel 11 without the protection of this script. From Laravel 11
on, deploy.sh stops a routine server deploy that would raise the Laravel major version.
In its "pull conflict" question, never choose option 2 (reset and git clean): it deletes untracked files,
such as uploads in storage/.
5. Rollback
Roll back with the copy of the script in the upgrade's state folder. The script printed the exact command; without a stamp it takes the latest upgrade:
bash storage/app/upgrades/<stamp>/upgrade.sh --rollback
It asks for confirmation, switches on maintenance mode and resets the code to the previous commit. It then
restores vendor/, bootstrap/cache and the server's own public/build from the snapshot (no download
needed), clears the caches and tells you what to restart.
The database:
- Before going live, the rollback offers to restore the backup. That is safe.
- After going live, it rolls back the code only, unless you type
RESTORE. A restore deletes everything written since the backup: payments, messages, votes and new profiles. Usually the code-only rollback is right, because the old code ignores new tables.
The restore connects as DB_DEPLOY_USERNAME, or asks for a MySQL user that may drop and create the
database.
After a rollback, do not run ./deploy.sh until the problem is fixed: it would pull the version you just
left.
6. What the upgrade changes
- Laravel 10 to 11. Laravel 11.57 and the matching package versions (Livewire 3.8 among them); the application code was adapted. No database changes are needed for it. PHP 8.2 or newer is required.
- Platform config. A platform may use its own config name:
TIMEBANK_CONFIG=myorgwithconfig/myorg.php.deploy.shcreates the file fromconfig/timebank-default.php.examplewhen it is missing, andconfig:mergekeeps it up to date. A config file that is not in git is excluded from git on that server (.git/info/exclude), so deploys no longer report it as an uncommitted change. - Settings for platform-specific pages.
local_pagesandandroid_app, see After the upgrade. - Branding. Texts that showed "Timebank" or "Timebank.cc" now use the platform name. Emails use the theme's logo or the generated text logo.
- Deploys.
deploy.shno longer stops at a server-builtpublic/buildor at mode-only changes, and stops a routine server deploy that would raise the Laravel major version. - Platform folder. Everything platform-specific may live in
platform/(outside git): config, theme, text and template overrides, logos, favicons and the preview image. See MULTI_PLATFORM_INSTANCES.md, section 3. A platform branch of its own is no longer needed;upgrade.sh --leave-branchmoves an installation off one. - Open Graph tags (preview of shared links) and optional favicon formats for every platform.
- Admin roles. The legacy roles
Admin\{id}\adminare replaced by the roles fromadmin_roles(database:updaterunsadmin-roles:migrate).
7. Troubleshooting
| Message | Meaning and what to do |
|---|---|
There are local changes to tracked files |
Files were edited on the server. Look at them with git diff. Commit the change upstream, or save a copy and git checkout -- <file>. |
This installation has commits that are not on <target> |
The server has local commits. A fast-forward is not possible: ask the developers. |
This server's PHP does not meet the requirements / This site is served by PHP x |
Install PHP 8.2 or newer (with the extensions listed) and switch the site to it first. |
Could not ask the site which PHP serves it |
APP_URL is unreachable from the server itself. The script lists the PHP versions it finds and asks you to confirm. |
Only N MB free |
Free disk space (old backups in backups/, old snapshots in storage/app/upgrades/). |
deploy.sh stopped with an error |
The site stays in maintenance mode. Read the error above it. Either fix the cause, run bash deploy.sh -e server again and continue with steps 6 to 8 by hand (restart, test through the bypass URL, php artisan up), or roll back with the command the script printed (database restore allowed: nothing went live). |
⊘ <file>: N new keys available (not applied) or not merged - ... |
config:merge did not change that file; it keeps working with the old settings. Run php artisan config:merge <name> --dry-run to see the keys, and add a reported key by hand from the .example file if needed. |
✗ <file>: Merged config is invalid |
The merge checked its own result and restored the file. Nothing changed. Report the message to the developers. |
Restore fails with Access denied ... DROP |
Add DB_DEPLOY_USERNAME and DB_DEPLOY_PASSWORD to .env, or restore by hand: gzip -dc <backup>.sql.gz | mysql -u root -p |
| The site still shows the maintenance page | Run php artisan up in the application root. |
apt-get update: GPG error: https://packages.sury.org/php ... EXPKEYSIG |
The signing key of the Sury PHP repository expired: no PHP updates arrive and apt-get install stops. Install Sury's key package and point the source at it (backup first):cp -a /etc/apt/sources.list.d/php.list /root/php.list.bak-$(date +%F)curl -fsSLo /tmp/sury-keyring.deb https://packages.sury.org/debsuryorg-archive-keyring.deb && dpkg -i /tmp/sury-keyring.debecho "deb [signed-by=/usr/share/keyrings/debsuryorg-archive-keyring.gpg] https://packages.sury.org/php/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/php.listapt-get update |
All output of an upgrade and a rollback is logged in storage/app/upgrades/<stamp>/ (upgrade.log,
rollback.log), together with state.env. Send these files along when you ask for help.