Skip to content
prod e051e98
Browse

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.

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.

Do not deploy blind. Preview the full pipeline, then confirm the PHP version the server actually runs — not just the binary Deployer points at.

  1. Preview the plan.

    Terminal window
    # Deployer 7 uses --plan; the old --dry-run was removed and errors out
    dep deploy staging --plan 2>&1 | tail -60
    # Expected: a ~40–50 task pipeline including the gates that matter

    You 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 migrations
    deploy:verify_clear_paths ← BLOCKS if a sensitive file survives clear_paths
    deploy:vendors ← composer install — fails here on PHP mismatch
    deploy:verify_migrations
    deploy:smoke_test ← BLOCKS before the symlink switch
    deploy:symlink ← atomic cutover
    deploy:health_check ← BLOCKS after symlink; auto-rollback on failure
    • ✅ The plan previews a sane ~40–50 task pipeline with the gates above present.
  2. Triple-check PHPcomposer.jsonbin/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 satisfy composer.json’s require.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:

PatternMeaningAction
bin/php, web SAPI, and hPanel all match composer.jsonSafe to deployProceed
bin/php matches but web SAPI is lowerDeploy will fail at deploy:vendorsUpgrade PHP in hPanel, wait ~60s, re-check
bin/php matches but hPanel shows lowerhPanel controls the web SAPIFix hPanel first
bin/php path doesn’t exist on the serverbin/php is staleRe-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| CI

The recipe disables and enables a few tasks deliberately. Confirm the flags are set the way the pipeline expects before you trust the plan.

  1. 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:migrate is disabled, clear_paths_strict is true, and atlas_enabled is set.

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.

  1. 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; do
    echo "=== $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.

Bring staging current with develop and push it, then confirm the host config and SSH still resolve.

  1. Merge develop into staging and push.

    Terminal window
    git status # expect: on develop, working tree clean
    git branch -a | grep staging # exists?
    git checkout staging || git checkout -b staging
    git merge develop
    git push origin staging
    # Expected: staging is up to date with develop and pushed to origin
    • staging exists, is merged from develop, and is pushed to origin.
  2. Confirm the host config and SSH resolve.

    Terminal window
    grep -A5 "host('staging')" deploy.php # correct hostname, remote_user, deploy_path
    ssh <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.

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.

  1. 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).

Run this on a first deploy, after major composer.lock changes, or when you hit mysterious migration conflicts.

Capture the current local DB before the first staging deploy so you have a rollback reference.

  1. 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.sql and notes.md.

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.

  1. 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/storage exists on the server.
  2. 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 = 300
    memory_limit = 512M
    max_input_time = 300
    upload_max_filesize = 64M
    post_max_size = 64M
    EOF
    # Expected: shared/.user.ini written with the installer limits
    • shared/.user.ini sets max_execution_time = 300 and the upload limits.
  3. Place the staging .env — always via the SSH alias, never raw user@IP:port.

    Terminal window
    scp Admin-Local/1-Project/2-ProjectVault/.env.staging \
    <staging-alias>:~/domains/staging.yourapp.com/deploy/shared/.env
    ssh <staging-alias> "chmod 640 ~/domains/staging.yourapp.com/deploy/shared/.env"
    # Expected: .env uploaded and chmod 640 on the server
    • shared/.env is present and chmod 640.

A deploy fails if .env still has unfilled placeholders or empty required values. Catch them before you ship.

  1. 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/null
    ssh <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"

Do not mark this step done until every box below is checked.

  • 🤖 Plan previeweddep deploy staging --plan pipeline shape looks right.
  • 🔀 PHP triple-check passes — web SAPI / hPanel satisfy composer.json (👤 upgrade in panel if needed).
  • 🤖 Deploy-safety flags confirmedartisan:migrate disabled, clear_paths_strict true.
  • 🤖 Optional tasks guarded — each carries a graceful-skip guard (no unguarded hard-fail).
  • 🤖 staging branch ready — merged from develop and pushed; SSH + host config verified.
  • 🤖 Cache cleared + snapshot taken — composer cache cleared (first deploy); pre-deploy snapshot saved.
  • 🤖 Server scaffoldeddeploy/shared/storage tree exists; shared/.user.ini sets installer limits.
  • 🤖 shared/.env placedchmod 640, no placeholders, APP_KEY empty, APP_DEBUG=false.