skip to content

In Laravel, how do FILESYSTEM_DISK and Storage::disk() pick the disk a Storage call writes to, and what does moving uploads to S3 require?

level: middleimportance: must knowfreq 60%

answer

  1. config default reads an env variable
  2. FILESYSTEM_DISK falls back to local
  3. Storage::disk('name') overrides the default
  4. unknown disk: InvalidArgumentException
  5. league/flysystem-aws-s3-v3 plus AWS_* keys

basics

~20 s

Storage calls without disk() go to the default disk, config filesystems.default, read from FILESYSTEM_DISK and falling back to local. Storage::disk('s3') picks a named disk. Moving to S3 needs the Flysystem S3 package, AWS credentials and bucket settings.

solid answer

~30 s

`config/filesystems.php` sets `'default' => env('FILESYSTEM_DISK', 'local')`, and any `Storage` facade call made without `disk()` is forwarded by the `FilesystemManager` to that default disk. `Storage::disk('s3')` resolves a named entry under `filesystems.disks` (a string or a backed enum), and the manager caches the resolved instance per name. A name with no configured driver throws `InvalidArgumentException`. To move a portfolio site's uploads to S3 you `composer require league/flysystem-aws-s3-v3`, fill `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_DEFAULT_REGION` and `AWS_BUCKET`, and either set `FILESYSTEM_DISK=s3` or call `Storage::disk('s3')` explicitly. Code that hard-codes `disk('public')` ignores the default, and existing files do not move by themselves.

code

ini · 6 lines
ini
FILESYSTEM_DISK=s3
AWS_ACCESS_KEY_ID=your-key-id
AWS_SECRET_ACCESS_KEY=your-secret
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=portfolio-uploads
AWS_USE_PATH_STYLE_ENDPOINT=false

go deeper

for a junior

Know that Storage calls without disk() use the default disk from FILESYSTEM_DISK, which is local in a fresh app, and that disk('name') picks another.

for a middle

Walk through how the manager resolves and caches a disk, the exceptions for unknown names or drivers, and the package plus AWS_* settings S3 needs.

for a senior

Plan the switch: audit hard-coded disk names, handle existing files before flipping the default, and choose credentials that avoid static keys in production.

for a principal

Decide which files belong on shared object storage and which may stay local, and make disk names a team convention rather than scattered strings.

## Where the default comes from Laravel's filesystem layer is built on **Flysystem**, a PHP library that gives one API over many storage backends. Laravel wraps it in the `FilesystemManager`, which the `Storage` facade resolves from the service container. The manager needs to know which **disk** (named configuration) to use when you do not say. That is the `default` key in `config/filesystems.php`: ```php 'default' => env('FILESYSTEM_DISK', 'local'), ``` The Laravel 13 skeleton's `.env.example` sets `FILESYSTEM_DISK=local`, and the fallback is also `local`, so a fresh app writes to `storage/app/private` until you change it. ## How a Storage call finds its disk 1. `Storage::put('portfolios/9/cover.jpg', $bytes)` hits the facade, which forwards to the `FilesystemManager`. 2. The manager has no `put` method of its own, so its `__call` passes the call to `disk()` with no name. 3. `disk(null)` looks up `filesystems.default` and resolves that disk. 4. `Storage::disk('s3')->put(...)` skips step 3 and resolves the `s3` entry under `filesystems.disks`. Resolution reads the disk's `driver` key and calls the matching `create{Driver}Driver` method (`createLocalDriver`, `createS3Driver`, `createFtpDriver`, `createSftpDriver`, `createScopedDriver`, `createReadThroughDriver`) or a custom creator registered with `Storage::extend()`. The result is stored in the manager's `$disks` array, so later calls with the same name reuse one instance for the rest of the process. `disk()` accepts a string or an enum (the value is unwrapped with `enum_value`), which lets a team keep disk names in one backed enum instead of scattering string literals. ## Failure modes when naming a disk | Situation | What happens | |---|---| | Name not in `filesystems.disks` | `InvalidArgumentException`: `Disk [media] does not have a configured driver.` | | `driver` value with no creator | `InvalidArgumentException`: `Driver [dropbox] is not supported.` | | `s3` driver without the Flysystem S3 package | a class-not-found error when the disk is first resolved | None of these fall back to the default disk silently; a typo in a disk name fails loudly on first use. ## Moving a portfolio site from local disk to S3 A design-portfolio site that grows past one server needs its uploads in shared object storage. The Laravel side of the switch: - **Install the adapter.** `composer require league/flysystem-aws-s3-v3 "^3.0" --with-all-dependencies`; the framework only *suggests* it. - **Fill the environment.** The skeleton's `s3` disk reads `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_DEFAULT_REGION`, `AWS_BUCKET`, `AWS_URL`, `AWS_ENDPOINT` and `AWS_USE_PATH_STYLE_ENDPOINT`. - **Credentials without static keys.** Laravel passes `key`/`secret` to the S3 client only when both are non-empty; the disk can instead take a `credentials` entry whose provider is `ecs` or `instance`, and with neither the AWS SDK looks credentials up itself. - **Choose the switch.** `FILESYSTEM_DISK=s3` moves every default-disk call at once; explicit `Storage::disk('s3')` (or an enum case) moves only the upload code and leaves, say, generated reports on the local disk. - **Audit hard-coded names.** Any `Storage::disk('public')` call keeps writing to the local public folder whatever the default says. - **Expect private objects.** The skeleton's `s3` disk sets no `visibility`, so objects are written private unless a visibility is configured or passed on write. - **Existing files stay put.** Changing the default does not copy a single byte; old paths now point at a bucket where they do not exist. Plan a migration (a backfill command, or a read-through disk) before flipping the switch. ## Rolling the switch out safely - **Try the disk before depending on it.** A one-off `Storage::disk('s3')->put('healthcheck.txt', 'ok')` from `php artisan tinker` on the target server shows whether credentials, region and bucket are right. - **Turn on failures while testing.** The skeleton sets `'throw' => false`, so a misconfigured disk makes writes return `false` quietly; `'throw' => true` surfaces the underlying Flysystem exception, and `'report' => true` logs it without throwing. - **Switch one caller at a time.** Pointing only the upload code at `Storage::disk('s3')` first limits the blast radius; flipping `FILESYSTEM_DISK` later catches the stragglers. - **Keep paths relative.** Storing `portfolios/9/cover.jpg` rather than a full URL in the database is what lets the same row work on any disk. ## Why disks are named, not hard-wired Because every caller asks for a disk by name, the same code runs against `local` on a laptop and `s3` in production; only configuration changes. That portability is the point of the abstraction, and also its trap: the API is identical, but a remote disk makes a network call for each operation, so code that was cheap against the local disk can become slow after the switch.

  • What happens when code calls Storage::disk('media') and no media disk is configured?
    The manager finds an empty config for that name and throws `InvalidArgumentException` with the message `Disk [media] does not have a configured driver.` It never falls back to the default disk, so a typo fails on first use instead of writing somewhere unexpected.
  • After setting FILESYSTEM_DISK=s3, why do old portfolio images break?
    Changing the default only changes where new calls go. The stored relative paths now resolve against the bucket, where the old files were never uploaded. You need a backfill that copies existing files to the bucket, or a read-through disk that serves them from the old disk until they are promoted.
  • Is Storage::disk('s3') resolved again on every call?
    No. The `FilesystemManager` caches each resolved disk in its `$disks` array by name, so repeated calls in one process reuse the same adapter and S3 client. `Storage::forgetDisk()` or `purge()` drops the cached instance when configuration changes at runtime.

saying these in an interview costs you the question

  • Storage::put() always writes to the public disk
  • An unknown disk name silently falls back to the default disk
  • Setting FILESYSTEM_DISK=s3 also moves existing files into the bucket
  • The s3 driver works out of the box without installing a Flysystem package
  • Storage::disk('public') follows FILESYSTEM_DISK when the default changes