Laravel

Automations

A new Laravel release isn't ready the moment its container starts. Storage needs its link. The database needs its migrations. Config, routes, views, and events need caching. Set one environment variable, and the container does all of it before it serves a request.

compose.yml
services:
  app:
    image: my-org/my-app:1.0.0
    ports:
      - "80:8080"
    environment:
      AUTORUN_ENABLED: "true"

That's the whole setup. There's no entrypoint script to write and no deploy step to remember. The official PHP images leave this work to you.

Three steps run on every start

php artisan storage:link connects public/storage to your app's public files. When the link already exists, the script moves on.

Migrate the database

The script waits until your database accepts connections. It clears the config cache, then runs php artisan migrate --force.

Cache the app

php artisan optimize caches your config, routes, views, and events. It's the command Laravel recommends for every production deploy.

Then the web server starts, or your own command runs. Every step is on by default. Set a step's variable to false to skip it.

StepVariableDefault
Link storageAUTORUN_LARAVEL_STORAGE_LINKtrue
Migrate the databaseAUTORUN_LARAVEL_MIGRATIONtrue
Cache the appAUTORUN_LARAVEL_OPTIMIZEtrue

Your logs show each step

The container prints a line as each step starts. Laravel's own output appears between them.

docker compose logs app
πŸ” Running storage link: "php artisan storage:link"...
πŸš€ Clearing Laravel cache before attempting migrations...
⚑️ Attempting connection to default database...
βœ… Database connection successful
πŸš€ Running migrations for default database
πŸš€ Running optimize command: "php artisan optimize"...

The startup banner above these lines shows Automations: true whenever the script is on.

Turn it on for one service

The script runs in every container that starts with AUTORUN_ENABLED=true, whatever its command. Set it on your web service and leave it off your workers. Each deployment then migrates once.

compose.yml
services:
  app:
    image: my-org/my-app:1.0.0
    ports:
      - "80:8080"
    environment:
      AUTORUN_ENABLED: "true"

  queue:
    image: my-org/my-app:1.0.0
    command: ["php", "artisan", "queue:work"]

One-off commands count too. docker compose run app php artisan tinker starts a new app container, and that container runs the automations first. Running workers has a full example with Horizon and the scheduler.

Migrations wait for your database

Your database container may still be starting when your app comes up. The script tries to connect once a second for up to 30 seconds. As soon as the database answers, it migrates.

VariableDefaultWhat it does
AUTORUN_LARAVEL_MIGRATION_TIMEOUT30Seconds to wait for the database.
AUTORUN_LARAVEL_MIGRATION_SKIP_DB_CHECKfalsetrue migrates without waiting.
AUTORUN_LARAVEL_MIGRATION_DATABASE""Connections to migrate in turn, such as mysql,pgsql. Empty uses your default connection.
AUTORUN_LARAVEL_MIGRATION_ISOLATIONfalsetrue adds --isolated.
AUTORUN_LARAVEL_MIGRATION_SEEDfalsetrue adds --seed.
AUTORUN_LARAVEL_MIGRATION_FORCEtruefalse removes --force.
AUTORUN_LARAVEL_MIGRATION_MODEdefaultfresh or refresh runs migrate:fresh or migrate:refresh.

Run several replicas safely

When you scale a service to several containers, they all start at once. --isolated takes a lock, and only one container migrates at a time. It needs Laravel 9.38.0 or newer, the default migration mode, and a cache server every container shares, such as Redis or your database. A file or array cache gives each container its own lock, so they all migrate at once.

The flag needs the migrations table to exist. On your first deployment, the script leaves it off and adds it from the next one.

fresh drops every table, and refresh rolls back every migration. Use them only in development or testing.

Choose what gets cached

Each cache has its own variable. All four are on by default.

VariableDefaultCaches
AUTORUN_LARAVEL_CONFIG_CACHEtrueConfig
AUTORUN_LARAVEL_ROUTE_CACHEtrueRoutes
AUTORUN_LARAVEL_VIEW_CACHEtrueBlade views
AUTORUN_LARAVEL_EVENT_CACHEtrueEvents

With every cache on, the script runs php artisan optimize. Packages can add their own steps to that command, and they run too.

Turn one cache off, and the script runs php artisan optimize --except=... without it. Laravel added --except in 11.38.0. On older versions, the script runs one cache command for each cache that's still on.

Set AUTORUN_LARAVEL_OPTIMIZE=false to skip optimize and its package steps. The script still runs a cache command for each cache variable set to true. Set those to false too, and nothing gets cached.

A broken release never serves traffic

If a step fails, the script exits with an error. The container stops before the web server starts. A failing migration shows up as a container that won't start, with the error in its logs.

The script also stops when it can't find Laravel. It looks for artisan and a vendor directory in APP_BASE_DIR. Run composer install when you build the image. If a container can start before Laravel is installed, set AUTORUN_LARAVEL_SKIP_IF_NOT_FOUND=true to skip the automations instead.

Two variables help you find the cause:

  • AUTORUN_DEBUG=true prints the script's settings and each connection attempt.
  • LOG_OUTPUT_LEVEL=debug turns on debug output for every startup script.

Keep it for production

In development, you change routes and config all day. Cached copies hide those changes until the container restarts. Leave AUTORUN_ENABLED off on your machine, its default, and run artisan yourself.

To turn the automations off anywhere, set AUTORUN_ENABLED=false.

Every AUTORUN_* variable is in the environment variable specification.