Rescue your software
Moving a Live Laravel Platform's Uploads to Object Storage: The Playbook We Reuse
The playbook we reuse to move a live Laravel app's uploads from local disk to S3-compatible object storage without breaking downloads or previews.
MindForge Engineering4 min read
If a Laravel application stores user uploads on the server's own disk, that server becomes impossible to replace without a careful copy job. Moving those files to S3-compatible object storage fixes that, and we have now done it on several live Laravel platforms: a multivendor booking marketplace, a portfolio and CV-builder SaaS, and a multi-tenant course marketplace. Each was a licensed platform that a client needed running reliably in production.
The playbook below is the one we reuse. It is deliberately boring, which is what you want when the files belong to real users.
Why local disk becomes a problem
Local storage feels fine until one of these happens:
- You need to move to a new host, and the uploads have to travel with the code.
- You want more than one application server, and each one sees a different set of files.
- The disk fills up, and the only fix is resizing the server that runs everything else.
Object storage separates files from the machine. Laravel supports this through its filesystem layer and the s3 driver, which also works with S3-compatible services by setting a custom endpoint, as described in the Laravel filesystem documentation. In these projects the target was DigitalOcean Spaces, which speaks the S3 API.
Step 1: Find every place the app touches files
Licensed platforms rarely use one consistent storage call. Uploads are handled in one controller with the Storage facade, in another with a direct file move, and somewhere else with a helper written years ago.
Before changing any configuration, we list every upload, update and delete path. On the course marketplace alone, that inventory covered roughly 23 controllers. The inventory is the plan: anything missing from it becomes a broken image in production.
Step 2: Route everything through one storage helper
Next, every one of those paths is changed to call a single storage helper instead of talking to the disk directly. The helper decides which disk to use, how to name files and how to build their public URLs.
This is the step that makes the migration safe. Once all file handling goes through one place, switching from local disk to object storage is a configuration change in that helper, not a hunt through the codebase. It also means the next developer has one place to read.
Step 3: Configure the new disk and move the files
With one code path in place:
- Add the S3-compatible disk to the filesystem configuration, with credentials and endpoint in environment variables, never in code.
- Copy existing files to the bucket with the same relative paths, so stored references stay valid.
- Switch the helper to the new disk.
- Keep the old files until the new setup has been verified in production.
Step 4: Test the read paths hardest
Uploads are the obvious thing to test. The failures that reach users usually come from reading files back.
On the portfolio builder, CV downloads were the path that needed several rounds of debugging and correction before they behaved correctly from object storage. The same project surfaced a localisation fallback problem on profile views, fixed in the same pass. Neither shows up if you only test "can I upload a picture".
Our read-path checklist covers:
- Public images and thumbnails in every place they render.
- File downloads, especially ones that set a custom filename.
- Pages that render files in more than one language or theme.
- Admin screens that list or preview uploaded files.
Step 5: Harden the environment in the same move
A storage migration usually coincides with a new host, so we treat hardening as part of the job rather than a later ticket:
- HTTPS enforcement, so file URLs and pages are never served over plain HTTP.
- Environment configuration for the new host, with secrets out of the repository.
- Locked dependency builds, installing exactly what the lock file specifies, so production matches what was tested.
When a platform needs more than a helper
On a multi-tenant platform, a single helper is not quite enough, because some tenants' files will not have been migrated yet at any given moment. That case needs a URL generator that can resolve either location and fall back gracefully, which we cover in centralising file resolution in a multi-tenant SaaS.
A checklist before you migrate
- Inventory every file write, update, delete and read.
- Route them through one helper before changing drivers.
- Keep credentials and endpoints in environment variables.
- Copy files with identical relative paths.
- Test downloads, previews and localised views, not just uploads.
- Enforce HTTPS and lock dependencies as part of the move.
- Keep the old files until production has been verified.
You can read the anonymised write-ups for the booking marketplace, the portfolio builder and the course marketplace. This kind of work sits under our software rescue service.
Client details in this post are anonymised to respect confidentiality. The engineering described is from the project listed below.