## What it is

Artisan is Laravel's command-line companion — every framework concern that benefits from automation (database migrations, cache priming, queue management, code scaffolding, scheduled tasks) is exposed as an `artisan` subcommand. For deployment workflows it's the orchestrator: a Laravel release isn't "live" until `migrate --force` has run, the config/route/view caches are warm, queue workers have restarted against the new code, and OPcache has been reset.

This sheet covers the commands and flags you reach for when shipping Laravel to production — the cache-warming sequence that turns a cold release into a 10ms response, the `queue:restart` discipline that prevents workers from holding old code in memory, and the maintenance-mode pattern that lets your team keep working during a database migration without blocking real traffic.

## Quick reference

### Basics

```bash
php artisan                                             # list every command grouped by namespace
php artisan list --raw                                  # one command per line — script-friendly
php artisan help migrate                                # detailed help for one command
php artisan about                                       # environment, drivers, package versions
php artisan --version                                   # framework version
php artisan inspire                                     # confirms artisan is wired correctly
```

`php artisan about` is the most useful single command in a deploy debug session — it dumps the active environment, the cache/queue/session drivers in use, and the Laravel/PHP versions in one screen.

### Make (scaffolding)

```bash
php artisan make:controller PostController --resource
php artisan make:model Post -mfsc                       # model + migration + factory + seeder + controller
php artisan make:migration create_posts_table
php artisan make:seeder PostSeeder
php artisan make:factory PostFactory --model=Post
php artisan make:request StorePostRequest
php artisan make:resource PostResource
php artisan make:middleware EnsureSubscribed
php artisan make:command SyncSubscribers
php artisan make:job ProcessUpload
php artisan make:event UserSignedUp
php artisan make:listener SendWelcomeEmail
php artisan make:notification InvoicePaid
php artisan make:policy PostPolicy --model=Post
php artisan make:provider CustomServiceProvider
php artisan make:test PostTest                          # phpunit
php artisan make:test PostTest --pest                   # pest
```

### Database, migrations, and seeding

```bash
php artisan migrate                                     # ask for confirmation in prod
php artisan migrate --force                             # skip prompt (deploy default)
php artisan migrate --pretend                           # show SQL without executing
php artisan migrate --step                              # one migration at a time
php artisan migrate --path=database/migrations/tenants  # scoped path
php artisan migrate:status                              # which migrations have run
php artisan migrate:rollback --step=1                   # rollback last batch (or N steps)
php artisan migrate:reset                               # rollback ALL — never in prod
php artisan migrate:refresh                             # reset + re-run
php artisan migrate:fresh                               # DROP all tables + re-run
php artisan migrate:fresh --seed                        # + seed

php artisan db:seed                                     # run DatabaseSeeder
php artisan db:seed --class=PostSeeder
php artisan db:wipe                                     # drop every table — never in prod
```

### Cache: the four caches Laravel uses

| Cache | Purpose | Build with | Clear with |
|---|---|---|---|
| Config | Compiled merge of `config/*.php` | `config:cache` | `config:clear` |
| Routes | Compiled route table | `route:cache` | `route:clear` |
| Views | Compiled Blade templates | `view:cache` | `view:clear` |
| Application | Runtime `Cache::*` store (Redis/Memcached/file) | n/a | `cache:clear` |

```bash
php artisan config:cache                                # ship the compiled config
php artisan config:clear

php artisan route:cache
php artisan route:clear

php artisan view:cache
php artisan view:clear

php artisan cache:clear                                 # the runtime cache store
php artisan cache:forget my_key
php artisan optimize                                    # config:cache + route:cache + view:cache
php artisan optimize:clear                              # clear all of the above
```

### Queue

```bash
php artisan queue:work                                  # run worker in foreground
php artisan queue:work --tries=3 --timeout=120 --queue=high,default
php artisan queue:listen                                # auto-reload code each job (dev-only)
php artisan queue:restart                               # ask all workers to gracefully exit
php artisan queue:failed                                # list failed jobs
php artisan queue:retry 5                               # retry job by ID
php artisan queue:retry all
php artisan queue:forget 5                              # delete one failed job
php artisan queue:flush                                 # delete all failed jobs
php artisan queue:monitor default,high --max=100        # alert if queue depth > 100
php artisan queue:prune-failed --hours=168              # prune entries older than 7 days
php artisan queue:prune-batches --hours=48              # batch table cleanup
```

### Scheduling

```bash
php artisan schedule:run                                # called every minute from system cron
php artisan schedule:list                               # next run time per task
php artisan schedule:test                               # interactively run a scheduled task
php artisan schedule:work                               # local dev: stand-in for system cron
php artisan schedule:clear-cache                        # invalidate withoutOverlapping() locks
```

System crontab line for Laravel — exactly one entry, ever:

```cron
* * * * * cd /var/www/current && php artisan schedule:run >> /dev/null 2>&1
```

See the [Cron and Crontab cheatsheet](https://www.deployhq.com/cheatsheets/cron) for atomic crontab installation across deploys.

### Maintenance mode

```bash
php artisan down                                        # bring site down — generic 503
php artisan down --refresh=15                           # retry header tells client to retry in 15s
php artisan down --retry=60                             # also sets Retry-After header
php artisan down --secret="deploy-bypass-$(uuidgen)"    # bypass cookie via /<secret>
php artisan down --render="errors::503-custom"          # render a custom Blade template
php artisan down --status=503                           # set the HTTP status

php artisan up                                          # bring back up
```

### Tinker (REPL)

```bash
php artisan tinker
> User::count()
> User::factory()->count(5)->create()
> Cache::store('redis')->put('key', 'value', 60)
> dispatch(new ProcessUpload($path));
> exit
```

Tinker is invaluable in a production debug session — `wp eval` for Laravel. Treat it the same way: read-only first (`select`-style queries), and never paste in untested code.

### Storage, app keys, and environment

```bash
php artisan storage:link                                # symlink public/storage → storage/app/public
php artisan key:generate                                # generate APP_KEY (one-time per install)
php artisan env                                         # print current APP_ENV
php artisan config:show database                        # dump computed config for a section
php artisan env:encrypt                                 # encrypt .env into .env.encrypted
php artisan env:decrypt --key="$ENV_KEY"                # decrypt on the host
```

### Tests and code quality

```bash
php artisan test                                        # phpunit or pest depending on stack
php artisan test --parallel
php artisan test --filter=UserTest
php artisan test --coverage
php artisan test --testsuite=Feature
```

### Telescope, Horizon, and other first-party packages

```bash
# Horizon (Redis queue dashboard)
php artisan horizon                                     # start the supervisor
php artisan horizon:terminate                           # ask Horizon to gracefully restart
php artisan horizon:pause / horizon:continue
php artisan horizon:status

# Telescope (debug recorder — usually disabled in prod)
php artisan telescope:prune --hours=24
php artisan telescope:clear
```

---

## Deployment workflows (the moat)

### 1. The post-deploy command sequence that earns its ranking

Every Laravel deploy ends with the same six commands, in this order, on the new release directory. Get the order wrong and you either ship a broken release or serve stale code:

```bash
#!/usr/bin/env bash
set -euo pipefail
cd /var/www/releases/"$SHA"

# 1. Migrate first — schema must match the new code before anyone runs against it
php artisan migrate --force --no-interaction

# 2. Clear caches BEFORE rebuilding — config:cache reads the live config
php artisan optimize:clear

# 3. Rebuild caches against the new code
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache    # Laravel 8+

# 4. Atomic symlink swap — now the new release is "live" on disk
ln -sfn "/var/www/releases/$SHA" /var/www/current

# 5. Tell long-running workers to exit so supervisor restarts them against new code
php artisan queue:restart

# 6. Reset PHP OPcache so php-fpm picks up the new bytecode
sudo systemctl reload php8.2-fpm
# or, without sudo:
# php artisan opcache:clear (via appstract/laravel-opcache) — or curl an internal endpoint that runs opcache_reset()
```

Why this order, in detail:

- **Migrate first, not after.** If migrations run *after* the symlink swap, the new code briefly runs against the old schema — every request in that window hits a "column not found" error.
- **`optimize:clear` before `config:cache`.** A common bug: a developer adds a new `.env` key but the cached config (`bootstrap/cache/config.php`) is stale. `config:cache` reads from the *runtime* config — but on a first run after the swap, the runtime config can itself include cached values from a prior release. Clearing first, then caching, guarantees the new release's `.env` and `config/*.php` are what end up in the compiled cache.
- **`view:cache` and `route:cache` BEFORE the symlink swap.** The compiled artifacts go to `bootstrap/cache/` *inside the release directory* — they belong to the release, not to the server. If you swap first and compile second, the very first request hits a route that hasn't been compiled yet and re-builds it on the fly. Fine — but slow, and your `route:cache` step then races against the request that triggered the lazy compile.
- **`queue:restart` AFTER the swap.** Workers gracefully finish the current job and exit. Your supervisor (systemd / supervisord / Horizon) respawns them against the symlink, which now points at the new release. Restarting *before* the swap means the new worker process started against the old code path — pointless restart.
- **OPcache reset last.** Until it runs, php-fpm is still executing bytecode it compiled from the previous release's file paths. The symlink swap doesn't invalidate OPcache; you have to do it explicitly.

This is the same sequence [zero-downtime deployments](https://www.deployhq.com/features/zero-downtime-deployments) implements internally — the symlink swap plus a php-fpm reload is what makes a deploy invisible to live traffic.

### 2. `migrate --force` with a safety rail

`--force` is required in production because the default `migrate` prompts for confirmation. The safety rail you actually want is `--pretend` in a pre-deploy step, so a destructive migration shows up in your CI logs before it hits the database:

```bash
#!/usr/bin/env bash
set -euo pipefail

# Run on the build host with a copy of the production schema
php artisan migrate --pretend > /tmp/migrate-plan.sql

# Block deploy if the plan contains keywords that should never appear unsupervised
BANNED='DROP TABLE|TRUNCATE TABLE|DROP DATABASE'
if grep -E "$BANNED" /tmp/migrate-plan.sql; then
  echo "::error::Destructive SQL detected in migration plan — review before deploy" >&2
  exit 1
fi

# Plan looks safe — show it in CI logs for audit and proceed
cat /tmp/migrate-plan.sql
```

In production then run `php artisan migrate --force --no-interaction` as the first deploy step. If the migration includes a DROP or TRUNCATE you intentionally need, override the gate explicitly (set `ALLOW_DESTRUCTIVE_MIGRATIONS=1` and skip the grep) — making it a conscious action.

### 3. `queue:restart` discipline

A Laravel queue worker is a long-running PHP process: it boots the framework once and processes jobs in a loop. That's a performance win — but it means the worker holds a *snapshot* of your code in memory. After a deploy, every running worker is still executing the *previous* release's job code.

`php artisan queue:restart` doesn't kill workers — it sets a flag in the cache. Workers check the flag between jobs and exit gracefully when they see it. Your supervisor (systemd / supervisord / Horizon) restarts them, and the new processes pick up the new code via the symlink.

```bash
# Last step of the post-deploy hook
php artisan queue:restart
```

Three gotchas the docs don't shout about:

1. **The cache store must be shared** between the deploy host and the workers' host. If the deploy script runs `queue:restart` against the local file cache and workers read from Redis, the flag never propagates. Set `CACHE_DRIVER=redis` (or anything shared) in production.
2. **Long jobs delay the restart.** A worker processing a 10-minute job won't see the flag for 10 minutes. Set `--max-time=300` (5 min) on the worker so it gracefully exits at most 5 minutes after a deploy.
3. **Horizon uses its own restart mechanism.** If you're on Horizon, the equivalent is `php artisan horizon:terminate` — `queue:restart` is a no-op against Horizon supervisors.

### 4. Maintenance mode with `--secret` for safe deploys

For deploys that include a migration that locks a hot table for 30+ seconds, `php artisan down` returns 503 to real traffic while you and the team can still access the site:

```bash
SECRET="bypass-$(openssl rand -hex 16)"
php artisan down \
  --secret="$SECRET" \
  --refresh=30 \
  --render="errors::503-deploy"

echo "Bypass URL: https://example.com/$SECRET" >&2
echo "Visit that URL once — your browser gets a cookie; subsequent requests skip 503"

# Run migration that locks the table
php artisan migrate --force --no-interaction

php artisan up
```

The `--secret` flag is the killer feature: a request to `https://example.com/<secret>` sets a cookie, and subsequent requests from that browser bypass the 503 — so you can verify the deploy from inside maintenance mode before flipping `up`.

For deploys that DON'T touch schema, skip maintenance mode entirely — the atomic symlink swap plus a php-fpm reload is invisible to real traffic and faster than a `down`/`up` cycle.

### 5. OPcache reset without sudo

Most deploy hooks run as a non-root user (`deploy`, `www-data`) without `sudo systemctl reload php-fpm` privileges. The cleanest workaround is an HTTP endpoint inside the app that calls `opcache_reset()`, locked to localhost:

```php
// routes/web.php — wired only when APP_ENV is production
Route::get('/_internal/opcache/reset', function (Request $request) {
    abort_unless($request->ip() === '127.0.0.1', 404);
    abort_unless(hash_equals(env('OPCACHE_RESET_TOKEN', ''), $request->header('X-Reset-Token', '')), 403);

    if (function_exists('opcache_reset')) {
        opcache_reset();
        return response()->noContent();
    }
    return response('opcache disabled', 503);
});
```

Then in the deploy hook:

```bash
curl -fsS -X POST -H "X-Reset-Token: $OPCACHE_RESET_TOKEN" \
  http://127.0.0.1/_internal/opcache/reset
```

`hash_equals()` is constant-time — guards against timing attacks even though the endpoint is localhost-only. Belt and braces; the cost is zero.

For deploys that *can* run as root (DeployHQ project running with a root-equivalent SSH key, or via passwordless sudo for one specific command), `sudo systemctl reload php8.2-fpm` is the simpler path.

### 6. Health-check endpoint that proves the new release is alive

The final deploy step should hit a `/health` endpoint that exercises the new code + database, not just `/` which might be served from a page cache:

```php
// routes/web.php
Route::get('/_internal/health', function () {
    $checks = [
        'app'   => app()->version(),
        'db'    => DB::selectOne('SELECT 1 AS ok')->ok === 1,
        'cache' => Cache::store()->put('hc', 'ok', 5) && Cache::get('hc') === 'ok',
        'queue' => Queue::size() < 10_000,
    ];
    $ok = !in_array(false, $checks, true);
    return response()->json(['status' => $ok ? 'ok' : 'fail'] + $checks, $ok ? 200 : 503);
});
```

```bash
# Final step in the deploy script
for _ in 1 2 3 4 5; do
  if curl -fsS -m 5 http://127.0.0.1/_internal/health -o /tmp/health.json; then
    jq -e '.status == "ok"' /tmp/health.json && break
  fi
  sleep 2
done

jq -e '.status == "ok"' /tmp/health.json \
  || { echo "health check failed after 5 attempts" >&2; exit 1; }
```

See the [jq cheatsheet](https://www.deployhq.com/cheatsheets/jq) for the assertion patterns that turn a JSON health response into a deploy gate.

---

## Common errors and fixes

| Error / symptom | Cause | Fix |
|---|---|---|
| `Application In Production!` prompt during `migrate` in CI | `--force` missing | Add `--force --no-interaction` to all destructive commands in CI |
| `Class "App\Models\Foo" not found` after deploy | Composer autoloader cached against old classes | `composer dump-autoload --optimize --classmap-authoritative` in deploy hook |
| `MethodNotAllowedHttpException` for a route that exists | `route:cache` stale (cached against previous release) | `php artisan route:clear` then `route:cache` in post-deploy |
| `No application encryption key has been specified` | `.env` deployed without `APP_KEY` | One-time `php artisan key:generate --force` then commit the encrypted env |
| Site shows old config values after deploy | Config cache wasn't rebuilt | `php artisan config:clear` then `config:cache` in post-deploy |
| Workers process jobs against old code | `queue:restart` never ran, or cache driver isn't shared | Set `CACHE_DRIVER=redis`; run `queue:restart` after symlink swap |
| `SQLSTATE[23000]: Integrity constraint violation` mid-deploy | Migration ordering — FK created before referenced table | Split into two migrations; or write a `Schema::disableForeignKeyConstraints()` block |
| `View [foo] not found` after deploy | `view:cache` referenced a removed view | `view:clear` then `view:cache` AFTER the file tree updates |
| `Allowed memory size exhausted` running `php artisan` in CI | Default 128M too small for app boot | `php -d memory_limit=512M artisan ...` or set globally |
| Maintenance bypass cookie ignored | Hosts behind CDN strip cookies | Configure CDN to forward cookie for the bypass path, or fetch via API |
| `Target class [Foo] does not exist.` after deploy | Service provider not registered or autoload not regenerated | Verify in `config/app.php` providers; re-run `composer dump-autoload` |
| `php artisan schedule:run` produces no output in cron | Output sent to `/dev/null 2>&1` — masking errors | Temporarily redirect to a log file to debug, then route back to `/dev/null` |
| `php-fpm` still serving previous release | OPcache holding old bytecode | Reload php-fpm or call `opcache_reset()` from a deploy hook |

---

## Companion: full DeployHQ deploy workflow

A Laravel deploy is the six-step post-deploy sequence in workflow 1, wrapped in everything Laravel doesn't run for you: `git archive` of the release, `composer install --no-dev --optimize-autoloader --classmap-authoritative` on the build host, asset compilation (`npm run build`), the atomic release directory layout (`releases/<sha>` + `current/` symlink), and a smoke test against the health endpoint.

The end-to-end pattern is documented in the [deploy from GitHub guide](https://www.deployhq.com/deploy-from-github), and the [build pipeline](https://www.deployhq.com/features/build-pipelines) is where `composer install` and `npm run build` run — your production hosts never see source code or build tooling, only the resolved artifact. If a release ships a bad migration, [one-click rollback](https://www.deployhq.com/features/one-click-rollback) swaps the symlink back; you re-run `php artisan migrate:rollback` from the new-active release to undo the schema change.

[Start a free DeployHQ trial](https://www.deployhq.com/signup) to wire `artisan` into a production deploy pipeline in minutes.

---

## Related cheatsheets

- [Composer cheatsheet](https://www.deployhq.com/cheatsheets/composer) — for the `composer install --no-dev --optimize-autoloader` step that precedes every Artisan command in this sheet.
- [WP-CLI cheatsheet](https://www.deployhq.com/cheatsheets/wp-cli) — the sibling CLI for WordPress deploys with parallel lifecycle patterns.
- [Cron and Crontab cheatsheet](https://www.deployhq.com/cheatsheets/cron) — for the `* * * * * php artisan schedule:run` entry every Laravel host needs.
- [jq cheatsheet](https://www.deployhq.com/cheatsheets/jq) — for parsing the health-check JSON and webhook payloads referenced in workflow 6.
- [Bash cheatsheet](https://www.deployhq.com/cheatsheets/bash) — for the `set -euo pipefail` orchestration around the six-step post-deploy.
- [Cheatsheets hub](https://www.deployhq.com/cheatsheets) — every DeployHQ cheatsheet in one place.

---

Need help? Email [support@deployhq.com](mailto:support@deployhq.com) or follow [@deployhq on X](https://x.com/deployhq).