1 · Pre-flight & provision host
Objective — get three things right before the first byte ships: the server PHP actually satisfies composer.json, the staging branch is current, and the server scaffolding (deploy tree, PHP limits, .env) exists — because a pre-flight failure is cheap and a deploy:vendors failure three minutes into a release is not.
Background
Section titled “Background”Everything in this phase rides on three things being right before the first byte ships: the server PHP actually satisfies composer.json, the staging branch is current, and the server scaffolding (deploy tree, PHP limits, .env) exists. A pre-flight failure is cheap; a deploy:vendors failure three minutes into a release is not.
1. Run the canonical pre-flight
Section titled “1. Run the canonical pre-flight”Do not deploy blind. Preview the full pipeline, then confirm the PHP version the server actually runs — not just the binary Deployer points at.
-
Preview the plan.
Terminal window # Deployer 7 uses --plan; the old --dry-run was removed and errors outdep deploy staging --plan 2>&1 | tail -60# Expected: a ~40–50 task pipeline including the gates that matterYou should see a ~40–50 task pipeline. The exact list depends on enabled features (Atlas, AWS backup, Sentry, Slack/Discord, module migrations, Livewire publish), but the canonical shape includes the gates that matter:
deploy:check_existing_pending ← BLOCKS if staging has pending migrationsdeploy:verify_clear_paths ← BLOCKS if a sensitive file survives clear_pathsdeploy:vendors ← composer install — fails here on PHP mismatchdeploy:verify_migrationsdeploy:smoke_test ← BLOCKS before the symlink switchdeploy:symlink ← atomic cutoverdeploy:health_check ← BLOCKS after symlink; auto-rollback on failure- ✅ The plan previews a sane ~40–50 task pipeline with the gates above present.
-
Triple-check PHP —
composer.json↔bin/php↔ web SAPI.Terminal window # 1. What does composer.json require?grep '"php"' composer.json | head -1# 2. What does deploy.php point bin/php at, and what does that binary report?BIN_PHP=$(grep "set('bin/php'" deploy.php | grep -oE "/[^']+")dep ssh staging "$BIN_PHP -v | head -1"# 3. What does the WEB SAPI run as? (this is what composer install actually uses)dep ssh staging "php -v | head -1"# Expected: all three PHP versions satisfy composer.json's require- ✅
bin/php, the web SAPI, and hPanel all satisfycomposer.json’srequire.php.
- ✅
Then cross-check the control panel: hPanel → Websites → [domain] → Advanced → PHP Configuration. On shared hosting the web-serving PHP is set there per-domain — not by bin/php in deploy.php.
Use the table to read the result of the triple-check:
| Pattern | Meaning | Action |
|---|---|---|
bin/php, web SAPI, and hPanel all match composer.json | Safe to deploy | Proceed |
bin/php matches but web SAPI is lower | Deploy will fail at deploy:vendors | Upgrade PHP in hPanel, wait ~60s, re-check |
bin/php matches but hPanel shows lower | hPanel controls the web SAPI | Fix hPanel first |
bin/php path doesn’t exist on the server | bin/php is stale | Re-run the SSH PHP discovery from Phase 4 |
The three layers and which one actually runs composer install:
flowchart TD CJ["composer.json<br/>requires PHP ^8.x"] BP["deploy.php bin/php<br/>(CLI binary)"] WS["web SAPI php<br/>(hPanel setting)"] CI["composer install<br/>(deploy:vendors)"] WS -->|actually runs| CI CJ -->|must be satisfied by| CI BP -.->|verified, but not the whole story| CI2. Confirm the deploy-safety flags
Section titled “2. Confirm the deploy-safety flags”The recipe disables and enables a few tasks deliberately. Confirm the flags are set the way the pipeline expects before you trust the plan.
-
Confirm the three deploy-safety flags.
Terminal window grep -n "artisan:migrate.*disable" deploy.php # expect: task('artisan:migrate')->disable();grep -n "clear_paths_strict" deploy.php # expect: set('clear_paths_strict', true);grep -n "atlas_enabled" deploy.php # expect: set('atlas_enabled', true);# Expected: each grep prints its matching line- ✅
artisan:migrateis disabled,clear_paths_strictistrue, andatlas_enabledis set.
- ✅
3. Spot-check optional-task gating
Section titled “3. Spot-check optional-task gating”Optional tasks (AWS backup, Atlas, ZajModule migrations, Livewire publish, Sentry/Slack/Discord) must each carry a graceful-skip guard so a feature you haven’t configured can’t hard-fail the deploy. Confirm each guards on missing config.
-
Scan each optional task for a skip guard.
Terminal window for t in deploy:aws_backup deploy:atlas_preflight deploy:zajmodule_migrations \livewire:publish_assets sentry:release slack:notify:success; doecho "=== $t ==="awk -v t="$t" '$0 ~ "task(\047" t "\047" { grab=1 }grab && /^task\(/ && $0 !~ "task(\047" t "\047" { exit }grab { print }' deploy.php \| grep -E "if \(!get\(|if \(empty|NOT_FOUND|return;" | head -3 | sed 's/^/ /'done# Expected: each task prints at least one if (!get('..._enabled')) / if (empty(...)) / NOT_FOUND … return guard- ✅ Every optional task prints at least one graceful-skip guard; none is unguarded.
A task with zero guards may abort the pipeline when its feature isn’t configured — read its full body before you deploy.
4. Prepare the staging branch
Section titled “4. Prepare the staging branch”Bring staging current with develop and push it, then confirm the host config and SSH still resolve.
-
Merge
developintostagingand push.Terminal window git status # expect: on develop, working tree cleangit branch -a | grep staging # exists?git checkout staging || git checkout -b staginggit merge developgit push origin staging# Expected: staging is up to date with develop and pushed to origin- ✅
stagingexists, is merged fromdevelop, and is pushed to origin.
- ✅
-
Confirm the host config and SSH resolve.
Terminal window grep -A5 "host('staging')" deploy.php # correct hostname, remote_user, deploy_pathssh <staging-alias> "echo 'Staging SSH OK'"# Expected: deploy.php host block looks right; SSH prints "Staging SSH OK"- ✅ The
host('staging')block is correct and SSH responds via the alias.
- ✅ The
5. Clear the server composer cache (first deploy only)
Section titled “5. Clear the server composer cache (first deploy only)”Shared hosts carry stale composer cache from prior projects — even an identical composer.lock can pull old cached package versions with extra migration files.
-
Clear the server’s composer cache.
Terminal window dep clear_composer_cache staging# manual equivalent:ssh <staging-alias> 'CACHE_DIR=~/.cache/composer/files; [ -d "$CACHE_DIR" ] && mv "$CACHE_DIR" "${CACHE_DIR}_backup_$(date +%Y%m%d%H%M%S)"; mkdir -p "$CACHE_DIR"; find "$CACHE_DIR" -mindepth 1 | wc -l'# Expected: 0- ✅ The server composer cache is empty (
0).
- ✅ The server composer cache is empty (
Run this on a first deploy, after major composer.lock changes, or when you hit mysterious migration conflicts.
6. Take a pre-deploy snapshot
Section titled “6. Take a pre-deploy snapshot”Capture the current local DB before the first staging deploy so you have a rollback reference.
-
Snapshot the current local DB.
Terminal window SNAP="Admin-Local/1-Project/3-ProjectDB/2-Snapshots/$(date +%Y-%m-%d)-pre-staging"mkdir -p "$SNAP"cp Admin-Local/1-Project/3-ProjectDB/1-Current/local.sql "$SNAP/local.sql"echo "Pre-first-staging-deploy snapshot" > "$SNAP/notes.md"# Expected: a dated snapshot dir holding local.sql + notes.md- ✅ A dated pre-staging snapshot exists with
local.sqlandnotes.md.
- ✅ A dated pre-staging snapshot exists with
7. Scaffold the server
Section titled “7. Scaffold the server”Create the deploy directory tree, set the PHP limits the installer needs, then place the staging .env — three commands that prepare the host for its first release.
-
Create the server directory tree.
Terminal window ssh <staging-alias> "mkdir -p ~/domains/staging.yourapp.com/deploy/shared/storage"# Expected: no output (directories created)- ✅
deploy/shared/storageexists on the server.
- ✅
-
Set the PHP limits the installer needs.
Terminal window # Feed the heredoc to the LOCAL ssh's stdin (EOF at column 0 here), and let# ssh forward it to the remote `cat`. Keep the heredoc outside the remote# quoted command so the terminator is controlled by the local shell.ssh <staging-alias> 'cat > ~/domains/staging.yourapp.com/deploy/shared/.user.ini' <<'EOF'max_execution_time = 300memory_limit = 512Mmax_input_time = 300upload_max_filesize = 64Mpost_max_size = 64MEOF# Expected: shared/.user.ini written with the installer limits- ✅
shared/.user.inisetsmax_execution_time = 300and the upload limits.
- ✅
-
Place the staging
.env— always via the SSH alias, never rawuser@IP:port.Terminal window scp Admin-Local/1-Project/2-ProjectVault/.env.staging \<staging-alias>:~/domains/staging.yourapp.com/deploy/shared/.envssh <staging-alias> "chmod 640 ~/domains/staging.yourapp.com/deploy/shared/.env"# Expected: .env uploaded and chmod 640 on the server- ✅
shared/.envis present andchmod 640.
- ✅
8. Scan the .env for placeholders
Section titled “8. Scan the .env for placeholders”A deploy fails if .env still has unfilled placeholders or empty required values. Catch them before you ship.
-
Scan for placeholders and empty required secrets.
Terminal window ssh <staging-alias> "grep -inE '_HERE|your_|YOUR_|\[.*\]|CHANGE_ME|REPLACE|PLACEHOLDER|TODO|FIXME|password_here' \~/domains/staging.yourapp.com/deploy/shared/.env" 2>/dev/nullssh <staging-alias> "grep -E '^(DB_PASSWORD|MAIL_PASSWORD|REDIS_PASSWORD)=(\"\")?$' \~/domains/staging.yourapp.com/deploy/shared/.env" 2>/dev/null# Expected: no output (no placeholders, no empty required secrets)- ✅ Neither scan returns a line — no placeholders, no empty required secrets.
If a service isn’t ready yet (e.g. Redis/Upstash), set it to a local driver temporarily and switch later:
CACHE_DRIVER="file" # Laravel 11+: CACHE_STORE="file"SESSION_DRIVER="file"QUEUE_CONNECTION="sync"Checklist
Section titled “Checklist”Do not mark this step done until every box below is checked.
- 🤖 Plan previewed —
dep deploy staging --planpipeline shape looks right. - 🔀 PHP triple-check passes — web SAPI / hPanel satisfy
composer.json(👤 upgrade in panel if needed). - 🤖 Deploy-safety flags confirmed —
artisan:migratedisabled,clear_paths_stricttrue. - 🤖 Optional tasks guarded — each carries a graceful-skip guard (no unguarded hard-fail).
- 🤖
stagingbranch ready — merged fromdevelopand pushed; SSH + host config verified. - 🤖 Cache cleared + snapshot taken — composer cache cleared (first deploy); pre-deploy snapshot saved.
- 🤖 Server scaffolded —
deploy/shared/storagetree exists;shared/.user.inisets installer limits. - 🤖
shared/.envplaced —chmod 640, no placeholders,APP_KEYempty,APP_DEBUG=false.