Laravel
A Laravel app is one surface: its database, its .env and storage/app
in one snapshot, with one drill verdict. The CLI runs on the host the app runs on and reads
the connection from .env at every backup, so a rotated password needs no change.
Set it up
From the app, with Composer. For a host where the app's user has no root, such as Forge, Ploi or shared hosting. Laravel 10 or later, PHP 8.1 or later.
safegrd:install downloads the CLI and checks it against the release's
checksums.txt, prints a code to approve in the console, adds the app and takes the
first backup. Laravel's scheduler then runs php artisan safegrd:run every five
minutes, which backs up when the app is due and drills when a drill is due, so the host needs the
scheduler's cron entry. The package hands the CLI the connection as Laravel resolved it, so a
password set in the host's environment and not in .env works. The package's
options are in its README.
On the host, with the CLI. For a server you manage. Enroll first, so the key is made with the custody you choose, then add the app as the user it runs as:
init --laravel checks the app, names the connection it found, and adds the surface
to the config. Its id is APP_NAME as a slug unless --surface-id names
one. The daemon backs it up on its schedule and drills it. To use Laravel's scheduler instead of
a service, init prints the line for routes/console.php.
The root is the app's root, where artisan and .env are. Run the CLI
as a user that can read .env and storage/app, usually the user PHP
runs as (forge on a Forge server, www-data on Debian). Laravel Vapor and
Laravel Cloud have no server for the CLI to run on.
What is captured
| Part | How |
|---|---|
| The database | As the engine's own surface backs it up: mysqldump for MySQL and MariaDB,
pg_dump and binary COPY for PostgreSQL, a page copy for SQLite. Taken first |
.env | Whole, APP_KEY included, sealed like everything else |
storage/app | Every file with its mode and time. A file that did not change since the last backup is not read or uploaded again |
database/migrations and database/schema | A few kilobytes, so the drill can compare them with the migrations table |
Left out: vendor, node_modules, storage/framework,
storage/logs, bootstrap/cache and the rest of the code, which lives in
git. In storage/app: backup-temp, livewire-tmp, and the
zip archives spatie/laravel-backup writes there. The surface's excludes adds more,
as paths from the app's root such as storage/app/exports.
Why the .env is in the backup
Laravel encrypts cookies, signed URLs, encrypted casts and anything passed to
Crypt with APP_KEY. A database restored without the key it was
written with has those columns and nothing to read them with. The .env is
encrypted on the host before upload, with the host's key: with a SafeGrd-managed key, SafeGrd
keeps that key sealed and releases it only to your enrolled hosts; with a customer-managed key,
only you can decrypt the backup. Keep a copy of a customer-managed key somewhere safe.
Where the connection comes from
DB_URL (or DATABASE_URL) when it is set, otherwise
DB_CONNECTION with DB_HOST, DB_PORT,
DB_DATABASE, DB_USERNAME and DB_PASSWORD, as Laravel's
own config reads them. mysql, mariadb, pgsql and
sqlite are backed up. A SQLite path is relative to the app's root, and an unset
DB_DATABASE means database/database.sqlite.
To back up as a different role than the app's (a read-only one, say), or a connection set
outside .env, give the surface its own database_url or
credential block (init --laravel --database-url env:VAR writes one).
A migrations table with another name goes in migrations_table. The .env is still
backed up whole. The password read from .env is never written to the config, the
state directory or SafeGrd.
| The backup refuses | Why, and what to do |
|---|---|
No .env | The APP_KEY would not be in the backup. Create one, or back up the database alone as a database surface |
No DB_CONNECTION | Laravel's default changed in version 11, so SafeGrd does not guess. Set it, or give the surface its own database_url |
sqlsrv | SQL Server is not backed up yet |
DB_SOCKET | The CLI connects over TCP. Set database_url on the surface |
With FILESYSTEM_DISK=s3 the uploads are in a bucket, not in
storage/app. The backup still takes the database and the .env and
prints a warning. Back the bucket up as a files surface with
an object_store block.
What a Fire Drill checks
On every plan, in memory (a SQLite database is opened from a temporary copy that is deleted after):
- The database passes its engine's drill: the dump is complete and every table's rows match the manifest.
- Every file reads back and matches its SHA-256.
- AppKey: the
.envin the snapshot holds anAPP_KEYof the length Laravel's cipher takes (32 bytes for AES-256, 16 for AES-128). - Migrations: the dump holds the
migrationstable with the rows the backup read, and at least one for each migration file that had run. A migration not yet run when the backup was taken is named in the backup's output.
One failing check fails the drill. Booting the restored app is not part of the drill yet.
Restoring an app
The database goes into an empty database of the same engine, and the .env,
storage/app and the migration files into an empty directory. Then, in a checkout
of the app at the version the restore prints:
- Copy
.envandstorage/appfrom the restored directory into it. - Point
DB_HOST,DB_DATABASE,DB_USERNAMEandDB_PASSWORDat the restored database. - Run
php artisan config:clear,php artisan storage:linkandphp artisan migrate:status.