For zero-downtime deploys of a busy Laravel ticketing platform using release directories and a current symlink, what is shared, what is per release, and in what order do the steps run?
answer
- releases/<timestamp> plus current symlink
- storage and .env are shared
- bootstrap/cache and vendor per release
- optimize inside the release, then switch
- atomic swap, reload, keep old releases
basics
~20 sEach deploy builds a new releases/<timestamp> directory with its own vendor, assets and bootstrap/cache, symlinks in a shared storage directory and .env, migrates and runs optimize there, then atomically repoints current and runs php artisan reload.
solid answer
~40 sBuild each release in `releases/<timestamp>`: check out the code, symlink `storage` to `shared/storage` and `.env` to `shared/.env`, run `composer install --no-dev`, build assets, run `php artisan migrate --force` and then `php artisan optimize` inside that directory. Only then swap `current` to the new release atomically (create a temporary symlink and rename it over `current`) and run `php artisan reload`. Shared: `storage` (logs, file sessions, local uploads, the maintenance file) and `.env`. Per release: code, `vendor`, `public/build` and `bootstrap/cache`, whose cached config holds that release's absolute paths. Both `storage` and `bootstrap/cache` must be writable by the PHP user. Keep a few old releases so rollback is a symlink swap plus reload, remembering migrations are not reversed by it, so they must be compatible with the release still serving.
go deeper
Recall that each deploy builds a new release directory and switches a current symlink, with storage and .env shared.
Explain which directories are shared or per release, why bootstrap/cache is per release, and the order of install, migrate, optimize, switch, reload.
Make the swap atomic, keep migrations backward compatible with the serving release, and design rollback as a symlink swap plus reload.
Decide how many releases to keep, what rollback promises about data, and when a managed platform should own this process.
## The layout A zero-downtime deploy never modifies the code that is serving traffic. Each deploy builds a complete new directory next to the old one and then switches to it in one step: ```bash /var/www/tickets/ releases/ 20260928T1012/ 20260929T0930/ shared/ storage/ .env current -> releases/20260929T0930 ``` The web server's document root is `current/public`, so switching the `current` symlink switches the site. ## Shared versus per release | Item | Where it lives | Why | |---|---|---| | `storage/` | `shared/storage`, symlinked into each release | logs, file sessions, local uploads, `storage/framework/down` must survive releases | | `.env` | `shared/.env`, symlinked | one environment definition for every release | | code, `vendor/` | per release | each release has its own dependencies from its own `composer.lock` | | `public/build` | per release | Vite assets match that release's code | | `bootstrap/cache/` | per release | cached config, routes, events, packages and services describe **this** release | `bootstrap/cache` must not be shared. `config:cache` resolves `base_path()` and `storage_path()` to absolute paths inside the release directory, and the route and event caches describe that release's code. Sharing it would let one release read another's caches. Both `storage` and `bootstrap/cache` must be **writable** by the PHP process: the docs name exactly these two directories. ## The order of steps 1. Create `releases/<timestamp>` and put the code there. 2. Link `storage` and `.env` from `shared/`. 3. `composer install --no-dev --no-interaction` in the release. 4. Build front-end assets. 5. `php artisan migrate --force` from the new release. 6. `php artisan optimize` in the new release. 7. Swap `current` atomically. 8. `php artisan reload`, run from `current`. 9. Delete releases beyond the last few. The atomic swap matters: removing `current` and recreating it leaves a moment with no document root. Creating a new link under a temporary name and renaming it over `current` replaces it in one filesystem operation. ```bash ln -sfn releases/20260929T0930 current_tmp mv -Tf current_tmp current ``` ## Traps specific to Laravel - **Migrations run before the switch.** For a short time the old release serves traffic against the new schema. Migrations must be backward compatible with the release still serving: add columns before using them, remove them a release later. - **Caches built in the wrong directory.** Running `optimize` from `current` before the swap builds caches for the old release. - **PHP's opcode cache and resolved paths.** If PHP resolves `current` once and caches the result, it can keep executing the old release; web servers are usually configured to pass the resolved real path. That is PHP and web-server territory rather than Laravel's. - **Compiled views in shared storage.** Blade names compiled files by a hash of each template's absolute path, so each release writes its own compiled files into the shared `storage/framework/views`; clearing old ones occasionally keeps it small. ## Housekeeping - **Prune old releases** after a successful deploy, keeping enough for a rollback (three to five is common). - **Keep `shared/` out of pruning**; it is the only place user uploads and logs survive. - **Create writable directories with the right group** when the release is built, so `bootstrap/cache` is writable by the PHP user from the start. ## Rollback Rolling back is pointing `current` at the previous release and running `php artisan reload`. The previous release still has its own `vendor` and `bootstrap/cache`, so nothing needs rebuilding. What it does **not** undo is the database: migrations already applied stay applied, which is another reason to keep them backward compatible.
- Why should bootstrap/cache stay inside each Laravel release directory instead of being shared?Its files describe one release: the cached config contains absolute paths into that release directory, and the route, event, services and packages caches reflect that release's code and dependencies. Sharing it would let a new release read an old release's caches, or break rollback by overwriting the previous release's caches.
- Why run php artisan optimize in the new release directory before switching the current symlink rather than after?Before the switch, requests still go to the old release, so building caches has no effect on live traffic. After the switch, the first requests would hit an uncached release. Building inside the new directory also bakes the correct release paths into the config cache.
- Does switching the current symlink back roll back a Laravel release completely?It restores the code, `vendor` and caches of the previous release, and `php artisan reload` restarts workers on it. It does not reverse migrations; the schema stays at the newer version, so migrations must be written so the previous release keeps working against it.
saying these in an interview costs you the question
- storage should be copied fresh into every release
- bootstrap/cache can be shared like storage
- Delete current and recreate it; the gap is negligible
- Switching the symlink back also rolls back migrations
- Run optimize in current before switching the symlink