safegrd / laravel
Back up a Laravel app with SafeGrd: the database, .env and storage/app in one encrypted backup, locked against deletion and test-restored on a schedule.
Requires
- php: ^8.1
- guzzlehttp/guzzle: ^7.5|^8.0
- illuminate/console: ^10.0|^11.0|^12.0|^13.0
- illuminate/http: ^10.0|^11.0|^12.0|^13.0
- illuminate/log: ^10.0|^11.0|^12.0|^13.0
- illuminate/support: ^10.0|^11.0|^12.0|^13.0
- symfony/process: ^6.2|^7.0|^8.0
Requires (Dev)
- larastan/larastan: ^2.9|^3.0
- laravel/pint: ^1.13
- orchestra/testbench: ^8.0|^9.0|^10.0|^11.0
- phpunit/phpunit: ^10.5|^11.0|^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Backs up a Laravel app with SafeGrd: its database, its .env and
storage/app in one backup, encrypted on your server, stored locked against deletion, and
test-restored on a schedule to prove it opens.
composer require safegrd/laravel php artisan safegrd:install
safegrd:install downloads the SafeGrd CLI, prints a code to approve in the SafeGrd console,
adds the app and takes the first backup. From then on Laravel's scheduler backs it up and
test-restores it. The app shows up in the console under Hosts.
Laravel 10, 11, 12 and 13 on PHP 8.1 or later, on Linux or macOS (amd64 or arm64). The database is MySQL, MariaDB, PostgreSQL or SQLite.
What is in a backup
| Part | How |
|---|---|
| The database | MySQL and MariaDB with mysqldump (or mariadb-dump), which the host needs. PostgreSQL and SQLite need nothing installed |
.env |
Whole, with the APP_KEY your encrypted columns, cookies and signed URLs need |
storage/app |
Every file with its mode. A file that did not change since the last backup is not uploaded again |
database/migrations, 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.
With FILESYSTEM_DISK=s3 the uploads are in a bucket, not in storage/app. The backup takes the
database and the .env and warns. Back the bucket up as a files surface:
safegrd.dev/docs/surfaces/files.
The drill
A drill decrypts the latest backup and reads it in memory. It passes when:
- the database dump is complete and every table has the rows the backup counted;
- every file matches its SHA-256;
- the
.envholds anAPP_KEYof the length Laravel's cipher takes; - the migrations table has a row for each migration that had run.
The console shows each drill, and alerts you when a drill fails or a backup is overdue: to Slack, Microsoft Teams, Discord, PagerDuty or a webhook URL on every plan, by email on paid plans. The overdue alert is how you hear about a scheduler that stopped running.
Where the connection comes from
On every run the package hands the CLI the connection as Laravel resolved it:
config('database.connections.<default>'), with whatever your host's environment sets, so a
password kept outside .env works. It reaches the CLI in an environment variable, never on a
command line, and is not written to disk. config('database.migrations') names the migrations
table the drill checks.
TLS settings come along with it. For MySQL and MariaDB that is MYSQL_ATTR_SSL_CA in the
connection's options (Laravel's default config reads it from the variable of that name), which
managed databases such as DigitalOcean's and AWS RDS need; with MYSQL_ATTR_SSL_VERIFY_SERVER_CERT
set to false the connection is encrypted without checking the certificate. For PostgreSQL it is
sslmode, sslrootcert, sslcert and sslkey.
Commands
| Command | What it does |
|---|---|
php artisan safegrd:install |
Install the CLI, sign in, add the app and take the first backup |
php artisan safegrd:run |
Back up when due and drill when due. The scheduler runs it every five minutes |
php artisan safegrd:backup |
Back up now |
php artisan safegrd:status |
The last backup, the next one due and the last drill (--json for JSON) |
php artisan safegrd:list |
The backups, with the ids restore takes (--json for JSON) |
php artisan safegrd:doctor |
Check the connection, the CLI, its home and the schedule, then run the CLI's own checks |
php artisan safegrd:upgrade |
Download the CLI release SAFEGRD_VERSION names and put it in place of this app's copy |
php artisan safegrd:cli -- <args> |
The CLI with this app's setup, for example safegrd:cli -- find .env |
safegrd:install takes:
--token=env:SAFEGRD_TOKEN(orfile:/path) to sign in with a personal access token from Tokens in the console, for a deploy script or CI. Without it, it prints a code to approve. With--no-interactionthe token is required.--key-custody=localfor a customer-managed key. The default is a SafeGrd-managed key: SafeGrd keeps it sealed and releases it only to your enrolled hosts, so you can restore even after losing this server. With a customer-managed key only you can decrypt these backups; keep a copy ofstorage/safegrd/.safegrd/keys/daemon.keysomewhere safe.--storage=localto keep the backups in a directory on this host instead of SafeGrd-hosted storage. A project with its own bucket uses that bucket either way.--no-backupto leave the first backup to the scheduler.
Run it as the user Laravel's scheduler runs as (the owner of storage/, such as forge or
www-data), so the scheduled runs can read what it sets up: sudo -u forge php artisan safegrd:install.
It warns when storage/ belongs to another user. A safegrd already on PATH that is too old to
set up a Laravel app is left alone, and a newer one is downloaded for this app.
The scheduler
The package registers safegrd:run with Laravel's scheduler, every five minutes, once
safegrd:install has run, in maintenance mode too. It exits at once when nothing is due, and logs
to storage/logs/safegrd.log, which is moved to safegrd.log.1 once it passes 5 MB. The scheduler
needs its cron entry on the host:
* * * * * cd /path/to/app && php artisan schedule:run >> /dev/null 2>&1
Forge and Ploi each have a setting that adds it.
safegrd:run runs in the background, so a long backup does not hold up the app's other scheduled
tasks. Cron leaves it running when schedule:run returns. A systemd timer does not, by default:
when its service ends, systemd stops the backup with it, and the next runs are skipped until the
run's 3-hour lock expires. Give that service KillMode=process:
[Service] Type=oneshot User=forge WorkingDirectory=/path/to/app ExecStart=/usr/bin/php artisan schedule:run KillMode=process
When a backup fails
safegrd:backup and safegrd:run exit with the CLI's code: 10 when the database or the files could
not be read, 11 when the SafeGrd server could not be reached, 12 when the storage could not be
written, 1 for anything else. A CLI killed by a signal (the out-of-memory killer, say) or stopped by
SAFEGRD_TIMEOUT is a failure with its own message, never exit 0.
A failure is also written as an error to the app's log (SAFEGRD_LOG_CHANNEL, or the default
channel), so the error tracker the app reports to sees it, and dispatched as an event:
use Illuminate\Support\Facades\Event; use SafeGrd\Laravel\Events\BackupFailed; Event::listen(function (BackupFailed $e) { // $e->command, $e->surface, $e->exitCode, $e->message, $e->output (the last lines) });
The SafeGrd console alerts on the same failure, and on a backup that stops arriving at all.
php artisan safegrd:doctor checks the setup. The failures it finds most often:
- The home belongs to another user, because
safegrd:installran as root. Run it again as the app's user, orchown -Rthe home to that user. storageis on a filesystem mountednoexec, so the downloaded CLI cannot run. Installsafegrdelsewhere and setSAFEGRD_BINARY.- No cron entry for
schedule:run. The console says the backup is overdue. schedule:runstarted by a systemd timer withoutKillMode=process:storage/logs/safegrd.logstays empty and the console says the host stopped checking in. See the scheduler above.
Configuration
Every setting has a variable for .env. Publish the file to change them in code:
php artisan vendor:publish --tag=safegrd-config.
| Variable | Default | |
|---|---|---|
SAFEGRD_SCHEDULE |
true |
false to run safegrd:run yourself |
SAFEGRD_SURFACE |
APP_NAME as a slug |
The app's id in the console, chosen at install. Renaming the app later keeps the id |
SAFEGRD_CONNECTION |
the default connection | The connection to back up |
SAFEGRD_HOME |
storage/safegrd |
The CLI's config, key and state. Outside storage/app, so the key is not in the backups it opens |
SAFEGRD_BINARY |
A safegrd already on the host. Unset, one on PATH is used, or safegrd:install downloads one |
|
SAFEGRD_VERSION |
latest |
The CLI release safegrd:install downloads, checked against its checksums.txt |
SAFEGRD_SERVER_URL |
https://safegrd.dev |
The SafeGrd server safegrd:install enrolls with |
SAFEGRD_TIMEOUT |
Seconds a run of the CLI may take before it is stopped. Unset, there is no limit | |
SAFEGRD_LOG_CHANNEL |
the default channel | The log channel a failed backup is written to |
storage/safegrd holds the host's key and token. It gets a .gitignore, and is readable only by
the app's user. With Envoyer or Deployer the app is backed up through the current link, so the
backup follows each deploy, and storage is shared between releases, so the setup is too.
Restoring
php artisan safegrd:list php artisan safegrd:cli -- restore --snapshot <id> --target env:TARGET_URL --target-dir /tmp/app-restored
TARGET_URL names an empty database of the same engine. The .env, storage/app and the
migration files go into the empty directory. Then, in a checkout of the app:
- 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.
The full guide: safegrd.dev/docs/surfaces/laravel.
Not supported
Laravel Vapor and Laravel Cloud, which have no server for the CLI to run on, and SQL Server.
On a server you manage, the CLI can also run as a service without this package:
safegrd init --laravel /var/www/app, then sudo safegrd daemon install.
License
MIT. The SafeGrd CLI it downloads is source-available under the Business Source License: github.com/safegrd/cli.