A migration script run in a shell inherits no limits, so an unexpected memory usage takes the host down rather than the script.
$ systemd-run --scope --user
-p MemoryMax=2G -p CPUQuota=50%
php artisan migrate:backfill
Running scope as unit: run-r8814.scope
# the process is killed at 2G rather than the host
# starting to swap.
# and for anything long: --unit, so it survives the ssh
# session ending
$ systemd-run --unit=backfill --collect php artisan ...
The --unit form is the one that matters for anything long-running, because a script started in an SSH session dies with the connection and a script started with nohup has no supervision and no logs. Running it as a transient unit gives journal output, a status command and a limit, for the cost of one extra command.