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.
Background
Section titled “Background”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.
1. Dry-run, then deploy
Section titled “1. Dry-run, then deploy”Preview the pipeline one last time, then run the atomic release. The symlink flips only on a passing smoke test.
-
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!;currentnow 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:#fffFirst-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!.
2. Confirm APP_KEY and clear caches
Section titled “2. Confirm APP_KEY and clear caches”Confirm the health check generated APP_KEY, remove any stale install lock, then clear caches with the versioned PHP binary.
-
Confirm
APP_KEYis 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.
- ✅
-
Generate
APP_KEYonce 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.
-
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:clearflushes every cache layer.
- ✅ Stale lock removed;
3. Verify the Deployer directory structure
Section titled “3. Verify the Deployer directory structure”Confirm Deployer laid out the atomic release tree as expected.
-
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 tree shows
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 metadata4. Verify the .env symlink (BLOCKER)
Section titled “4. Verify the .env symlink (BLOCKER)”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.
-
Confirm
current/.envis a symlink intoshared/.env, and that Laravel reads your values.Terminal window # 1. Must be a symbolic linkssh <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/.envssh <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/.envis a symbolic link resolving toshared/.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 version | config/cache.php reads | .env must set |
|---|---|---|
| Laravel 11+ | env('CACHE_STORE', …) | CACHE_STORE=file |
| Laravel 10 and earlier | env('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).
5. Verify demo/default content deployed
Section titled “5. Verify demo/default content deployed”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.
-
Discover content dirs locally and confirm they’re in
shared_dirs.Terminal window # Discover content dirs locallyfor 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"donegrep -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 is listed in
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.
6. Loosen permissions for the installer
Section titled “6. Loosen permissions for the installer”The web installer requires writable paths. This is a deliberate, temporary loosening — it gets hardened on page 4.
-
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.
- ✅
Background
Section titled “Background”Troubleshooting — first-release failures
Section titled “Troubleshooting — first-release failures”Connection refused 127.0.0.1:6379on the first artisan task — the release is loading.envfrom a fallback, notshared/.env. This is the symlink BLOCKER above. Switchset('shared_files', …)→add('shared_files', …), or untrack a committed.env(git rm --cached .env), then redeploy.Too many levels of symbolic linksduringdeploy:shared— a file is listed inshared_filesand lives inside ashared_dirsdirectory (e.g.storage/.ignore_localeswithshared_dirs: ['storage']). A file may be in one or the other, never both. Remove theshared_filesentry —shared_dirsalready persists it. Fails before the symlink switch, so no downtime.Class not found(Faker / Debugbar / Telescope) — production code references a dev-only package excluded bycomposer install --no-dev. Guard withclass_exists()or move the logic into seeders; add to prod deps only as a last resort.Vite manifest not found—public/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, sowire:modelsilently fails and component state is lost on refresh. One-off fix: resolve$PHP_BINfromdeploy.php(same as §3), thenssh <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 alivewire:publish_assetstask aftervendor:restoreon every release — copy it in if yourdeploy.phppredates it. your php version does not satisfy that requirementatdeploy:vendors— web SAPI PHP is belowcomposer.json. Fixes belong on page 1’s PHP triple-check: upgrade in hPanel, updatebin/php, redeploy. Fails before the symlink switch.- Emergency rollback —
dep rollback staging(changes the symlink to the previous release; no files deleted).
Checklist
Section titled “Checklist”Do not mark this step done until every box below is checked.
- 🤖 Release deployed —
dep deploy stagingcompleted;currentsymlinks to the new release. - 🤖 Key + caches —
APP_KEYpresent (44+ chars); install lock removed; caches cleared with the versioned PHP. - 🤖 Deployer tree correct —
current,releases/,shared/,.dep/. - 🤖
.envsymlink resolves —current/.envis a symlink intoshared/.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).