Docs / Surfaces / Laravel

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.

composer require safegrd/laravel
php artisan safegrd:install

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:

curl -fsSL https://safegrd.dev/install.sh | sh
safegrd init --laravel /var/www/shop
safegrd doctor
sudo safegrd daemon install

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.

# what init --laravel writes to ~/.safegrd/config.yaml
surfaces:
  - id: shop
    type: laravel
    name: Shop
    roots: [/var/www/shop]

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

PartHow
The databaseAs 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
.envWhole, APP_KEY included, sealed like everything else
storage/appEvery 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/schemaA 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 refusesWhy, and what to do
No .envThe APP_KEY would not be in the backup. Create one, or back up the database alone as a database surface
No DB_CONNECTIONLaravel's default changed in version 11, so SafeGrd does not guess. Set it, or give the surface its own database_url
sqlsrvSQL Server is not backed up yet
DB_SOCKETThe 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):

One failing check fails the drill. Booting the restored app is not part of the drill yet.

Restoring an app

# TARGET_URL names an empty database: mysql://, postgres:// or sqlite:///path/new.sqlite
safegrd restore --snapshot <id> --target env:TARGET_URL --target-dir /tmp/shop-restored

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:

  1. Copy .env and storage/app from the restored directory into it.
  2. Point DB_HOST, DB_DATABASE, DB_USERNAME and DB_PASSWORD at the restored database.
  3. Run php artisan config:clear, php artisan storage:link and php artisan migrate:status.