Skip to content

Upgrading HiLMS

Every upgrade is the same three moves: back up, put the new code in place, run hilms:upgrade — which a Docker installation does by itself as the new container starts. The command is idempotent, so running it twice is safe.

docker compose exec app php artisan backup:run # Docker
php artisan backup:run # manual

The archive lands on the backups disk (storage/app/backups). Copy it off the machine before an upgrade you are unsure about, or set BACKUP_S3_ENABLED=true once and let the nightly job do it.

HILMS_VERSION in .env is the tag the stack runs, so an upgrade starts by moving the pin:

cd /srv/hilms
$EDITOR .env # HILMS_VERSION=0.7.0, the version you move to
docker compose pull
docker compose up -d
docker compose logs -f app # wait for "HiLMS is up to date"

The new app container runs hilms:deploy as it starts, which finds an installed database (an administrator exists) and runs hilms:upgrade — every step described under Manual below, with --no-interaction --force and the caches warmed. Horizon and the scheduler are replaced with the new image, start once the application reports healthy and pick up the new code. If the upgrade stops, the container does not come up healthy and the log names the reason; fix it and run docker compose exec app php artisan hilms:upgrade by hand, which is also the way to run it again at any time. Compose also hands .env to the containers, so the pin is what artisan about reports afterwards. Left commented out, HILMS_VERSION takes whatever latest points at, which leaves a rollback nothing to aim at.

The stack files themselves come out of the tag as well: when compose.yaml changed between the two versions, git show v0.7.0:compose.yaml on your machine and copy it over before pulling. docs/install/docker.md has the commands.

cd /var/www/hilms
php artisan down --render=errors::503 # optional, or let hilms:upgrade --down do it
git fetch --tags
git checkout v0.7.0
composer install --no-dev --optimize-autoloader
npm ci && npm run build
php artisan hilms:upgrade
sudo systemctl reload php8.5-fpm # opcache.validate_timestamps is 0

hilms:upgrade checks the machine, migrates, seeds any new roles and permissions, syncs the themes’ built-in styles and compiles every style again, gives any language with no menu of its own a header and a footer, links public storage, generates missing API keys, republishes the package assets, counts the media files not yet on the disk their owner names, warms the caches, asks the workers to restart and runs the health checks. Add --down to have it hold the maintenance page for you, and --no-optimize while debugging.

Three of those steps change data, and one only counts:

  • Seeding prunes dead permissions. A permission no policy consults any more is removed, so a role that held one loses a no-op. Nothing a role can actually do changes.
  • Every style is compiled again against the theme as it is now. A theme’s built-in style files are synced into their styles — a new file becomes a style, a changed one updates it, a removed one takes its style with it — and a theme with no active style gets its first built-in activated (the first that meets it, where accessibility is enforced), so a new installation reads HiLMS WCAG as the style it draws. A value a style holds for a token the theme no longer takes is left out of its stylesheet rather than breaking the site; php artisan hilms:theme:styles --check names every such value, and the health check warns about the active style’s.
  • A language with no menu of its own is given one. The header gets the page chosen as the course listing and, for whoever is signed in, the page chosen for students; the footer gets the starter legal pages that exist in that language. A language somebody has already assigned a menu to anywhere is left exactly as it is, and a second run creates nothing. Build your own in Menus in the panel: a menu is assigned to the header or the footer of one language, and a menu with nothing in it draws nothing at all.
  • Media files are counted, never moved. The upgrade says how many files are not on the disk their owner names (“16 files wait to move”), because a file is served from wherever its record says and moving them inside a starting container would keep the site down until every byte had crossed. Moving is the administrator’s act on Settings → Media and storage, or php artisan hilms:media:relocate from the command line (--dry-run shows what it would move). Lesson files are served only through signed links, so a lesson URL somebody bookmarked in an older version stops working — which is the point.

Media moves between this server and an S3-compatible bucket from the panel, with the site running. storage.md is the full version, with what each provider needs and a checklist for afterwards; in short:

  1. Create the buckets and a key with the provider.
  2. Under Settings → Media and storage, press Set up a bucket and follow the guide: it asks for the provider’s own fields, hands out the CORS rule for APP_URL, and checks the bucket before it saves. New uploads go to the bucket from then on; files already uploaded keep working where they are.
  3. Optionally, under Where your files are, press Move N files (…) to …. The files move in the background on the media-moves queue, one at a time, each removed from this server only once it has arrived whole; the section shows the progress and names any file that stayed.

Moving back is the same: Stop using the bucket, then Move … back to this server. The bucket’s setup stays while files live in it, so they can be fetched back; Forget the bucket clears it once the bucket is empty.

php artisan about --only=hilms
php artisan health:check

Both should be green. The panel’s Health page shows the same results without a terminal.

A block may gain a required field between versions, and every copy of that block written before the change is then a block missing something. hilms:upgrade counts them as its last step and says so; nothing is repaired, because which answer an editor meant is theirs to give.

php artisan hilms:blocks:check

lists every page and lesson holding one — where it is, which block, which field and why — and exits 1 when it finds any (--json for anything reading it instead of a person). Nothing is broken meanwhile: such a page is served exactly as it was, and only an editor who opens it is stopped, with the block marked on the canvas and the reason on it. The Health page carries the same count as a warning, worked out at most every half hour.

hilms:upgrade relinks every theme that publishes a public/ directory and the health check refuses to pass when the active theme does not resolve, so an upgrade that moves the contract says so. Read theming.md before upgrading an installation that runs a theme of its own: a new contract view is a view that theme does not have yet, and hilms:theme:check <name> lists exactly which ones. A theme that renamed or removed a token leaves the styles laid over it without that value: hilms:theme:styles --check says which. A production container caches compiled views, so run view:clear after editing a mounted theme.

hilms:upgrade seeds the language from APP_LOCALE when the table is empty, and never touches it again. languages.md is the operator’s guide to adding a second one.

  1. git checkout the previous tag, or set HILMS_VERSION back and docker compose up -d (the container then runs the older version’s own start-up step).
  2. Restore the database from the archive you made before the upgrade:
unzip 2026-09-10-01-30-00.zip -d /tmp/restore
gunzip -c /tmp/restore/db-dumps/mysql-hilms.sql.gz | mysql -u hilms -p hilms
  1. php artisan hilms:upgrade to re-warm the caches (in Docker, docker compose restart app does it).

A migration that dropped a column cannot be undone by rolling the code back alone; that is what the pre-upgrade backup is for.

HiLMS is MIT-licensed. No replicants were harmed in the writing of these books.