Skip to content
prod e051e98
Browse

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.

Cut the changelog and version before deploying so release docs travel with the release commit.

  1. 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.
  2. 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].
  3. 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.
  4. Commit the changelogs alone.

    Terminal window
    git add CHANGELOG.md CHANGELOG-PUBLIC.md
    git 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.

The core move: put the verified staging code live and complete the installer.

  1. Promote staging → production branch.

    Terminal window
    git checkout production
    git merge staging --ff-only
    git push origin production
    # Expected: staging commits fast-forward onto production when histories allow; otherwise resolve before merge
    • production carries the verified staging code and is pushed.
  2. Stage the production .env first — generate it from your template via the secrets manager, upload over an SSH alias (never raw user@IP), and lock permissions before any migration preview that needs DB credentials.

    Terminal window
    scp .env.production <SSH_PRODUCTION_ALIAS>:~/domains/<DOMAIN>/deploy/shared/.env
    ssh <SSH_PRODUCTION_ALIAS> "chmod 640 ~/domains/<DOMAIN>/deploy/shared/.env"
    # Expected: the .env uploads and ends up mode 640 on the server
    • ✅ The production .env is on the server at mode 640.
  3. Create the PHP override (.user.ini) — first deploy only. Long installs and migrations exceed the default PHP max_execution_time and 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 = 300
    memory_limit = 512M
    max_input_time = 300
    upload_max_filesize = 64M
    post_max_size = 64M
    EOF
    ssh <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.ini exists in deploy/shared/ with max_execution_time = 300; the grep prints OK.
  4. Pre-deploy migration safety check (only after .env is 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).
  5. Confirm the .env has 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.
  6. Deploy — dry-run, then for real. The first deploy auto-skips migrations; the web installer creates the schema.

    Terminal window
    dep deploy production --plan
    dep 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.

  1. Run the installer (first deploy only). The /install route is blocked at two layers — .htaccess and (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 .htaccess edit alone is not enough — the WAF returns a 403 ahead 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 /install returns 200 (both .htaccess and 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 /install returns 403, and both the .htaccess rule and the Cloudflare WAF rule are re-enabled.
  1. Verify it’s live. Load /login over HTTPS, sign in as admin, and confirm the dashboard loads with no stack traces, no debug bar, and a valid SSL padlock.

    • /login loads over HTTPS, admin login works, and the dashboard is clean with a valid padlock.
  • Cause — PHP max_execution_time is too short for migrations / a long first install.

  • Fix — confirm .user.ini exists in deploy/shared/ with max_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.

Installed marker missing or route block not restored — restore WAF/host block immediately and verify marker/DB install flag.

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

  • 🤖 Release prepared (P1) — version picked, internal + public changelogs updated, committed alone.
  • 🤖 Production promotedstaging merged into production and pushed.
  • 🤖 .env staged & clean — uploaded over the SSH alias at mode 640, placeholder check prints Clean.
  • 🤖 Deployed — dry-run plan reviewed, real dep deploy production completed.
  • 👤 Installer done/install wizard complete, route re-blocked, returns 403; admin login saved to the vault.
  • 👤 Live verified/login over HTTPS, admin signs in, dashboard clean with a valid padlock.