2 · Release & first production deploy (P1–P2)
Objective — cut the changelog and version, promote staging → production, stage the production .env, deploy (dry-run then real), run the installer on the first deploy, and confirm the app is live — so release docs travel with the release commit and verified staging code goes live cleanly.
1. Prepare the release (P1)
Section titled “1. Prepare the release (P1)”Cut the changelog and version before deploying so release docs travel with the release commit.
-
Pick the version using SemVer — patch for fixes, minor for non-breaking features, major for breaking changes or a major vendor update.
Terminal window git tag --sort=-v:refname | head -5 # what's already released# Expected: the most recent release tags, newest first- ✅ The chosen version follows SemVer and doesn’t collide with an existing tag.
-
Update the internal changelog — move
[Unreleased]items into a dated version section, tag entries ([PUBLIC],[INTERNAL],[VENDOR],[SECURITY]), and reset[Unreleased].- ✅ The internal changelog has a dated, tagged version section and an empty
[Unreleased].
- ✅ The internal changelog has a dated, tagged version section and an empty
-
Update the public changelog — extract the
[PUBLIC]items and rewrite them as user benefits (“added caching” → “dashboard loads 2× faster”).- ✅ The public changelog reads as user-facing benefits, not internal notes.
-
Commit the changelogs alone.
Terminal window git add CHANGELOG.md CHANGELOG-PUBLIC.mdgit commit -m "Update changelogs for v${VERSION}"# Expected: one commit containing only the two changelog files- ✅ A single commit holds both changelogs and nothing else.
2. Cut the first production release (P2)
Section titled “2. Cut the first production release (P2)”The core move: put the verified staging code live and complete the installer.
-
Promote staging → production branch.
Terminal window git checkout productiongit merge staging --ff-onlygit push origin production# Expected: staging commits fast-forward onto production when histories allow; otherwise resolve before merge- ✅
productioncarries the verified staging code and is pushed.
- ✅
-
Stage the production
.envfirst — generate it from your template via the secrets manager, upload over an SSH alias (never rawuser@IP), and lock permissions before any migration preview that needs DB credentials.Terminal window scp .env.production <SSH_PRODUCTION_ALIAS>:~/domains/<DOMAIN>/deploy/shared/.envssh <SSH_PRODUCTION_ALIAS> "chmod 640 ~/domains/<DOMAIN>/deploy/shared/.env"# Expected: the .env uploads and ends up mode 640 on the server- ✅ The production
.envis on the server at mode640.
- ✅ The production
-
Create the PHP override (
.user.ini) — first deploy only. Long installs and migrations exceed the default PHPmax_execution_timeand return a 504. Write the override into the shared dir idempotently so it survives every release.Terminal window ssh <SSH_PRODUCTION_ALIAS> 'cat > ~/domains/<DOMAIN>/deploy/shared/.user.ini' <<'EOF'max_execution_time = 300memory_limit = 512Mmax_input_time = 300upload_max_filesize = 64Mpost_max_size = 64MEOFssh <SSH_PRODUCTION_ALIAS> "chmod 644 ~/domains/<DOMAIN>/deploy/shared/.user.ini"ssh <SSH_PRODUCTION_ALIAS> "grep -q '^max_execution_time = 300' ~/domains/<DOMAIN>/deploy/shared/.user.ini \&& echo 'OK — .user.ini present with 300s timeout' || echo 'STOP — .user.ini missing or wrong'"# Expected: "OK — .user.ini present with 300s timeout"- ✅
.user.iniexists indeploy/shared/withmax_execution_time = 300; the grep printsOK.
- ✅
-
Pre-deploy migration safety check (only after
.envis staged). Preview before you run anything destructive.Terminal window ssh <SSH_PRODUCTION_ALIAS> 'cd ~/domains/<DOMAIN>/deploy/current && php artisan migrate --pretend'# Expected: on a first deploy with the web installer, the installer creates tables — treat DROP noise as expected only when that path applies- ✅ The pretend output is reviewed in context (installer-first vs migrate-first deploy).
-
Confirm the
.envhas no placeholders — the deploy fails on an unfilled value, and every production value must be real (APP_DEBUG=false, live keys, strong passwords).Terminal window ssh <SSH_PRODUCTION_ALIAS> "grep -inE '_HERE|your_|CHANGE_ME|REPLACE|PLACEHOLDER|TODO|xxx|<[A-Z_]+>' ~/domains/<DOMAIN>/deploy/shared/.env" \&& echo "STOP — fix placeholders" || echo "Clean"# Expected: "Clean" — no placeholder tokens remain (does not false-positive on https:// URLs)- ✅ The check prints
Clean; no placeholder values remain.
- ✅ The check prints
-
Deploy — dry-run, then for real. The first deploy auto-skips migrations; the web installer creates the schema.
Terminal window dep deploy production --plandep deploy production# Expected: the plan lists steps without applying them, then the real deploy completes- ✅ The dry-run plan looks right and the real deploy completes without errors.
The next two substeps are browser actions — completing the installer wizard and verifying the live app in a browser.
-
Run the installer (first deploy only). The
/installroute is blocked at two layers —.htaccessand (if configured) a Cloudflare WAF rule. Unblock both, complete the wizard, then re-block both and verify.a. Unblock
/install(agent —.htaccess+ user — WAF):Terminal window # .htaccess layer (idempotent — only flips an un-commented rule)ssh <SSH_PRODUCTION_ALIAS> 'cd ~/domains/<DOMAIN>/deploy/current/public \&& sed -i "s/^RewriteRule \^install/# RewriteRule ^install/" .htaccess && echo "/install unblocked in .htaccess"'# Reset the install lock so the wizard can run (auto-recreated on finish).ssh <SSH_PRODUCTION_ALIAS> "rm -f ~/domains/<DOMAIN>/deploy/shared/storage/installed"👤 WAF layer (browser): in the Cloudflare dashboard, toggle the WAF rule “Block install and update routes” OFF. The
.htaccessedit alone is not enough — the WAF returns a403ahead of the origin.Verify it is reachable before opening the wizard:
Terminal window curl -sI https://<DOMAIN>/install | head -1# Expected: HTTP/2 200 (if still 403, the Cloudflare WAF rule is still ON)- ✅
curl -sI /installreturns200(both.htaccessand the WAF rule are off).
b. Complete the wizard (👤 user, browser): requirements all green, production DB credentials, a strong admin password (saved to the vault immediately), production URL → Install/Finish.
c. Re-block both layers (agent —
.htaccess+ user — WAF):Terminal window ssh <SSH_PRODUCTION_ALIAS> 'cd ~/domains/<DOMAIN>/deploy/current/public \&& sed -i "s/^# RewriteRule \^install/RewriteRule ^install/" .htaccess && echo "/install re-blocked in .htaccess"'curl -sI https://<DOMAIN>/install | head -1# Expected: HTTP/2 403👤 WAF layer: toggle the Cloudflare WAF rule “Block install and update routes” back ON.
- ✅
curl -sI /installreturns403, and both the.htaccessrule and the Cloudflare WAF rule are re-enabled.
- ✅
-
Verify it’s live. Load
/loginover HTTPS, sign in as admin, and confirm the dashboard loads with no stack traces, no debug bar, and a valid SSL padlock.- ✅
/loginloads over HTTPS, admin login works, and the dashboard is clean with a valid padlock.
- ✅
Troubleshooting
Section titled “Troubleshooting”504 timeout during deploy or installer
Section titled “504 timeout during deploy or installer”-
Cause — PHP
max_execution_timeis too short for migrations / a long first install. -
Fix — confirm
.user.iniexists indeploy/shared/withmax_execution_time = 300(step 3 above). If missing, write it and retry:Terminal window ssh <SSH_PRODUCTION_ALIAS> "test -f ~/domains/<DOMAIN>/deploy/shared/.user.ini \&& grep '^max_execution_time' ~/domains/<DOMAIN>/deploy/shared/.user.ini \|| echo 'MISSING — create .user.ini (see step 3)'"# Expected: "max_execution_time = 300" — anything else means recreate it
“Access denied” database error during the installer
Section titled ““Access denied” database error during the installer”-
Cause — the config cache holds stale database credentials.
-
Fix — clear the config cache, then retry the installer:
Terminal window ssh <SSH_PRODUCTION_ALIAS> "cd ~/domains/<DOMAIN>/deploy/current && php artisan config:clear"# Expected: "Configuration cache cleared!"
/install stays blocked while expected open
Section titled “/install stays blocked while expected open”Both .htaccess and Cloudflare WAF may block it — temporarily open both, finish the installer, then re-block both and confirm curl -sI returns 403.
/install stays open after completion
Section titled “/install stays open after completion”Installed marker missing or route block not restored — restore WAF/host block immediately and verify marker/DB install flag.
Checklist
Section titled “Checklist”Do not mark this step done until every box below is checked.
- 🤖 Release prepared (P1) — version picked, internal + public changelogs updated, committed alone.
- 🤖 Production promoted —
stagingmerged intoproductionand pushed. - 🤖
.envstaged & clean — uploaded over the SSH alias at mode640, placeholder check printsClean. - 🤖 Deployed — dry-run plan reviewed, real
dep deploy productioncompleted. - 👤 Installer done —
/installwizard complete, route re-blocked, returns403; admin login saved to the vault. - 👤 Live verified —
/loginover HTTPS, admin signs in, dashboard clean with a valid padlock.