What are the core steps of a Laravel 13 production deploy script, in order, and why do composer install and migrate need --no-dev and --force?
answer
- code, dependencies, assets, schema, caches, processes
- require-dev stays off the server
- production confirm prompt cancels migrate
- composer install deletes config.php
- optimize after install, reload last
basics
~20 sFetch the code, run composer install --no-dev, build front-end assets, run php artisan migrate --force, run php artisan optimize, then php artisan reload. --no-dev skips require-dev packages; --force skips migrate's production confirmation, which a non-interactive script would otherwise cancel.
solid answer
~40 sA typical order is: fetch the release; `composer install --no-dev --no-interaction`; `npm ci && npm run build`; `php artisan migrate --force`; `php artisan optimize`; `php artisan reload`. `--no-dev` leaves out `require-dev` (in the skeleton: Faker, Pail, Pint, PHPUnit, Mockery, Collision), which production neither needs nor should expose. `migrate` uses Laravel's confirmation prompt whenever the environment is `production`; in a non-interactive script that prompt resolves to its default of "no", the command prints "Command cancelled." and exits with a failure, so the flag is required. Order matters: Composer's `post-autoload-dump` script deletes `bootstrap/cache/config.php`, `services.php` and `packages.php`, so `optimize` must come after `composer install`, and `reload` comes last so restarted workers boot the new code and caches.
code
bash · 10 linesset -e
cd /var/www/tickets
git pull origin main
composer install --no-dev --no-interaction --prefer-dist
npm ci && npm run build
php artisan migrate --force
php artisan optimize
php artisan reloadgo deeper
Recall the order: code, composer install --no-dev, assets, migrate --force, optimize, reload, and what each flag is for.
Explain why migrate needs --force in production, and why optimize must follow composer install because post-autoload-dump deletes cached files.
Make the script fail fast, keep dev packages and stale manifests off servers, and restart every long-running process after caches are built.
Standardise one deploy contract across services so ordering mistakes are caught by tooling rather than by an outage.
## The shape of a deploy A busy ticketing platform deploys several times a week. Whatever tool runs the script (a CI job, a hosting panel, Envoy, a shell script over SSH), the Laravel-specific steps are the same: 1. **Get the code** for the release (a `git` checkout or an unpacked artifact). 2. **Install PHP dependencies:** `composer install --no-dev --no-interaction`. 3. **Build front-end assets:** `npm ci` and `npm run build`, producing `public/build`. 4. **Apply schema changes:** `php artisan migrate --force`. 5. **Build Laravel's caches:** `php artisan optimize`. 6. **Restart long-running processes:** `php artisan reload`. In-place deploys often wrap steps 2 to 5 in maintenance mode; zero-downtime deploys build a separate release directory instead. Either way, the order above is the part interviewers probe. ## Why `--no-dev` `composer install` installs both `require` and `require-dev` by default. The skeleton's `require-dev` lists `fakerphp/faker`, `laravel/pail`, `laravel/pint`, `mockery/mockery`, `nunomaduro/collision`, `phpunit/phpunit` and `laravel/pao`. None of them belongs on a production server: - they add code and attack surface that production never runs; - they make installs slower and larger; - they can mask a mistake, such as production code that accidentally uses Faker, which would otherwise fail loudly. Composer's `post-autoload-dump` hook then runs `php artisan package:discover`, which rebuilds `bootstrap/cache/packages.php` from the packages actually installed, so dev-only service providers drop out of the discovered list. ## Why `--force` `migrate` uses Laravel's `ConfirmableTrait`. When `app()->environment()` is `production`, the command shows an **Application In Production** alert and asks "Are you sure you want to run this command?" with a default of no. A deploy script has no one to answer, so the prompt resolves to its default and the command prints "Command cancelled." and returns a failure code. `--force` skips the question. The same flag appears on other destructive commands for the same reason. ## Why the order matters | Step | Must come after | Reason | |---|---|---| | `composer install` | code checkout | installs what the new `composer.lock` says | | `migrate --force` | `composer install` | migrations and the framework must be the new version | | `optimize` | `composer install` | `post-autoload-dump` deletes `bootstrap/cache/config.php`, `services.php` and `packages.php` | | `optimize` | code checkout | caches must describe the new config, routes, events and views | | `reload` | `optimize` | restarted workers should boot the new code and fresh caches | The second `optimize` row is the subtle one. `Illuminate\Foundation\ComposerScripts::postAutoloadDump()` calls `clearCompiled()`, which removes the cached config, services and packages files. Running `optimize` first and `composer install` second leaves production running without a config cache at all. ## In place or side by side The script above updates files in the directory that is serving traffic. For the seconds it runs, a request can meet new PHP files with an old `vendor/`, or new code before its migration. Two ways out: - wrap the risky steps in maintenance mode, accepting a short outage; - build each release in its own directory and switch a symlink when it is complete, which is the zero-downtime approach. Either way the Laravel steps and their order stay the same; only where they run changes. ## Checking the result - Request the health route and a few key pages right after `reload`. - Watch the application log for the first minutes of traffic. - Confirm workers restarted: their process start times should be after the deploy. ## What the script should not do - **Run `optimize:clear` as a routine step.** It also flushes the default cache store; `optimize` alone replaces the old cache files. - **Install with `--no-scripts` on an in-place deploy.** Without `package:discover`, a `packages.php` left from an earlier install can still list dev-only providers that are no longer installed. - **Skip `reload`.** Queue workers and other long-running processes keep the previous release's code in memory until they restart. - **Continue after a failed step.** Use `set -e` or the deploy tool's equivalent so a failed migration stops the deploy before caches and restarts. ## Summary Code, dependencies without dev packages, assets, forced migrations, caches, reload: each step prepares what the next one relies on.
- Why does running php artisan optimize before composer install leave a Laravel app without a config cache?The skeleton's `post-autoload-dump` Composer script calls `Illuminate\Foundation\ComposerScripts::postAutoloadDump()`, which deletes `bootstrap/cache/config.php`, `services.php` and `packages.php`. Any `composer install` that dumps the autoloader therefore wipes the config cache built earlier, so `optimize` has to run after it.
- What happens if a Laravel deploy script runs php artisan migrate without --force on a production server?Because the environment is `production`, `ConfirmableTrait` asks for confirmation with a default of no. With no interactive terminal the default is taken, the command prints "Command cancelled." and returns a failure code, so no migrations run and a script using `set -e` stops there.
- Why can composer install --no-dev --no-scripts break an in-place Laravel deploy?`--no-scripts` skips `package:discover`, so a `bootstrap/cache/packages.php` left from an earlier install with dev packages can still list providers such as a dev tool's service provider. Laravel then tries to register a class that `--no-dev` did not install. Letting the scripts run rebuilds the manifest from what is installed.
saying these in an interview costs you the question
- migrate runs without prompting as long as APP_DEBUG is false
- --no-dev also removes unused production packages
- Run optimize first so composer install uses the cached config
- optimize:clear must run at the start of every deploy
- Queue workers pick up new code without a restart