Deployment
Evolve comes with built-in rsync file deployment over SSH. Every deploy builds a new release directory on the server, runs the migrations and optimizations there, and switches a current symlink to it once everything succeeded. The environments a project deploys to are declared in config/evolve.php, see Environments.
Configure SSH connection
Before running your first deployment, configure the SSH connection for your server in the deployment section of config/evolve.php.
Example
'deployment' => [
'environments' => [
'live' => ['type' => 'live', 'target' => 'www'],
'staging' => ['type' => 'staging', 'target' => 'staging'],
],
'server' => [
'user' => env('DEPLOYMENT_USER', 'web161'),
'host' => env('DEPLOYMENT_HOST', 'web.onlime.ch'),
'path' => env('DEPLOYMENT_PATH', 'webhome/public_html'),
'php-bin' => env('DEPLOYMENT_PHP_BIN', 'php84'),
],
...
],This example deploys live to web161@web.onlime.ch:~/webhome/public_html/www. The web server's document root for that host points at www/current/public.
Directory layout on the server
2.12 Since 2.12 an environment's target directory looks like this:www/ the target of one environment
├── current -> releases/20260926143012
├── releases/
│ ├── 20260920101500/
│ ├── 20260925090000/
│ └── 20260926143012/ the live release
│ ├── .env
│ ├── public/
│ │ ├── img/ rendered image variants, per release
│ │ └── page-cache/ static page cache, per release
│ └── storage/
│ ├── app -> ../../../shared/storage/app
│ ├── framework/
│ └── logs/
└── shared/
└── storage/
└── app/ uploads, one copy per environment
└── evolve/uploads/media/Everything under storage/app (media uploads under evolve/uploads, lead attachments, anything a project stores on a local disk rooted there) lives once in shared/storage/app. The rest of a release is its own: code, .env, public/img, public/page-cache, logs and framework caches. A new release is created as a hard-linked copy (cp -al) of the live one, so unchanged files cost no disk space.
Shared storage
2.12 Uploads used to live inside each release.storage/app is excluded from the rsync, so copying the previous release was the only thing carrying uploads forward, and an upload that reached an old release after it had been copied (a PHP worker still resolving the old path, for hours in one observed case) existed in that release only. The purge then deleted it. Shared storage removes that failure: a release is never the only copy of an upload, so purging one cannot lose any.What a deploy does
- Prepare. The new release is created and linked to
shared/storage/appbefore any file is uploaded. If it was copied with a realstorage/appdirectory, that directory is merged into the shared one first. - Upload, migrate, optimize, go live. Unchanged.
- After go-live, every older release is merged and linked too. That picks up what the previous release received while the deploy ran, and a worker that still resolves an old release path now writes into the shared directory.
- Purge. Old releases beyond
keep-releasesare deleted, except the live one and any release that still carries its ownstorage/app. The purge does not run at all when step 3 failed.
The merge uses rsync --ignore-existing --link-dest: files are hard linked, not copied, so it costs neither time nor disk space, and a file the shared directory already holds is never overwritten.
The first deploy with 2.12
There is nothing to configure and no manual migration. The first deploy with 2.12 migrates the environment: it creates shared/storage/app, moves the uploads of the new release and then of every older release into it, and replaces each storage/app with the symlink. current/storage/app/... keeps resolving, so evolve:env:setup, the Envoy media tasks and the page cache work unchanged.
Verify it on the server:
php artisan evolve:ssh live
ls -l current/storage/app # app -> ../../../shared/storage/app
find releases -maxdepth 3 -path '*/storage/app' ! -type l
# no output: every release is linked
ls -d releases/*/storage/app.migrated # no output: no move was interrupted
du -sh shared/storage/app # roughly the size of the old uploadsThen upload a test image in the panel and check that it appears under shared/storage/app/evolve/uploads/media/.
Backups and scripts on the server
Anything that reads releases/<release>/storage/app directly (backup jobs, sync scripts, cron tasks) must read shared/storage/app or go through current/storage/app from now on.
When linking fails
If an older release cannot be linked after go-live, the deploy says so:
Releases could not be linked to the shared storage. The new release is live and uses shared/storage/app, but an older release still carries its own storage/app. No releases were purged; run the deploy again or check the directory on the server.
The site is fine at that point. Run the deploy again; if it keeps failing, look for a release whose storage/app is a real directory (the find command above) and for a leftover storage/app.migrated, typically caused by permissions or a full disk.
Rollback and shared storage
php artisan deploy --rollback links every release to the shared directory first, then switches current to the previous release and deletes the one rolled back from. The release rolled back to therefore serves the shared uploads, including those that arrived after it was replaced, and the deleted release cannot hold the only copy of anything. A rollback to a release deployed before 2.12 is safe for the same reason: it is linked before it goes live.
Deploying an older Evolve version afterwards keeps working too: that version copies the live release with cp -al, which copies the storage/app symlink as a symlink.
Deployment
Start a full deployment with the deploy command. Without an argument it deploys to DEPLOYMENT_DEFAULT (live unless set):
php artisan deploy
php artisan deploy stagingA deploy to live asks you to type the environment name; --force skips the confirmation on other types only.
Simulating a deployment
Use the --dry-run flag to simulate the deployment without actually deploying anything.
php artisan deploy --dry-runDeploy a single file or folder
php artisan deploy --file=public/.htaccessDeploy without purging old releases
Use the --keep-releases flag to keep all releases on the server.
php artisan deploy --keep-releasesDeploy without flushing caches
Use the --keep-caches flag to skip optimize:clear on the server. The page cache of the new release is cleared either way.
php artisan deploy --keep-cachesWhat runs on the server
In the new release, before it goes live: migrate --force, evolve:clear-page-cache, optimize:clear (unless --keep-caches), optimize and event:cache. After go-live, the commands in hooks.after run in current. Hooks are plain shell commands, so name the PHP binary of the environment:
'hooks' => [
'before' => [
// run locally before deploying, e.g. 'yarn --cwd theme build'
],
'after' => [
'php84 artisan evolve:images:warm --missing',
'php84 artisan evolve:rebuild-page-cache',
],
],evolve:images:warm --missing queues image variants that do not exist yet (Image Rendering); evolve:rebuild-page-cache warms the page cache the deploy just cleared (Page Cache).
Cron jobs
Onlime has no process supervisor, so everything that must run in the background runs from cron. Two entries per environment, both pointing at current so they follow every deploy:
# Laravel scheduler: health checks, Shopify import and purge, pruning
* * * * * cd ~/webhome/public_html/www/current && php84 artisan schedule:run >> /dev/null 2>&1
# Queue worker: every queued job (only needed when QUEUE_CONNECTION is not sync)
* * * * * cd ~/webhome/public_html/www/current && php84 artisan queue:work --stop-when-empty --max-time=55 >> /dev/null 2>&1Adjust the path to the environment's target and php84 to its php-bin. The worker starts every minute, works until the queue is empty or 55 seconds have passed, and exits, so two workers never overlap for long and a new release is picked up within a minute.
Switching away from sync
Once QUEUE_CONNECTION is not sync, every queued job waits for the worker: image conversions, image metadata, server side tracking events, the scheduled Shopify product and collection import, checkout recovery and back in stock mails. Add the worker line in the same step as the .env change.
To verify: crontab -l over SSH lists both lines. /evolve/health shows the Scheduler check green, and the Queue check green once QUEUE_CONNECTION is not sync (see Health Checks).
A queue needs a jobs table
QUEUE_CONNECTION=database stores jobs in the jobs table. New Laravel skeletons ship its migration; an older project creates it with php artisan make:queue-table and migrates. The cache store must support locks (file, database or redis all do).
Rollback
Use the --rollback flag for rolling back the latest release. See Rollback and shared storage for how uploads are handled.
php artisan deploy --rollbackOpen server target with SSH
Use the evolve:ssh command to open a new SSH connection to your configured server. The starting working directory is your configured deployment target.
php artisan evolve:ssh
php artisan evolve:ssh stagingInspecting a remote environment
Read-only commands that answer the usual "what is the server doing" questions in one SSH round trip, without logging in:
| Command | Shows |
|---|---|
evolve:remote:status {environment} | the health of the live release: current release, PHP, queue and scheduler state |
evolve:releases {environment} | the release directories on the server and which one current points to |
evolve:logs {environment} | the tail of the application log of the current release |
evolve:remote:cat {environment} {path} | one file from the current release, for example a config file or the .htaccess |
evolve:artisan {environment} {command} | an allowlisted read-only artisan command on the server, such as about or route:list |
evolve:env:show {environment} | the server .env with secrets masked, see Environments |
Anything that writes goes through deploy, evolve:env:setup, evolve:db:copy or the Envoy tasks below, each with the confirmation its environment type demands.
Envoy tasks
Evolve ships Laravel Envoy tasks for moving media, databases and .env files between your machine and the server. Projects created from the starter already have them. Otherwise create an Envoy.blade.php in the project root with one line:
@import('vendor/racerfish/evolve/core/bin/Envoy.blade.php')Run them with php vendor/bin/envoy run <task> (an alias such as alias envoy="php vendor/bin/envoy" helps).
| Task | What it does |
|---|---|
media:down | Download all media from the server |
media:up | Upload all local media to the server |
database:down | Download the database from the server |
database:up | Upload the local database to the server |
env:down | Download the .env file from the server |
env:up | Upload the local .env.[server]-[target] file to the server |
server:env | Show the current .env file on the server |
server:migrate | Run migrations on the server with --force |
server:clean | Clear and rebuild all caches and compiled files on the server |
server:diff --file=path/to/file | Compare a local file to the file on the server |
database:down and database:up replace the database on the receiving side, so the local database often holds the only copy of live data. Treat it accordingly. Both tasks forget the settings cache on the receiving side since 2.16, see Settings Store.