Skip to content
prod e051e98
Browse

3 · First release

Objective — ship the app’s first atomic release and prove it booted correctly: confirm APP_KEY, clear caches with the versioned PHP binary, verify the Deployer tree and the .env symlink (a 5-second check that prevents a 90-minute outage), confirm demo content, then loosen permissions for the installer.

The payoff of Phase 4 — the app’s first atomic release. Deployer flips the current symlink only after the smoke test passes, so a failed build never touches the live release.

Preview the pipeline one last time, then run the atomic release. The symlink flips only on a passing smoke test.

  1. Preview, then run the atomic deploy.

    Terminal window
    dep deploy staging --plan # final review of the task pipeline (see page 1)
    dep deploy staging # atomic release; symlink flips only on success
    # Expected: "Successfully deployed!" — symlink flips only after the smoke test passes
    • Successfully deployed!; current now points at the new release.

The smoke test gates the symlink flip — a failed build never touches the live release:

flowchart LR
P["--plan<br/>(preview)"] --> B["build release"]
B --> S["smoke test"]
S -->|pass| F["flip current ✅"]
S -->|fail| K["abort — live release untouched"]
style F fill:#0b3,stroke:#062,color:#fff
style K fill:#a40,stroke:#600,color:#fff

First-deploy behavior (Release 1): migrations are skipped automatically — the web installer owns the initial schema — and APP_KEY is auto-generated if missing. Expect Successfully deployed!.

Confirm the health check generated APP_KEY, remove any stale install lock, then clear caches with the versioned PHP binary.

  1. Confirm APP_KEY is present.

    Terminal window
    ssh <staging-alias> "grep APP_KEY ~/domains/staging.yourapp.com/deploy/shared/.env | head -1"
    # Expected: APP_KEY=base64:... (44+ chars)
    • APP_KEY=base64:... is set with 44+ characters.
  2. Generate APP_KEY once if it’s empty (common after a failed first deploy where the health check never ran).

    Terminal window
    ssh <staging-alias> "cd ~/domains/staging.yourapp.com/deploy/current && php artisan key:generate --force"
    # Expected: "Application key set successfully."
    • ✅ A key is set if one was missing.
  3. Remove any stale install lock, then clear caches with the versioned PHP binary.

    Terminal window
    ssh <staging-alias> "rm -f ~/domains/staging.yourapp.com/deploy/shared/storage/installed 2>/dev/null"
    # Use the absolute versioned binary from deploy.php — NOT bare `php`.
    PHP_BIN=$(grep "set('bin/php'" deploy.php | grep -oE "/[^'\"]+/php[^'\"]*" | head -1)
    PHP_BIN="${PHP_BIN:-php}"
    ssh <staging-alias> "cd ~/domains/staging.yourapp.com/deploy/current && \
    $PHP_BIN artisan optimize:clear"
    # Expected: each cache layer reports "cleared"
    • ✅ Stale lock removed; optimize:clear flushes every cache layer.

3. Verify the Deployer directory structure

Section titled “3. Verify the Deployer directory structure”

Confirm Deployer laid out the atomic release tree as expected.

  1. List the deploy tree.

    Terminal window
    ssh <staging-alias> "ls -la ~/domains/staging.yourapp.com/deploy/"
    # Expected: releases/, shared/, current -> releases/1, and .dep/
    • ✅ The tree shows releases/, shared/, current -> releases/1, and .dep/.

The expected shape:

deploy/
├── releases/
│ └── 1/ ← current release
├── shared/
│ ├── .env ← persistent environment
│ ├── storage/ ← persistent files (logs, uploads)
│ └── .user.ini ← PHP settings
├── current -> releases/1 ← symlink to latest release
└── .dep/ ← Deployer metadata

shared_files controls which files Deployer symlinks from shared/ into each release. If someone used set('shared_files', []) instead of add('shared_files', [...]), the recipe’s default ['.env'] is wiped — and every release boots from .env.example or a stale vendor .env, silently loading the wrong database, cache driver, or keys. This exact bug cost a 90-minute debugging session. The check below catches it in seconds.

  1. Confirm current/.env is a symlink into shared/.env, and that Laravel reads your values.

    Terminal window
    # 1. Must be a symbolic link
    ssh <staging-alias> "stat -c '%F' ~/domains/staging.yourapp.com/deploy/current/.env"
    # expected: symbolic link (regular file = shared_files broken; missing = app will crash)
    # 2. Target must resolve into shared/.env
    ssh <staging-alias> "readlink -f ~/domains/staging.yourapp.com/deploy/current/.env"
    # expected: .../deploy/shared/.env
    # 3. Laravel must actually read your values (not the vendor fallback)
    PHP_BIN=$(grep "set('bin/php'" deploy.php | grep -oE "/[^'\"]+/php[^'\"]*" | head -1)
    PHP_BIN="${PHP_BIN:-php}"
    ssh <staging-alias> "cd ~/domains/staging.yourapp.com/deploy/current && \
    $PHP_BIN artisan tinker --execute=\"
    echo 'cache.default = '.config('cache.default').PHP_EOL;
    echo 'session.driver = '.config('session.driver').PHP_EOL;
    echo 'queue.default = '.config('queue.default').PHP_EOL;
    \""
    # Expected: "symbolic link", a path ending in shared/.env, and YOUR cache/session/queue values
    • current/.env is a symbolic link resolving to shared/.env, and Laravel resolves your real cache/session/queue values.

If cache.default resolves to something other than what shared/.env sets, suspect a variable-name mismatch for your Laravel version:

Laravel versionconfig/cache.php reads.env must set
Laravel 11+env('CACHE_STORE', …)CACHE_STORE=file
Laravel 10 and earlierenv('CACHE_DRIVER', …)CACHE_DRIVER=file

Setting CACHE_STORE in a Laravel 10 .env is silently ignored — the config reads CACHE_DRIVER, gets nothing, and falls through to the template’s hardcoded default (often redis).

CodeCanyon apps ship demo content (images, avatars, themes, sample data) in app-specific locations. The shared:init_defaults task copies defaults into shared dirs, but verify the right content reached the right place.

  1. Discover content dirs locally and confirm they’re in shared_dirs.

    Terminal window
    # Discover content dirs locally
    for dir in uploads public/uploads public/images public/avatars public/themes \
    storage/app/public public/media public/assets/images; do
    [ -d "$dir" ] && echo " $dir — $(find "$dir" -type f 2>/dev/null | wc -l | tr -d ' ') files"
    done
    grep -A10 "shared_dirs" deploy.php | grep "'"
    # Expected: every content directory with demo files also appears in shared_dirs
    • ✅ Every content directory with demo files is listed in shared_dirs.

Every content directory with demo files should be in shared_dirs; otherwise those files are lost on redeploy. If content is missing on the server, upload it once with scp -r (via the alias) or add the folder to shared_dirs and redeploy.

The web installer requires writable paths. This is a deliberate, temporary loosening — it gets hardened on page 4.

  1. Loosen storage and cache permissions for the installer.

    Terminal window
    ssh <staging-alias> "cd ~/domains/staging.yourapp.com/deploy/current && \
    chmod -R 777 storage bootstrap/cache resources/lang uploads 2>/dev/null; \
    chmod -R 777 ~/domains/staging.yourapp.com/deploy/shared/storage 2>/dev/null && echo 'Permissions set to 777'"
    # Expected: "Permissions set to 777"
    • Permissions set to 777 — the installer’s writable paths are ready.

Troubleshooting — first-release failures

Section titled “Troubleshooting — first-release failures”
  • Connection refused 127.0.0.1:6379 on the first artisan task — the release is loading .env from a fallback, not shared/.env. This is the symlink BLOCKER above. Switch set('shared_files', …)add('shared_files', …), or untrack a committed .env (git rm --cached .env), then redeploy.
  • Too many levels of symbolic links during deploy:shared — a file is listed in shared_files and lives inside a shared_dirs directory (e.g. storage/.ignore_locales with shared_dirs: ['storage']). A file may be in one or the other, never both. Remove the shared_files entry — shared_dirs already persists it. Fails before the symlink switch, so no downtime.
  • Class not found (Faker / Debugbar / Telescope) — production code references a dev-only package excluded by composer install --no-dev. Guard with class_exists() or move the logic into seeders; add to prod deps only as a last resort.
  • Vite manifest not foundpublic/build/ is gitignored and wasn’t deployed. For the build-locally strategy, un-ignore it: git add -f public/build/ && git commit && dep deploy staging.
  • Livewire “published assets are out of date” — the Livewire package and the published assets in public/vendor/livewire/ disagree, so wire:model silently fails and component state is lost on refresh. One-off fix: resolve $PHP_BIN from deploy.php (same as §3), then ssh <staging-alias> "cd ~/domains/staging.yourapp.com/deploy/current && $PHP_BIN artisan livewire:publish --assets", then purge the CDN cache. Prevention: the deploy template runs a livewire:publish_assets task after vendor:restore on every release — copy it in if your deploy.php predates it.
  • your php version does not satisfy that requirement at deploy:vendors — web SAPI PHP is below composer.json. Fixes belong on page 1’s PHP triple-check: upgrade in hPanel, update bin/php, redeploy. Fails before the symlink switch.
  • Emergency rollbackdep rollback staging (changes the symlink to the previous release; no files deleted).

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

  • 🤖 Release deployeddep deploy staging completed; current symlinks to the new release.
  • 🤖 Key + cachesAPP_KEY present (44+ chars); install lock removed; caches cleared with the versioned PHP.
  • 🤖 Deployer tree correctcurrent, releases/, shared/, .dep/.
  • 🤖 .env symlink resolvescurrent/.env is a symlink into shared/.env; Laravel resolves your cache/session/queue values.
  • 🤖 Demo content present — in the right shared dirs.
  • 🤖 Installer permissions set — 777 for the installer (re-hardened on page 4).